@idealyst/theme 1.2.102 → 1.2.104

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idealyst/theme",
3
- "version": "1.2.102",
3
+ "version": "1.2.104",
4
4
  "description": "Theming system for Idealyst Framework",
5
5
  "readme": "README.md",
6
6
  "main": "src/index.ts",
@@ -63,7 +63,7 @@
63
63
  "publish:npm": "npm publish"
64
64
  },
65
65
  "dependencies": {
66
- "@idealyst/tooling": "^1.2.102"
66
+ "@idealyst/tooling": "^1.2.104"
67
67
  },
68
68
  "peerDependencies": {
69
69
  "react-native-unistyles": ">=3.0.0"
@@ -66,10 +66,18 @@ function getOrCreateEntry(componentName) {
66
66
  // AST Deep Merge - Merges style object ASTs at build time
67
67
  // ============================================================================
68
68
 
69
+ // Platform-specific keys used by Unistyles
70
+ const PLATFORM_KEYS = new Set(['_web', '_ios', '_android']);
71
+
69
72
  /**
70
73
  * Deep merge two ObjectExpression ASTs.
71
74
  * Source properties override target properties.
72
75
  * Nested objects are recursively merged.
76
+ *
77
+ * Also propagates extension properties into platform-specific blocks (_web, _ios, _android)
78
+ * when those blocks already contain the same key. This ensures that e.g. setting
79
+ * `fontFamily: 'MyFont'` in an extension properly overrides `_web: { fontFamily: 'inherit' }`
80
+ * in the base styles.
73
81
  */
74
82
  function mergeObjectExpressions(t, target, source) {
75
83
  if (!t.isObjectExpression(target) || !t.isObjectExpression(source)) {
@@ -90,6 +98,9 @@ function mergeObjectExpressions(t, target, source) {
90
98
 
91
99
  const resultProps = [...target.properties];
92
100
 
101
+ // Collect non-platform source property keys and values for propagation
102
+ const sourceNonPlatformProps = new Map();
103
+
93
104
  for (const prop of source.properties) {
94
105
  if (!t.isObjectProperty(prop)) continue;
95
106
 
@@ -97,6 +108,10 @@ function mergeObjectExpressions(t, target, source) {
97
108
  t.isStringLiteral(prop.key) ? prop.key.value : null;
98
109
  if (!key) continue;
99
110
 
111
+ if (!PLATFORM_KEYS.has(key)) {
112
+ sourceNonPlatformProps.set(key, prop);
113
+ }
114
+
100
115
  const existingProp = targetProps.get(key);
101
116
 
102
117
  if (existingProp) {
@@ -111,11 +126,42 @@ function mergeObjectExpressions(t, target, source) {
111
126
  const idx = resultProps.indexOf(existingProp);
112
127
  resultProps[idx] = t.objectProperty(existingProp.key, mergedValue);
113
128
  }
114
- // If both are arrow functions (dynamic styles), merge their return values
115
- else if (t.isArrowFunctionExpression(existingValue) && t.isArrowFunctionExpression(newValue)) {
116
- const mergedFn = mergeDynamicStyleFunctions(t, existingValue, newValue);
117
- const idx = resultProps.indexOf(existingProp);
118
- resultProps[idx] = t.objectProperty(existingProp.key, mergedFn);
129
+ // If base is a dynamic function and extension is anything (plain object
130
+ // or arrow function), extract the extension's object and merge it into the
131
+ // base function's return body. Extensions should only provide plain objects,
132
+ // but if a function is given we unwrap its body to get the object.
133
+ else if (t.isArrowFunctionExpression(existingValue)) {
134
+ let extObj = newValue;
135
+ // If extension mistakenly provides a function, unwrap its return body
136
+ if (t.isArrowFunctionExpression(extObj)) {
137
+ extObj = extObj.body;
138
+ if (t.isParenthesizedExpression(extObj)) {
139
+ extObj = extObj.expression;
140
+ }
141
+ }
142
+
143
+ if (t.isObjectExpression(extObj)) {
144
+ let baseBody = existingValue.body;
145
+ if (t.isParenthesizedExpression(baseBody)) {
146
+ baseBody = baseBody.expression;
147
+ }
148
+ if (t.isObjectExpression(baseBody)) {
149
+ const mergedBody = mergeObjectExpressions(t, baseBody, extObj);
150
+ const idx = resultProps.indexOf(existingProp);
151
+ resultProps[idx] = t.objectProperty(
152
+ existingProp.key,
153
+ t.arrowFunctionExpression(existingValue.params, mergedBody)
154
+ );
155
+ } else {
156
+ // Block body or other complex form — fall back to replacement
157
+ const idx = resultProps.indexOf(existingProp);
158
+ resultProps[idx] = prop;
159
+ }
160
+ } else {
161
+ // Extension body isn't an object — fall back to replacement
162
+ const idx = resultProps.indexOf(existingProp);
163
+ resultProps[idx] = prop;
164
+ }
119
165
  }
120
166
  // Otherwise, source replaces target
121
167
  else {
@@ -128,34 +174,49 @@ function mergeObjectExpressions(t, target, source) {
128
174
  }
129
175
  }
130
176
 
131
- return t.objectExpression(resultProps);
132
- }
133
-
134
- /**
135
- * Merge two dynamic style functions (arrow functions that return style objects).
136
- * Creates a new function that merges both return values.
137
- */
138
- function mergeDynamicStyleFunctions(t, baseFn, extFn) {
139
- // Get the bodies (assuming they return ObjectExpressions)
140
- let baseBody = baseFn.body;
141
- let extBody = extFn.body;
177
+ // Propagate extension properties into platform-specific blocks.
178
+ // If the base has _web: { fontFamily: 'inherit' } and the extension sets
179
+ // fontFamily: 'MyFont' at the top level, we need to also override fontFamily
180
+ // inside the _web block so the platform-specific value doesn't shadow the extension.
181
+ if (sourceNonPlatformProps.size > 0) {
182
+ for (let i = 0; i < resultProps.length; i++) {
183
+ const prop = resultProps[i];
184
+ if (!t.isObjectProperty(prop)) continue;
142
185
 
143
- // Handle parenthesized expressions
144
- if (t.isParenthesizedExpression(baseBody)) {
145
- baseBody = baseBody.expression;
146
- }
147
- if (t.isParenthesizedExpression(extBody)) {
148
- extBody = extBody.expression;
149
- }
186
+ const key = t.isIdentifier(prop.key) ? prop.key.name :
187
+ t.isStringLiteral(prop.key) ? prop.key.value : null;
188
+ if (!key || !PLATFORM_KEYS.has(key)) continue;
189
+ if (!t.isObjectExpression(prop.value)) continue;
190
+
191
+ // Check if any source properties conflict with keys in this platform block
192
+ const platformProps = prop.value.properties;
193
+ let modified = false;
194
+ const newPlatformProps = [...platformProps];
195
+
196
+ for (let j = 0; j < newPlatformProps.length; j++) {
197
+ const platProp = newPlatformProps[j];
198
+ if (!t.isObjectProperty(platProp)) continue;
199
+
200
+ const platKey = t.isIdentifier(platProp.key) ? platProp.key.name :
201
+ t.isStringLiteral(platProp.key) ? platProp.key.value : null;
202
+ if (!platKey) continue;
203
+
204
+ const extProp = sourceNonPlatformProps.get(platKey);
205
+ if (extProp) {
206
+ // Extension has a property that conflicts with this platform block key.
207
+ // Override the platform block value with the extension value.
208
+ newPlatformProps[j] = t.objectProperty(platProp.key, t.cloneDeep(extProp.value));
209
+ modified = true;
210
+ }
211
+ }
150
212
 
151
- // If both return ObjectExpressions directly, merge them
152
- if (t.isObjectExpression(baseBody) && t.isObjectExpression(extBody)) {
153
- const mergedBody = mergeObjectExpressions(t, baseBody, extBody);
154
- return t.arrowFunctionExpression(baseFn.params, mergedBody);
213
+ if (modified) {
214
+ resultProps[i] = t.objectProperty(prop.key, t.objectExpression(newPlatformProps));
215
+ }
216
+ }
155
217
  }
156
218
 
157
- // For block statements, this is more complex - just use extension for now
158
- return extFn;
219
+ return t.objectExpression(resultProps);
159
220
  }
160
221
 
161
222
  /**
@@ -873,7 +934,10 @@ module.exports = function idealystStylesPlugin({ types: t }) {
873
934
  opts.processAll ||
874
935
  (opts.autoProcessPaths?.some(p => filename.includes(p)));
875
936
 
876
- if (!shouldProcess) return;
937
+ // extendStyle/overrideStyle must ALWAYS be processed regardless of
938
+ // shouldProcess, since they are called from user code (not just from
939
+ // @idealyst/* packages). Only defineStyle and StyleSheet.create
940
+ // $iterator expansion are gated by autoProcessPaths.
877
941
 
878
942
  // ============================================================
879
943
  // Handle extendStyle - Store extension AST for later merging
@@ -965,6 +1029,10 @@ module.exports = function idealystStylesPlugin({ types: t }) {
965
1029
  return;
966
1030
  }
967
1031
 
1032
+ // defineStyle and StyleSheet.create are only processed for files
1033
+ // matching autoProcessPaths (i.e., framework packages)
1034
+ if (!shouldProcess) return;
1035
+
968
1036
  // ============================================================
969
1037
  // Handle defineStyle - Merge with extensions and output StyleSheet.create
970
1038
  // ============================================================
@@ -1,10 +1,71 @@
1
- import { UnistylesRuntime } from 'react-native-unistyles';
1
+ import { StyleSheet, UnistylesRuntime } from 'react-native-unistyles';
2
+ import type { UnistylesThemes } from 'react-native-unistyles';
3
+ import type { BuiltTheme } from './builder';
2
4
 
3
5
  /**
4
6
  * Color scheme preference type.
5
7
  */
6
8
  export type ColorScheme = 'light' | 'dark';
7
9
 
10
+ /**
11
+ * Any theme produced by the builder system (fromTheme().build() or createTheme().build()).
12
+ */
13
+ type AnyBuiltTheme = BuiltTheme<string, string, string, string, string, string, string, string, string>;
14
+
15
+ /**
16
+ * Options for configureThemes().
17
+ */
18
+ export interface ConfigureThemesOptions {
19
+ /**
20
+ * Theme instances keyed by name. Must include at least `light` and `dark`.
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * { light: customLightTheme, dark: customDarkTheme }
25
+ * ```
26
+ */
27
+ themes: { light: AnyBuiltTheme; dark: AnyBuiltTheme } & Record<string, AnyBuiltTheme>;
28
+
29
+ /**
30
+ * Which theme to activate on startup (default: 'light').
31
+ */
32
+ initialTheme?: string;
33
+ }
34
+
35
+ /**
36
+ * Configure the theme system. Call this **once** at app startup,
37
+ * before any component renders.
38
+ *
39
+ * This replaces the need to import `StyleSheet` from `react-native-unistyles` directly.
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * import { configureThemes, lightTheme, darkTheme, fromTheme } from '@idealyst/theme';
44
+ *
45
+ * const light = fromTheme(lightTheme).build();
46
+ * const dark = fromTheme(darkTheme).build();
47
+ *
48
+ * configureThemes({ themes: { light, dark } });
49
+ * ```
50
+ *
51
+ * @example
52
+ * ```typescript
53
+ * // With a custom initial theme
54
+ * configureThemes({
55
+ * themes: { light, dark, midnight },
56
+ * initialTheme: 'midnight',
57
+ * });
58
+ * ```
59
+ */
60
+ export function configureThemes(options: ConfigureThemesOptions): void {
61
+ StyleSheet.configure({
62
+ themes: options.themes as Record<string, object> as UnistylesThemes,
63
+ settings: {
64
+ initialTheme: (options.initialTheme ?? 'light') as keyof UnistylesThemes,
65
+ },
66
+ });
67
+ }
68
+
8
69
  /**
9
70
  * Get the current system/device color scheme preference.
10
71
  *
@@ -27,19 +88,21 @@ export function getColorScheme(): ColorScheme | null {
27
88
  }
28
89
 
29
90
  /**
30
- * Theme settings controller - wraps Unistyles runtime for theme management.
91
+ * Theme settings controller wraps Unistyles runtime for theme management.
92
+ *
93
+ * Use this instead of importing `UnistylesRuntime` directly.
31
94
  */
32
95
  export const ThemeSettings = {
33
96
  /**
34
97
  * Set the active theme by name with content color scheme.
35
98
  *
36
- * @param themeName - The theme name to activate
99
+ * @param themeName - The theme name to activate (e.g. 'light', 'dark')
37
100
  * @param contentColor - The content color scheme ('light' or 'dark')
38
101
  * @param animated - Whether to animate the status bar transition (default: false)
39
102
  *
40
103
  * @example
41
104
  * ```typescript
42
- * ThemeSettings.setTheme('darkBlue', 'dark');
105
+ * ThemeSettings.setTheme('dark', 'dark');
43
106
  * ThemeSettings.setTheme('light', 'light', true); // animated
44
107
  * ```
45
108
  */
@@ -50,4 +113,36 @@ export const ThemeSettings = {
50
113
  );
51
114
  UnistylesRuntime.statusBar.setStyle((contentColor === 'dark' ? 'light' : 'dark') as any, animated);
52
115
  },
116
+
117
+ /**
118
+ * Get the name of the currently active theme.
119
+ *
120
+ * @returns The current theme name (e.g. 'light', 'dark')
121
+ *
122
+ * @example
123
+ * ```typescript
124
+ * const current = ThemeSettings.getThemeName(); // 'dark'
125
+ * ```
126
+ */
127
+ getThemeName(): string {
128
+ return String(UnistylesRuntime.themeName);
129
+ },
130
+
131
+ /**
132
+ * Enable or disable adaptive (system-following) themes.
133
+ *
134
+ * When enabled, the theme automatically switches to match the device's
135
+ * light/dark mode setting.
136
+ *
137
+ * @param enabled - Whether to follow the system theme
138
+ *
139
+ * @example
140
+ * ```typescript
141
+ * ThemeSettings.setAdaptiveThemes(true); // follow system
142
+ * ThemeSettings.setAdaptiveThemes(false); // manual control
143
+ * ```
144
+ */
145
+ setAdaptiveThemes(enabled: boolean): void {
146
+ UnistylesRuntime.setAdaptiveThemes(enabled);
147
+ },
53
148
  };
@@ -22,10 +22,12 @@ import type { TextStyle, ViewStyle } from 'react-native';
22
22
  /**
23
23
  * Registry interface that components augment to register their style types.
24
24
  * This enables type-safe extendStyle and overrideStyle calls.
25
+ *
26
+ * Style definitions must use plain style objects, not functions.
25
27
  */
26
28
  export interface ComponentStyleRegistry {
27
29
  // Components augment this interface to add their style types
28
- // Example: Text: { text: (params: TextStyleParams) => TextStyleObject }
30
+ // Example: Text: { text: TextStyle & { variants?: { ... } } }
29
31
  }
30
32
 
31
33
  /**
@@ -37,38 +39,25 @@ export type ComponentStyleDef<K extends string> = K extends keyof ComponentStyle
37
39
  : Record<string, any>;
38
40
 
39
41
  /**
40
- * Deep partial type that works with functions.
41
- * For style functions, preserves the function signature but makes the return type partial.
42
+ * Deep partial type for style objects.
43
+ * Recursively makes all properties optional.
44
+ * Extensions must be plain style objects — functions are not supported.
42
45
  */
43
- export type DeepPartialStyle<T> = T extends (...args: infer A) => infer R
44
- ? (...args: A) => DeepPartialStyle<R>
45
- : T extends object
46
- ? { [K in keyof T]?: DeepPartialStyle<T[K]> }
47
- : T;
46
+ export type DeepPartialStyle<T> = T extends object
47
+ ? { [K in keyof T]?: DeepPartialStyle<T[K]> }
48
+ : T;
48
49
 
49
50
  /**
50
- * Style definition for extendStyle - requires functions with same params as base.
51
- * All style properties must be functions to access dynamic params.
51
+ * Style definition for extendStyle - plain style objects only.
52
+ * Functions are not supported; the babel plugin merges plain objects into base styles.
52
53
  */
53
54
  export type ExtendStyleDef<K extends string> = DeepPartialStyle<ComponentStyleDef<K>>;
54
55
 
55
56
  /**
56
- * Style definition for overrideStyle - requires full implementation with functions.
57
+ * Style definition for overrideStyle - requires full style implementation.
57
58
  */
58
59
  export type OverrideStyleDef<K extends string> = ComponentStyleDef<K>;
59
60
 
60
- /**
61
- * Helper to extract the params type from a dynamic style function.
62
- * Use this to type your extension functions.
63
- *
64
- * @example
65
- * ```typescript
66
- * type TextParams = StyleParams<TextStyleDef['text']>;
67
- * // TextParams = { color?: TextColorVariant }
68
- * ```
69
- */
70
- export type StyleParams<T> = T extends (params: infer P) => any ? P : never;
71
-
72
61
  // =============================================================================
73
62
  // Common Style Types
74
63
  // =============================================================================
@@ -86,8 +75,3 @@ export interface StyleWithVariants<TVariants extends Record<string, any> = Recor
86
75
  [K in keyof TVariants]?: TVariants[K];
87
76
  } & { styles: ViewStyle | TextStyle }>;
88
77
  }
89
-
90
- /**
91
- * Dynamic style function type.
92
- */
93
- export type DynamicStyleFn<TParams, TStyle> = (params: TParams) => TStyle;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Global component defaults.
3
+ *
4
+ * These values are used as fallbacks when neither the component prop
5
+ * nor a component-specific default is set.
6
+ * Call the setter once at app startup (e.g., in App.tsx).
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import { setDefaultMaxFontSizeMultiplier } from '@idealyst/theme';
11
+ *
12
+ * setDefaultMaxFontSizeMultiplier(1.5);
13
+ * ```
14
+ */
15
+
16
+ let _defaultMaxFontSizeMultiplier: number | undefined = undefined;
17
+
18
+ /**
19
+ * Set the global default `maxFontSizeMultiplier` for all text-rendering components.
20
+ * Any component without an explicit prop or component-level default will use this value.
21
+ * Pass `undefined` to clear (no limit).
22
+ */
23
+ export function setDefaultMaxFontSizeMultiplier(value: number | undefined): void {
24
+ _defaultMaxFontSizeMultiplier = value;
25
+ }
26
+
27
+ /**
28
+ * Get the current global default `maxFontSizeMultiplier`.
29
+ * Returns `undefined` if no default has been set.
30
+ */
31
+ export function getDefaultMaxFontSizeMultiplier(): number | undefined {
32
+ return _defaultMaxFontSizeMultiplier;
33
+ }
package/src/index.ts CHANGED
@@ -41,6 +41,9 @@ export { useStyleProps, type StyleProps } from './useStyleProps';
41
41
  // Shadow utility (platform-specific via .native.ts)
42
42
  export { shadow, type ShadowOptions, type ShadowStyle } from './shadow';
43
43
 
44
+ // Component defaults
45
+ export { setDefaultMaxFontSizeMultiplier, getDefaultMaxFontSizeMultiplier } from './defaults';
46
+
44
47
  // Animation tokens and utilities
45
48
  // Note: Use '@idealyst/theme/animation' for full animation API
46
49
  export { durations, easings, presets } from './animation/tokens';
@@ -15,7 +15,6 @@ 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>;
19
18
  };
20
19
  sizes: {
21
20
  button: Record<string, ButtonSizeValue>;