@idealyst/components 1.1.6 → 1.1.7
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 +8 -3
- package/src/Accordion/Accordion.native.tsx +15 -9
- package/src/Accordion/Accordion.styles.tsx +193 -168
- package/src/Accordion/Accordion.web.tsx +12 -7
- package/src/ActivityIndicator/ActivityIndicator.native.tsx +3 -2
- package/src/ActivityIndicator/ActivityIndicator.styles.tsx +22 -11
- package/src/ActivityIndicator/ActivityIndicator.web.tsx +2 -2
- package/src/Alert/Alert.native.tsx +11 -10
- package/src/Alert/Alert.styles.tsx +162 -253
- package/src/Alert/Alert.web.tsx +6 -10
- package/src/Avatar/Avatar.native.tsx +5 -2
- package/src/Avatar/Avatar.styles.tsx +48 -18
- package/src/Avatar/Avatar.web.tsx +2 -2
- package/src/Badge/Badge.native.tsx +2 -2
- package/src/Badge/Badge.styles.tsx +37 -16
- package/src/Badge/Badge.web.tsx +6 -6
- package/src/Breadcrumb/Breadcrumb.native.tsx +12 -5
- package/src/Breadcrumb/Breadcrumb.styles.tsx +59 -58
- package/src/Breadcrumb/Breadcrumb.web.tsx +13 -6
- package/src/Button/Button.native.tsx +39 -14
- package/src/Button/Button.styles.tsx +106 -208
- package/src/Button/Button.web.tsx +10 -8
- package/src/Card/Card.native.tsx +14 -6
- package/src/Card/Card.styles.tsx +64 -62
- package/src/Card/Card.web.tsx +5 -4
- package/src/Checkbox/Checkbox.native.tsx +7 -3
- package/src/Checkbox/Checkbox.styles.tsx +49 -25
- package/src/Checkbox/Checkbox.web.tsx +3 -3
- package/src/Chip/Chip.native.tsx +5 -5
- package/src/Chip/Chip.styles.tsx +71 -21
- package/src/Chip/Chip.web.tsx +5 -5
- package/src/Dialog/Dialog.native.tsx +10 -4
- package/src/Dialog/Dialog.styles.tsx +130 -90
- package/src/Dialog/Dialog.web.tsx +4 -4
- package/src/Divider/Divider.native.tsx +29 -42
- package/src/Divider/Divider.styles.tsx +138 -242
- package/src/Divider/Divider.web.tsx +17 -14
- package/src/Icon/Icon.native.tsx +11 -3
- package/src/Icon/Icon.styles.tsx +10 -4
- package/src/Image/Image.styles.tsx +53 -37
- package/src/Input/Input.native.tsx +6 -7
- package/src/Input/Input.styles.tsx +194 -174
- package/src/Input/Input.web.tsx +5 -8
- package/src/Link/Link.native.tsx +4 -1
- package/src/List/List.styles.tsx +79 -105
- package/src/List/ListItem.native.tsx +5 -3
- package/src/List/ListItem.web.tsx +4 -3
- package/src/Menu/Menu.native.tsx +1 -1
- package/src/Menu/Menu.styles.tsx +53 -37
- package/src/Menu/Menu.web.tsx +2 -2
- package/src/Menu/MenuItem.native.tsx +5 -3
- package/src/Menu/MenuItem.styles.tsx +68 -69
- package/src/Menu/MenuItem.web.tsx +16 -3
- package/src/Popover/Popover.native.tsx +1 -1
- package/src/Popover/Popover.styles.tsx +40 -29
- package/src/Popover/Popover.web.tsx +1 -1
- package/src/Pressable/Pressable.native.tsx +3 -1
- package/src/Pressable/Pressable.styles.tsx +20 -13
- package/src/Pressable/Pressable.web.tsx +1 -1
- package/src/Progress/Progress.native.tsx +15 -6
- package/src/Progress/Progress.styles.tsx +125 -85
- package/src/Progress/Progress.web.tsx +10 -9
- package/src/RadioButton/RadioButton.native.tsx +8 -3
- package/src/RadioButton/RadioButton.styles.tsx +44 -37
- package/src/RadioButton/RadioButton.web.tsx +3 -3
- package/src/SVGImage/SVGImage.styles.tsx +28 -16
- package/src/Screen/Screen.native.tsx +23 -13
- package/src/Screen/Screen.styles.tsx +57 -46
- package/src/Screen/Screen.web.tsx +1 -1
- package/src/Select/Select.native.tsx +11 -5
- package/src/Select/Select.styles.tsx +72 -52
- package/src/Select/Select.web.tsx +5 -5
- package/src/Skeleton/Skeleton.styles.tsx +26 -14
- package/src/Slider/Slider.native.tsx +9 -5
- package/src/Slider/Slider.styles.tsx +59 -48
- package/src/Slider/Slider.web.tsx +5 -5
- package/src/Switch/Switch.native.tsx +6 -2
- package/src/Switch/Switch.styles.tsx +46 -19
- package/src/Switch/Switch.web.tsx +4 -4
- package/src/TabBar/TabBar.native.tsx +23 -31
- package/src/TabBar/TabBar.styles.tsx +215 -371
- package/src/TabBar/TabBar.web.tsx +21 -33
- package/src/Table/Table.native.tsx +1 -1
- package/src/Table/Table.styles.tsx +11 -4
- package/src/Table/Table.web.tsx +1 -1
- package/src/Text/Text.native.tsx +3 -4
- package/src/Text/Text.styles.tsx +7 -1
- package/src/Text/Text.web.tsx +1 -1
- package/src/TextArea/TextArea.styles.tsx +90 -58
- package/src/Tooltip/Tooltip.native.tsx +2 -2
- package/src/Tooltip/Tooltip.styles.tsx +21 -12
- package/src/Tooltip/Tooltip.web.tsx +2 -2
- package/src/Video/Video.styles.tsx +39 -23
- package/src/View/View.native.tsx +4 -2
- package/src/View/View.styles.tsx +33 -22
- package/src/View/View.web.tsx +13 -2
- package/src/extensions/applyExtension.ts +210 -0
- package/src/extensions/extendComponent.ts +377 -0
- package/src/extensions/index.ts +102 -0
- package/src/extensions/types.ts +497 -0
- package/src/globals.ts +16 -0
- package/src/index.native.ts +4 -0
- package/src/index.ts +28 -0
- package/src/utils/deepMerge.ts +54 -2
package/src/View/View.web.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import React, { forwardRef } from 'react';
|
|
1
|
+
import React, { forwardRef, useMemo } from 'react';
|
|
2
|
+
import { StyleSheet } from 'react-native';
|
|
2
3
|
import { getWebProps } from 'react-native-unistyles/web';
|
|
3
4
|
import { ViewProps } from './types';
|
|
4
5
|
import { viewStyles } from './View.styles';
|
|
@@ -48,13 +49,23 @@ const View = forwardRef<HTMLDivElement, ViewProps>(({
|
|
|
48
49
|
if (borderWidth !== undefined) dynamicStyles.borderWidth = borderWidth;
|
|
49
50
|
if (borderColor) dynamicStyles.borderColor = borderColor;
|
|
50
51
|
|
|
52
|
+
// Flatten style array to object (HTML divs don't support style arrays)
|
|
53
|
+
const flattenedStyle = useMemo(() => {
|
|
54
|
+
if (!style) return undefined;
|
|
55
|
+
if (Array.isArray(style)) {
|
|
56
|
+
return StyleSheet.flatten(style);
|
|
57
|
+
}
|
|
58
|
+
return style;
|
|
59
|
+
}, [style]);
|
|
60
|
+
|
|
51
61
|
/** @ts-ignore */
|
|
52
|
-
const webProps = getWebProps([viewStyles.view
|
|
62
|
+
const webProps = getWebProps([(viewStyles.view as any)({}), dynamicStyles]);
|
|
53
63
|
|
|
54
64
|
const mergedRef = useMergeRefs(ref, webProps.ref);
|
|
55
65
|
|
|
56
66
|
return (
|
|
57
67
|
<div
|
|
68
|
+
style={flattenedStyle as any}
|
|
58
69
|
{...webProps}
|
|
59
70
|
ref={mergedRef}
|
|
60
71
|
id={id}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { deepMerge } from '../utils/deepMerge';
|
|
2
|
+
import { Styles, ElementStyle, ComponentName } from './types';
|
|
3
|
+
import { getExtension, getReplacement } from './extendComponent';
|
|
4
|
+
import { Theme } from '@idealyst/theme';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Wrap a dynamic style function to merge with extension styles.
|
|
8
|
+
*
|
|
9
|
+
* All styles in Unistyles must be dynamic functions (not static objects)
|
|
10
|
+
* to avoid Babel transform issues. This utility wraps a style function
|
|
11
|
+
* to automatically merge extension styles when the function is called.
|
|
12
|
+
*
|
|
13
|
+
* @param styleFn - The original dynamic style function
|
|
14
|
+
* @param elementExtension - Extension styles for this element (can be undefined).
|
|
15
|
+
* Can be either a static styles object or a function (props) => styles
|
|
16
|
+
* for prop-aware extensions.
|
|
17
|
+
* @returns A new function that returns merged styles
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* import { withExtension } from '../extensions/applyExtension';
|
|
22
|
+
* import { getExtension } from '../extensions/extendComponent';
|
|
23
|
+
*
|
|
24
|
+
* export const buttonStyles = StyleSheet.create((theme: Theme) => {
|
|
25
|
+
* const ext = getExtension('Button', theme);
|
|
26
|
+
*
|
|
27
|
+
* return {
|
|
28
|
+
* button: withExtension(createButtonStyles(theme), ext?.button),
|
|
29
|
+
* text: withExtension(createTextStyles(theme), ext?.text),
|
|
30
|
+
* };
|
|
31
|
+
* });
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @remarks
|
|
35
|
+
* - If no extension is provided, returns the original function unchanged
|
|
36
|
+
* - Extension styles take priority over base styles (deep merged)
|
|
37
|
+
* - Works with any style function signature
|
|
38
|
+
* - If extension is a function, it receives the same props as the base style function
|
|
39
|
+
*/
|
|
40
|
+
export function withExtension<TProps, TResult extends Styles>(
|
|
41
|
+
styleFn: (props: TProps) => TResult,
|
|
42
|
+
elementExtension: Styles | ((props: TProps) => Styles) | undefined
|
|
43
|
+
): (props: TProps) => TResult {
|
|
44
|
+
// If no extension, return original function unchanged
|
|
45
|
+
if (!elementExtension) {
|
|
46
|
+
return styleFn;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Return wrapped function that merges extension
|
|
50
|
+
return (props: TProps): TResult => {
|
|
51
|
+
const baseStyles = styleFn(props);
|
|
52
|
+
// If extension is a function, call it with props; otherwise use as-is
|
|
53
|
+
const extStyles = typeof elementExtension === 'function'
|
|
54
|
+
? elementExtension(props)
|
|
55
|
+
: elementExtension;
|
|
56
|
+
return deepMerge(baseStyles, extStyles) as TResult;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Wrap a parameterless style function with extension.
|
|
62
|
+
*
|
|
63
|
+
* Use this for style functions that don't take any parameters.
|
|
64
|
+
* This is common for simpler elements like iconContainer.
|
|
65
|
+
*
|
|
66
|
+
* @param styleFn - The original style function (no parameters)
|
|
67
|
+
* @param elementExtension - Extension styles for this element
|
|
68
|
+
* @returns A new function that returns merged styles
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* const createIconContainerStyles = (theme: Theme) => {
|
|
73
|
+
* return () => ({
|
|
74
|
+
* display: 'flex',
|
|
75
|
+
* flexDirection: 'row',
|
|
76
|
+
* gap: 4,
|
|
77
|
+
* });
|
|
78
|
+
* };
|
|
79
|
+
*
|
|
80
|
+
* // In StyleSheet.create:
|
|
81
|
+
* iconContainer: withSimpleExtension(
|
|
82
|
+
* createIconContainerStyles(theme),
|
|
83
|
+
* ext?.iconContainer
|
|
84
|
+
* ),
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export function withSimpleExtension<TResult extends Styles>(
|
|
88
|
+
styleFn: () => TResult,
|
|
89
|
+
elementExtension: Styles | undefined
|
|
90
|
+
): () => TResult {
|
|
91
|
+
if (!elementExtension) {
|
|
92
|
+
return styleFn;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (): TResult => {
|
|
96
|
+
const baseStyles = styleFn();
|
|
97
|
+
return deepMerge(baseStyles, elementExtension) as TResult;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Normalize a style value (from replacement) into a dynamic function.
|
|
103
|
+
*
|
|
104
|
+
* Replacements can be either:
|
|
105
|
+
* - A function (props) => styles - used directly
|
|
106
|
+
* - A static styles object - wrapped in a function
|
|
107
|
+
*
|
|
108
|
+
* @param value - The replacement value (function or static object)
|
|
109
|
+
* @param defaultFn - Default function to use if value is undefined
|
|
110
|
+
* @returns The default function (type-safe) - replacement handling is done at runtime
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```typescript
|
|
114
|
+
* const replacement = getReplacement('Button', theme);
|
|
115
|
+
*
|
|
116
|
+
* // If replacement.button is a function, use it directly
|
|
117
|
+
* // If replacement.button is an object, wrap it in () => replacement.button
|
|
118
|
+
* // If undefined, use createButtonStyles(theme)
|
|
119
|
+
* button: withExtension(
|
|
120
|
+
* normalizeStyleFn(replacement?.button, createButtonStyles(theme)),
|
|
121
|
+
* ext?.button
|
|
122
|
+
* ),
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
export function normalizeStyleFn<TProps, TResult>(
|
|
126
|
+
value: unknown,
|
|
127
|
+
defaultFn: (props: TProps) => TResult
|
|
128
|
+
): (props: TProps) => TResult {
|
|
129
|
+
if (value === undefined || value === null) {
|
|
130
|
+
return defaultFn;
|
|
131
|
+
}
|
|
132
|
+
if (typeof value === 'function') {
|
|
133
|
+
return value as (props: TProps) => TResult;
|
|
134
|
+
}
|
|
135
|
+
// Static object - wrap in a function that ignores props
|
|
136
|
+
return (() => value) as (props: TProps) => TResult;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Normalize a simple style value (no props) into a parameterless function.
|
|
141
|
+
*
|
|
142
|
+
* @param value - The replacement value (function or static object)
|
|
143
|
+
* @param defaultFn - Default function to use if value is undefined
|
|
144
|
+
* @returns A parameterless function that returns styles
|
|
145
|
+
*/
|
|
146
|
+
export function normalizeSimpleStyleFn<TResult>(
|
|
147
|
+
value: unknown,
|
|
148
|
+
defaultFn: () => TResult
|
|
149
|
+
): () => TResult {
|
|
150
|
+
if (value === undefined || value === null) {
|
|
151
|
+
return defaultFn;
|
|
152
|
+
}
|
|
153
|
+
if (typeof value === 'function') {
|
|
154
|
+
return value as () => TResult;
|
|
155
|
+
}
|
|
156
|
+
// Static object - wrap in a function
|
|
157
|
+
return (() => value) as () => TResult;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Apply extensions and replacements to a set of style creators.
|
|
162
|
+
*
|
|
163
|
+
* This is a simplified helper that handles the common pattern of:
|
|
164
|
+
* 1. Getting extensions and replacements for a component
|
|
165
|
+
* 2. Applying normalizeStyleFn for each element
|
|
166
|
+
* 3. Merging extensions on top
|
|
167
|
+
*
|
|
168
|
+
* @param component - The component name
|
|
169
|
+
* @param theme - The current theme
|
|
170
|
+
* @param styleCreators - Object mapping element names to their style creator functions
|
|
171
|
+
* @returns Object with the same keys, but with extensions/replacements applied
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* ```typescript
|
|
175
|
+
* export const buttonStyles = StyleSheet.create((theme: Theme) => {
|
|
176
|
+
* return applyExtensions('Button', theme, {
|
|
177
|
+
* button: createButtonStyles(theme),
|
|
178
|
+
* text: createTextStyles(theme),
|
|
179
|
+
* icon: createIconStyles(theme),
|
|
180
|
+
* });
|
|
181
|
+
* });
|
|
182
|
+
* ```
|
|
183
|
+
*/
|
|
184
|
+
export function applyExtensions<
|
|
185
|
+
K extends ComponentName,
|
|
186
|
+
T extends Record<string, ((...args: any[]) => any)>
|
|
187
|
+
>(
|
|
188
|
+
component: K,
|
|
189
|
+
theme: Theme,
|
|
190
|
+
styleCreators: T
|
|
191
|
+
): T {
|
|
192
|
+
const ext = getExtension(component, theme);
|
|
193
|
+
const replacement = getReplacement(component, theme);
|
|
194
|
+
|
|
195
|
+
const result = {} as T;
|
|
196
|
+
|
|
197
|
+
for (const key in styleCreators) {
|
|
198
|
+
const creator = styleCreators[key];
|
|
199
|
+
const elementExt = ext?.[key as string as keyof typeof ext] as ElementStyle | undefined;
|
|
200
|
+
const elementReplacement = replacement?.[key as string as keyof typeof replacement];
|
|
201
|
+
|
|
202
|
+
// Apply replacement (if any) then extension (if any)
|
|
203
|
+
result[key] = withExtension(
|
|
204
|
+
normalizeStyleFn(elementReplacement, creator),
|
|
205
|
+
elementExt
|
|
206
|
+
) as T[typeof key];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { Theme } from '@idealyst/theme';
|
|
2
|
+
import {
|
|
3
|
+
ComponentStyleElements,
|
|
4
|
+
ComponentName,
|
|
5
|
+
StyleExtension,
|
|
6
|
+
} from './types';
|
|
7
|
+
import { deepMergeAll } from '../utils/deepMerge';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Registry storing style extensions for each component.
|
|
11
|
+
* Key is the component name, value is an array of extensions (applied in order).
|
|
12
|
+
*/
|
|
13
|
+
const extensionRegistry = new Map<ComponentName, StyleExtension<any>[]>();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Registry storing complete style replacements for each component.
|
|
17
|
+
* When set, the replacement is used instead of base styles + extensions.
|
|
18
|
+
*/
|
|
19
|
+
const replacementRegistry = new Map<ComponentName, StyleExtension<any>>();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Completely replace the styles of a component.
|
|
23
|
+
*
|
|
24
|
+
* Unlike `extendComponent` which merges with base styles, `replaceStyles`
|
|
25
|
+
* completely overrides the default stylesheet. Extensions are NOT applied
|
|
26
|
+
* when a replacement is set.
|
|
27
|
+
*
|
|
28
|
+
* **Use with caution:** You're responsible for providing all necessary styles,
|
|
29
|
+
* including variant styles, platform-specific styles, and accessibility states.
|
|
30
|
+
*
|
|
31
|
+
* @param component - The component name to replace styles for
|
|
32
|
+
* @param replacement - Complete style replacement, either as an object or a function receiving theme
|
|
33
|
+
*
|
|
34
|
+
* @example Complete replacement with theme access
|
|
35
|
+
* ```typescript
|
|
36
|
+
* import { replaceStyles } from '@idealyst/components';
|
|
37
|
+
*
|
|
38
|
+
* replaceStyles('Button', (theme) => ({
|
|
39
|
+
* button: {
|
|
40
|
+
* backgroundColor: theme.colors.surface.primary,
|
|
41
|
+
* borderRadius: 0,
|
|
42
|
+
* padding: 16,
|
|
43
|
+
* variants: {
|
|
44
|
+
* size: {
|
|
45
|
+
* sm: { padding: 8 },
|
|
46
|
+
* md: { padding: 16 },
|
|
47
|
+
* lg: { padding: 24 },
|
|
48
|
+
* },
|
|
49
|
+
* },
|
|
50
|
+
* },
|
|
51
|
+
* text: {
|
|
52
|
+
* color: theme.colors.text.primary,
|
|
53
|
+
* fontSize: 16,
|
|
54
|
+
* },
|
|
55
|
+
* icon: {
|
|
56
|
+
* width: 20,
|
|
57
|
+
* height: 20,
|
|
58
|
+
* },
|
|
59
|
+
* iconContainer: {
|
|
60
|
+
* display: 'flex',
|
|
61
|
+
* alignItems: 'center',
|
|
62
|
+
* },
|
|
63
|
+
* }));
|
|
64
|
+
* ```
|
|
65
|
+
*
|
|
66
|
+
* @example Clear replacement to restore defaults
|
|
67
|
+
* ```typescript
|
|
68
|
+
* clearReplacement('Button');
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export function replaceStyles<K extends ComponentName>(
|
|
72
|
+
component: K,
|
|
73
|
+
replacement: StyleExtension<ComponentStyleElements[K]>
|
|
74
|
+
): void {
|
|
75
|
+
replacementRegistry.set(component, replacement);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get the replacement styles for a component, if set.
|
|
80
|
+
*
|
|
81
|
+
* @param component - The component name
|
|
82
|
+
* @param theme - The current theme (used if replacement is a function)
|
|
83
|
+
* @returns The resolved replacement styles, or undefined if none set
|
|
84
|
+
*
|
|
85
|
+
* @internal
|
|
86
|
+
*/
|
|
87
|
+
export function getReplacement<K extends ComponentName>(
|
|
88
|
+
component: K,
|
|
89
|
+
theme: Theme
|
|
90
|
+
): Partial<ComponentStyleElements[K]> | undefined {
|
|
91
|
+
const replacement = replacementRegistry.get(component);
|
|
92
|
+
if (!replacement) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
return typeof replacement === 'function' ? replacement(theme) : replacement;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Clear the style replacement for a specific component.
|
|
100
|
+
* After clearing, the component will use base styles + extensions again.
|
|
101
|
+
*
|
|
102
|
+
* @param component - The component name to clear replacement for
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```typescript
|
|
106
|
+
* import { clearReplacement } from '@idealyst/components';
|
|
107
|
+
*
|
|
108
|
+
* clearReplacement('Button');
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
export function clearReplacement<K extends ComponentName>(component: K): void {
|
|
112
|
+
replacementRegistry.delete(component);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Clear all style replacements.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```typescript
|
|
120
|
+
* import { clearAllReplacements } from '@idealyst/components';
|
|
121
|
+
*
|
|
122
|
+
* clearAllReplacements();
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
export function clearAllReplacements(): void {
|
|
126
|
+
replacementRegistry.clear();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Check if a component has a style replacement set.
|
|
131
|
+
*
|
|
132
|
+
* @param component - The component name to check
|
|
133
|
+
* @returns true if the component has a replacement set
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```typescript
|
|
137
|
+
* import { hasReplacement } from '@idealyst/components';
|
|
138
|
+
*
|
|
139
|
+
* if (hasReplacement('Button')) {
|
|
140
|
+
* console.log('Button styles are completely replaced');
|
|
141
|
+
* }
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
export function hasReplacement<K extends ComponentName>(component: K): boolean {
|
|
145
|
+
return replacementRegistry.has(component);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Globally extend the styles of a component.
|
|
150
|
+
*
|
|
151
|
+
* Extensions affect ALL instances of that component app-wide.
|
|
152
|
+
* Extensions are merged with base styles - extension styles win on conflict.
|
|
153
|
+
*
|
|
154
|
+
* **Precedence rules:**
|
|
155
|
+
* - Extensions override base component styles
|
|
156
|
+
* - Later extensions override earlier extensions
|
|
157
|
+
* - Setting a value to `undefined` removes that style property
|
|
158
|
+
* - Nested objects (like `_web`, `variants`) are deep merged
|
|
159
|
+
*
|
|
160
|
+
* @param component - The component name to extend (e.g., 'Button', 'Card')
|
|
161
|
+
* @param extension - Style overrides, either as an object or a function receiving theme
|
|
162
|
+
*
|
|
163
|
+
* @example Static extension
|
|
164
|
+
* ```typescript
|
|
165
|
+
* import { extendComponent } from '@idealyst/components';
|
|
166
|
+
*
|
|
167
|
+
* extendComponent('Button', {
|
|
168
|
+
* button: {
|
|
169
|
+
* borderRadius: 20,
|
|
170
|
+
* shadowColor: '#000',
|
|
171
|
+
* shadowOffset: { width: 0, height: 2 },
|
|
172
|
+
* shadowOpacity: 0.25,
|
|
173
|
+
* shadowRadius: 4,
|
|
174
|
+
* },
|
|
175
|
+
* text: {
|
|
176
|
+
* textTransform: 'uppercase',
|
|
177
|
+
* letterSpacing: 1,
|
|
178
|
+
* },
|
|
179
|
+
* });
|
|
180
|
+
* ```
|
|
181
|
+
*
|
|
182
|
+
* @example Theme-aware extension
|
|
183
|
+
* ```typescript
|
|
184
|
+
* extendComponent('Button', (theme) => ({
|
|
185
|
+
* button: {
|
|
186
|
+
* ...theme.shadows.lg,
|
|
187
|
+
* backgroundColor: theme.colors.surface.secondary,
|
|
188
|
+
* },
|
|
189
|
+
* text: {
|
|
190
|
+
* color: theme.colors.text.primary,
|
|
191
|
+
* },
|
|
192
|
+
* }));
|
|
193
|
+
* ```
|
|
194
|
+
*
|
|
195
|
+
* @example Multiple extensions (later ones have higher precedence)
|
|
196
|
+
* ```typescript
|
|
197
|
+
* // Base brand styles
|
|
198
|
+
* extendComponent('Button', {
|
|
199
|
+
* button: { borderRadius: 8 },
|
|
200
|
+
* });
|
|
201
|
+
*
|
|
202
|
+
* // Feature-specific override (wins over base)
|
|
203
|
+
* extendComponent('Button', {
|
|
204
|
+
* button: { borderRadius: 20 },
|
|
205
|
+
* });
|
|
206
|
+
* // Result: borderRadius is 20
|
|
207
|
+
* ```
|
|
208
|
+
*
|
|
209
|
+
* @example Removing a style property
|
|
210
|
+
* ```typescript
|
|
211
|
+
* extendComponent('Button', {
|
|
212
|
+
* button: {
|
|
213
|
+
* shadowColor: undefined, // Removes shadowColor
|
|
214
|
+
* shadowOpacity: undefined, // Removes shadowOpacity
|
|
215
|
+
* },
|
|
216
|
+
* });
|
|
217
|
+
* ```
|
|
218
|
+
*
|
|
219
|
+
* @example Web-specific styles
|
|
220
|
+
* ```typescript
|
|
221
|
+
* extendComponent('Button', {
|
|
222
|
+
* button: {
|
|
223
|
+
* _web: {
|
|
224
|
+
* cursor: 'pointer',
|
|
225
|
+
* transition: 'all 0.2s ease',
|
|
226
|
+
* _hover: {
|
|
227
|
+
* transform: 'scale(1.02)',
|
|
228
|
+
* },
|
|
229
|
+
* },
|
|
230
|
+
* },
|
|
231
|
+
* });
|
|
232
|
+
* ```
|
|
233
|
+
*/
|
|
234
|
+
// Overload for static object extension
|
|
235
|
+
export function extendComponent<K extends ComponentName>(
|
|
236
|
+
component: K,
|
|
237
|
+
extension: Partial<ComponentStyleElements[K]>
|
|
238
|
+
): void;
|
|
239
|
+
// Overload for theme-aware function extension
|
|
240
|
+
export function extendComponent<K extends ComponentName>(
|
|
241
|
+
component: K,
|
|
242
|
+
extension: (theme: Theme) => Partial<ComponentStyleElements[K]>
|
|
243
|
+
): void;
|
|
244
|
+
// Implementation
|
|
245
|
+
export function extendComponent<K extends ComponentName>(
|
|
246
|
+
component: K,
|
|
247
|
+
extension: StyleExtension<ComponentStyleElements[K]>
|
|
248
|
+
): void {
|
|
249
|
+
const existing = extensionRegistry.get(component) ?? [];
|
|
250
|
+
existing.push(extension);
|
|
251
|
+
extensionRegistry.set(component, existing);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Get the resolved extension for a component.
|
|
256
|
+
*
|
|
257
|
+
* This is an internal function used by component stylesheets to retrieve
|
|
258
|
+
* and apply extensions. All registered extensions are merged in order,
|
|
259
|
+
* with later extensions taking precedence.
|
|
260
|
+
*
|
|
261
|
+
* @param component - The component name
|
|
262
|
+
* @param theme - The current theme (used if extension is a function)
|
|
263
|
+
* @returns The resolved merged extension styles, or undefined if none registered
|
|
264
|
+
*
|
|
265
|
+
* @internal
|
|
266
|
+
*/
|
|
267
|
+
export function getExtension<K extends ComponentName>(
|
|
268
|
+
component: K,
|
|
269
|
+
theme: Theme
|
|
270
|
+
): Partial<ComponentStyleElements[K]> | undefined {
|
|
271
|
+
const extensions = extensionRegistry.get(component);
|
|
272
|
+
|
|
273
|
+
if (!extensions || extensions.length === 0) {
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Resolve all extensions (call functions with theme)
|
|
278
|
+
const resolved = extensions.map(ext =>
|
|
279
|
+
typeof ext === 'function' ? ext(theme) : ext
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
// Merge all extensions in order (later ones win)
|
|
283
|
+
return deepMergeAll(...resolved) as Partial<ComponentStyleElements[K]>;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Clear all extensions for a specific component.
|
|
288
|
+
*
|
|
289
|
+
* @param component - The component name to clear extensions for
|
|
290
|
+
*
|
|
291
|
+
* @example
|
|
292
|
+
* ```typescript
|
|
293
|
+
* import { clearExtension } from '@idealyst/components';
|
|
294
|
+
*
|
|
295
|
+
* // Remove all Button extensions
|
|
296
|
+
* clearExtension('Button');
|
|
297
|
+
* ```
|
|
298
|
+
*/
|
|
299
|
+
export function clearExtension<K extends ComponentName>(component: K): void {
|
|
300
|
+
extensionRegistry.delete(component);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Clear all component extensions.
|
|
305
|
+
*
|
|
306
|
+
* @example
|
|
307
|
+
* ```typescript
|
|
308
|
+
* import { clearAllExtensions } from '@idealyst/components';
|
|
309
|
+
*
|
|
310
|
+
* // Remove all component extensions
|
|
311
|
+
* clearAllExtensions();
|
|
312
|
+
* ```
|
|
313
|
+
*/
|
|
314
|
+
export function clearAllExtensions(): void {
|
|
315
|
+
extensionRegistry.clear();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Check if a component has any extensions registered.
|
|
320
|
+
*
|
|
321
|
+
* @param component - The component name to check
|
|
322
|
+
* @returns true if the component has at least one extension
|
|
323
|
+
*
|
|
324
|
+
* @example
|
|
325
|
+
* ```typescript
|
|
326
|
+
* import { hasExtension } from '@idealyst/components';
|
|
327
|
+
*
|
|
328
|
+
* if (hasExtension('Button')) {
|
|
329
|
+
* console.log('Button has custom styles');
|
|
330
|
+
* }
|
|
331
|
+
* ```
|
|
332
|
+
*/
|
|
333
|
+
export function hasExtension<K extends ComponentName>(component: K): boolean {
|
|
334
|
+
const extensions = extensionRegistry.get(component);
|
|
335
|
+
return extensions !== undefined && extensions.length > 0;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Get all registered component extensions.
|
|
340
|
+
* Useful for debugging or inspecting what extensions are active.
|
|
341
|
+
*
|
|
342
|
+
* @returns Array of component names that have extensions
|
|
343
|
+
*
|
|
344
|
+
* @example
|
|
345
|
+
* ```typescript
|
|
346
|
+
* import { getExtendedComponents } from '@idealyst/components';
|
|
347
|
+
*
|
|
348
|
+
* const extended = getExtendedComponents();
|
|
349
|
+
* console.log('Extended components:', extended);
|
|
350
|
+
* // ['Button', 'Card', 'Input']
|
|
351
|
+
* ```
|
|
352
|
+
*/
|
|
353
|
+
export function getExtendedComponents(): ComponentName[] {
|
|
354
|
+
return Array.from(extensionRegistry.keys()).filter(key => {
|
|
355
|
+
const extensions = extensionRegistry.get(key);
|
|
356
|
+
return extensions && extensions.length > 0;
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Get the number of extensions registered for a component.
|
|
362
|
+
* Useful for debugging.
|
|
363
|
+
*
|
|
364
|
+
* @param component - The component name
|
|
365
|
+
* @returns Number of extensions registered
|
|
366
|
+
*
|
|
367
|
+
* @example
|
|
368
|
+
* ```typescript
|
|
369
|
+
* import { getExtensionCount } from '@idealyst/components';
|
|
370
|
+
*
|
|
371
|
+
* console.log('Button extensions:', getExtensionCount('Button'));
|
|
372
|
+
* // 3
|
|
373
|
+
* ```
|
|
374
|
+
*/
|
|
375
|
+
export function getExtensionCount<K extends ComponentName>(component: K): number {
|
|
376
|
+
return extensionRegistry.get(component)?.length ?? 0;
|
|
377
|
+
}
|