@idealyst/components 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +568 -0
- package/package.json +107 -0
- package/plugin/web.js +186 -0
- package/src/Avatar/Avatar.native.tsx +44 -0
- package/src/Avatar/Avatar.styles.tsx +67 -0
- package/src/Avatar/Avatar.web.tsx +51 -0
- package/src/Avatar/index.native.ts +2 -0
- package/src/Avatar/index.ts +2 -0
- package/src/Avatar/index.web.ts +2 -0
- package/src/Avatar/types.ts +43 -0
- package/src/Badge/Badge.native.tsx +43 -0
- package/src/Badge/Badge.styles.tsx +154 -0
- package/src/Badge/Badge.web.tsx +45 -0
- package/src/Badge/index.native.ts +2 -0
- package/src/Badge/index.ts +2 -0
- package/src/Badge/index.web.ts +2 -0
- package/src/Badge/types.ts +34 -0
- package/src/Button/Button.native.tsx +39 -0
- package/src/Button/Button.styles.tsx +215 -0
- package/src/Button/Button.types.ts +12 -0
- package/src/Button/Button.web.tsx +56 -0
- package/src/Button/index.native.ts +3 -0
- package/src/Button/index.ts +5 -0
- package/src/Button/index.web.ts +3 -0
- package/src/Button/types.ts +49 -0
- package/src/Card/Card.native.tsx +52 -0
- package/src/Card/Card.styles.tsx +240 -0
- package/src/Card/Card.web.tsx +62 -0
- package/src/Card/index.native.ts +3 -0
- package/src/Card/index.ts +5 -0
- package/src/Card/index.web.ts +3 -0
- package/src/Card/types.ts +59 -0
- package/src/Checkbox/Checkbox.native.tsx +99 -0
- package/src/Checkbox/Checkbox.styles.tsx +292 -0
- package/src/Checkbox/Checkbox.web.tsx +131 -0
- package/src/Checkbox/index.native.ts +3 -0
- package/src/Checkbox/index.ts +5 -0
- package/src/Checkbox/index.web.ts +3 -0
- package/src/Checkbox/types.ts +79 -0
- package/src/Divider/Divider.native.tsx +145 -0
- package/src/Divider/Divider.styles.tsx +602 -0
- package/src/Divider/Divider.web.tsx +73 -0
- package/src/Divider/index.native.ts +3 -0
- package/src/Divider/index.ts +5 -0
- package/src/Divider/index.web.ts +3 -0
- package/src/Divider/types.ts +54 -0
- package/src/Icon/Icon.native.tsx +39 -0
- package/src/Icon/Icon.styles.tsx +50 -0
- package/src/Icon/Icon.web.tsx +47 -0
- package/src/Icon/icon-types.ts +7452 -0
- package/src/Icon/index.native.ts +3 -0
- package/src/Icon/index.ts +5 -0
- package/src/Icon/index.web.ts +3 -0
- package/src/Icon/types.ts +36 -0
- package/src/Input/Input.native.tsx +75 -0
- package/src/Input/Input.styles.tsx +177 -0
- package/src/Input/Input.web.tsx +71 -0
- package/src/Input/index.native.ts +3 -0
- package/src/Input/index.ts +5 -0
- package/src/Input/index.web.ts +3 -0
- package/src/Input/types.ts +69 -0
- package/src/Screen/Screen.native.tsx +41 -0
- package/src/Screen/Screen.styles.tsx +60 -0
- package/src/Screen/Screen.web.tsx +33 -0
- package/src/Screen/index.native.ts +2 -0
- package/src/Screen/index.ts +2 -0
- package/src/Screen/index.web.ts +2 -0
- package/src/Screen/types.ts +38 -0
- package/src/Text/Text.native.tsx +36 -0
- package/src/Text/Text.styles.tsx +67 -0
- package/src/Text/Text.web.tsx +41 -0
- package/src/Text/index.native.ts +3 -0
- package/src/Text/index.ts +5 -0
- package/src/Text/index.web.ts +3 -0
- package/src/Text/types.ts +39 -0
- package/src/View/View.native.tsx +56 -0
- package/src/View/View.styles.tsx +103 -0
- package/src/View/View.web.tsx +60 -0
- package/src/View/index.native.ts +3 -0
- package/src/View/index.ts +5 -0
- package/src/View/index.web.ts +3 -0
- package/src/View/types.ts +73 -0
- package/src/examples/AllExamples.tsx +72 -0
- package/src/examples/AvatarExamples.tsx +97 -0
- package/src/examples/BadgeExamples.tsx +200 -0
- package/src/examples/ButtonExamples.tsx +150 -0
- package/src/examples/CardExamples.tsx +176 -0
- package/src/examples/CheckboxExamples.tsx +217 -0
- package/src/examples/DividerExamples.tsx +218 -0
- package/src/examples/IconExamples.tsx +342 -0
- package/src/examples/InputExamples.tsx +134 -0
- package/src/examples/README.md +136 -0
- package/src/examples/ScreenExamples.tsx +154 -0
- package/src/examples/TextExamples.tsx +89 -0
- package/src/examples/ThemeExtensionExamples.tsx +91 -0
- package/src/examples/ValidationExamples.tsx +95 -0
- package/src/examples/ViewExamples.tsx +129 -0
- package/src/examples/extendedTheme.ts +331 -0
- package/src/examples/index.ts +15 -0
- package/src/index.native.ts +52 -0
- package/src/index.ts +48 -0
- package/src/theme/breakpoints.ts +8 -0
- package/src/theme/colorResolver.ts +218 -0
- package/src/theme/colors.ts +315 -0
- package/src/theme/defaultThemes.ts +326 -0
- package/src/theme/index.ts +188 -0
- package/src/theme/themeBuilder.ts +602 -0
- package/src/theme/unistyles.d.ts +6 -0
- package/src/theme/variantHelpers.ts +584 -0
- package/src/theme/variants.ts +56 -0
- package/src/unistyles.d.ts +108 -0
- package/src/unistyles.ts +43 -0
package/plugin/web.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
module.exports = function ({ types: t }) {
|
|
2
|
+
console.log('[mdi-auto-import] Plugin loaded');
|
|
3
|
+
|
|
4
|
+
const importedIcons = new Set();
|
|
5
|
+
const iconImportIdentifiers = new Map();
|
|
6
|
+
let hasIconImport = false;
|
|
7
|
+
|
|
8
|
+
function formatIconName(name) {
|
|
9
|
+
// Handle empty or invalid names
|
|
10
|
+
if (!name || typeof name !== 'string') {
|
|
11
|
+
throw new Error(`Invalid icon name: ${name}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const formatted = name
|
|
15
|
+
// Convert kebab-case and snake_case to PascalCase
|
|
16
|
+
.replace(/[-_]/g, ' ')
|
|
17
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
18
|
+
.split(' ')
|
|
19
|
+
.filter(part => part.length > 0)
|
|
20
|
+
.map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
|
|
21
|
+
.join('');
|
|
22
|
+
|
|
23
|
+
console.log(`[mdi-auto-import] formatIconName: ${name} -> ${formatted}`);
|
|
24
|
+
return formatted;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getMdiIconName(name) {
|
|
28
|
+
const mdiName = `mdi${formatIconName(name)}`;
|
|
29
|
+
console.log(`[mdi-auto-import] getMdiIconName: ${name} -> ${mdiName}`);
|
|
30
|
+
return mdiName;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getIconIdentifier(iconName) {
|
|
34
|
+
if (!iconImportIdentifiers.has(iconName)) {
|
|
35
|
+
iconImportIdentifiers.set(iconName, `_${iconName}`);
|
|
36
|
+
}
|
|
37
|
+
const identifier = iconImportIdentifiers.get(iconName);
|
|
38
|
+
console.log(`[mdi-auto-import] getIconIdentifier: ${iconName} -> ${identifier}`);
|
|
39
|
+
return identifier;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
name: 'mdi-auto-import',
|
|
44
|
+
visitor: {
|
|
45
|
+
Program: {
|
|
46
|
+
enter(path) {
|
|
47
|
+
// Reset state for each file
|
|
48
|
+
importedIcons.clear();
|
|
49
|
+
iconImportIdentifiers.clear();
|
|
50
|
+
hasIconImport = false;
|
|
51
|
+
|
|
52
|
+
// Check if Icon is already imported from @mdi/react
|
|
53
|
+
path.node.body.forEach(node => {
|
|
54
|
+
if (t.isImportDeclaration(node) && node.source.value === '@mdi/react') {
|
|
55
|
+
console.log('[mdi-auto-import] Found @mdi/react import');
|
|
56
|
+
const hasIconSpecifier = node.specifiers.some(spec =>
|
|
57
|
+
t.isImportDefaultSpecifier(spec) && spec.local.name === 'MdiIcon'
|
|
58
|
+
);
|
|
59
|
+
if (hasIconSpecifier) {
|
|
60
|
+
console.log('[mdi-auto-import] MdiIcon already imported');
|
|
61
|
+
hasIconImport = true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
exit(path) {
|
|
67
|
+
if (importedIcons.size === 0) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
console.log(`[mdi-auto-import] importedIcons.size: ${importedIcons.size}`);
|
|
71
|
+
|
|
72
|
+
// Add imports at the top of the file if any icons were used
|
|
73
|
+
if (importedIcons.size > 0) {
|
|
74
|
+
console.log('[mdi-auto-import] Adding imports for icons:', Array.from(importedIcons));
|
|
75
|
+
|
|
76
|
+
// Import individual icons from @mdi/js
|
|
77
|
+
const iconImportSpecifiers = Array.from(importedIcons).map(iconName => {
|
|
78
|
+
const identifier = getIconIdentifier(iconName);
|
|
79
|
+
return t.importSpecifier(
|
|
80
|
+
t.identifier(identifier),
|
|
81
|
+
t.identifier(iconName)
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const iconImportDeclaration = t.importDeclaration(
|
|
86
|
+
iconImportSpecifiers,
|
|
87
|
+
t.stringLiteral('@mdi/js')
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Import Icon component from @mdi/react if not already imported
|
|
91
|
+
if (!hasIconImport) {
|
|
92
|
+
console.log('[mdi-auto-import] Adding MdiIcon import from @mdi/react');
|
|
93
|
+
const iconComponentImport = t.importDeclaration(
|
|
94
|
+
[t.importDefaultSpecifier(t.identifier('MdiIcon'))],
|
|
95
|
+
t.stringLiteral('@mdi/react')
|
|
96
|
+
);
|
|
97
|
+
path.unshiftContainer('body', iconComponentImport);
|
|
98
|
+
} else {
|
|
99
|
+
console.log('[mdi-auto-import] MdiIcon already imported, skipping');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Add icon imports
|
|
103
|
+
path.unshiftContainer('body', iconImportDeclaration);
|
|
104
|
+
console.log('[mdi-auto-import] Imports added successfully');
|
|
105
|
+
} else {
|
|
106
|
+
console.log('[mdi-auto-import] No icons to import');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
JSXElement(path) {
|
|
112
|
+
const { node } = path;
|
|
113
|
+
|
|
114
|
+
// Check if this is an Icon component from @idealyst/components
|
|
115
|
+
if (
|
|
116
|
+
t.isJSXIdentifier(node.openingElement.name) &&
|
|
117
|
+
node.openingElement.name.name === 'Icon'
|
|
118
|
+
) {
|
|
119
|
+
|
|
120
|
+
// Find the name attribute
|
|
121
|
+
const nameAttr = node.openingElement.attributes.find(attr =>
|
|
122
|
+
t.isJSXAttribute(attr) &&
|
|
123
|
+
t.isJSXIdentifier(attr.name) &&
|
|
124
|
+
attr.name.name === 'name'
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
if (!nameAttr) {
|
|
128
|
+
console.log('[mdi-auto-import] No name attribute found');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let iconName;
|
|
133
|
+
|
|
134
|
+
// Handle both string literals and JSX expressions
|
|
135
|
+
if (nameAttr && t.isStringLiteral(nameAttr.value)) {
|
|
136
|
+
iconName = nameAttr.value.value;
|
|
137
|
+
} else if (nameAttr && t.isJSXExpressionContainer(nameAttr.value)) {
|
|
138
|
+
// Handle JSX expressions like name={'icon-name'} or name={iconVariable}
|
|
139
|
+
const expression = nameAttr.value.expression;
|
|
140
|
+
if (t.isStringLiteral(expression)) {
|
|
141
|
+
iconName = expression.value;
|
|
142
|
+
} else {
|
|
143
|
+
// For dynamic expressions, we can't determine the icon at build time
|
|
144
|
+
// Skip transformation but warn the user
|
|
145
|
+
console.warn(`[mdi-auto-import] Cannot determine icon name for dynamic expression at ${path.node.loc ? `${path.node.loc.start.line}:${path.node.loc.start.column}` : 'unknown location'}`);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (iconName) {
|
|
151
|
+
console.log(`[mdi-auto-import] Processing icon: ${iconName}`);
|
|
152
|
+
try {
|
|
153
|
+
const mdiIconName = getMdiIconName(iconName);
|
|
154
|
+
const iconIdentifier = getIconIdentifier(mdiIconName);
|
|
155
|
+
|
|
156
|
+
// Track that we need to import this icon
|
|
157
|
+
importedIcons.add(mdiIconName);
|
|
158
|
+
console.log(`[mdi-auto-import] Added icon to import list: ${mdiIconName}`);
|
|
159
|
+
|
|
160
|
+
// Replace name="iconName" with path={iconIdentifier}
|
|
161
|
+
const pathAttr = t.jsxAttribute(
|
|
162
|
+
t.jsxIdentifier('path'),
|
|
163
|
+
t.jsxExpressionContainer(t.identifier(iconIdentifier))
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// Remove the name attribute and add the path attribute
|
|
167
|
+
node.openingElement.attributes = node.openingElement.attributes
|
|
168
|
+
.filter(attr => !(
|
|
169
|
+
t.isJSXAttribute(attr) &&
|
|
170
|
+
t.isJSXIdentifier(attr.name) &&
|
|
171
|
+
attr.name.name === 'name'
|
|
172
|
+
))
|
|
173
|
+
.concat(pathAttr);
|
|
174
|
+
|
|
175
|
+
console.log(`[mdi-auto-import] Transformed Icon component: name="${iconName}" -> path={${iconIdentifier}}`);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
console.error(`[mdi-auto-import] Error processing icon "${iconName}": ${error.message}`);
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
console.log('[mdi-auto-import] No icon name found');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { View, Text, Image } from 'react-native';
|
|
3
|
+
import { AvatarProps } from './types';
|
|
4
|
+
import { avatarStyles } from './Avatar.styles';
|
|
5
|
+
|
|
6
|
+
const Avatar: React.FC<AvatarProps> = ({
|
|
7
|
+
src,
|
|
8
|
+
alt,
|
|
9
|
+
fallback,
|
|
10
|
+
size = 'medium',
|
|
11
|
+
shape = 'circle',
|
|
12
|
+
style,
|
|
13
|
+
testID,
|
|
14
|
+
}) => {
|
|
15
|
+
const [hasError, setHasError] = useState(false);
|
|
16
|
+
|
|
17
|
+
avatarStyles.useVariants({
|
|
18
|
+
size,
|
|
19
|
+
shape,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const handleImageError = () => {
|
|
23
|
+
setHasError(true);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<View style={[avatarStyles.avatar, style]} testID={testID}>
|
|
28
|
+
{src && !hasError ? (
|
|
29
|
+
<Image
|
|
30
|
+
source={typeof src === 'string' ? { uri: src } : src}
|
|
31
|
+
style={avatarStyles.image}
|
|
32
|
+
onError={handleImageError}
|
|
33
|
+
accessibilityLabel={alt}
|
|
34
|
+
/>
|
|
35
|
+
) : (
|
|
36
|
+
<Text style={avatarStyles.fallback}>
|
|
37
|
+
{fallback}
|
|
38
|
+
</Text>
|
|
39
|
+
)}
|
|
40
|
+
</View>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export default Avatar;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
2
|
+
|
|
3
|
+
export const avatarStyles = StyleSheet.create((theme) => ({
|
|
4
|
+
avatar: {
|
|
5
|
+
display: 'flex',
|
|
6
|
+
alignItems: 'center',
|
|
7
|
+
justifyContent: 'center',
|
|
8
|
+
backgroundColor: theme.colors?.surface?.secondary || '#f3f4f6',
|
|
9
|
+
overflow: 'hidden',
|
|
10
|
+
|
|
11
|
+
variants: {
|
|
12
|
+
size: {
|
|
13
|
+
small: {
|
|
14
|
+
width: 32,
|
|
15
|
+
height: 32,
|
|
16
|
+
},
|
|
17
|
+
medium: {
|
|
18
|
+
width: 40,
|
|
19
|
+
height: 40,
|
|
20
|
+
},
|
|
21
|
+
large: {
|
|
22
|
+
width: 48,
|
|
23
|
+
height: 48,
|
|
24
|
+
},
|
|
25
|
+
xlarge: {
|
|
26
|
+
width: 64,
|
|
27
|
+
height: 64,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
shape: {
|
|
31
|
+
circle: {
|
|
32
|
+
borderRadius: 9999,
|
|
33
|
+
},
|
|
34
|
+
square: {
|
|
35
|
+
borderRadius: theme.borderRadius?.md || 8,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
image: {
|
|
42
|
+
width: '100%',
|
|
43
|
+
height: '100%',
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
fallback: {
|
|
47
|
+
color: theme.colors?.text?.primary || '#374151',
|
|
48
|
+
fontWeight: '600',
|
|
49
|
+
|
|
50
|
+
variants: {
|
|
51
|
+
size: {
|
|
52
|
+
small: {
|
|
53
|
+
fontSize: 14,
|
|
54
|
+
},
|
|
55
|
+
medium: {
|
|
56
|
+
fontSize: 16,
|
|
57
|
+
},
|
|
58
|
+
large: {
|
|
59
|
+
fontSize: 18,
|
|
60
|
+
},
|
|
61
|
+
xlarge: {
|
|
62
|
+
fontSize: 24,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
}));
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { getWebProps } from 'react-native-unistyles/web';
|
|
3
|
+
import { AvatarProps } from './types';
|
|
4
|
+
import { avatarStyles } from './Avatar.styles';
|
|
5
|
+
|
|
6
|
+
const Avatar: React.FC<AvatarProps> = ({
|
|
7
|
+
src,
|
|
8
|
+
alt,
|
|
9
|
+
fallback,
|
|
10
|
+
size = 'medium',
|
|
11
|
+
shape = 'circle',
|
|
12
|
+
style,
|
|
13
|
+
testID,
|
|
14
|
+
}) => {
|
|
15
|
+
const [hasError, setHasError] = useState(false);
|
|
16
|
+
|
|
17
|
+
avatarStyles.useVariants({
|
|
18
|
+
size,
|
|
19
|
+
shape,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const avatarStyleArray = [avatarStyles.avatar, style];
|
|
23
|
+
const avatarProps = getWebProps(avatarStyleArray);
|
|
24
|
+
|
|
25
|
+
// Generate fallback text styles with proper theming and size
|
|
26
|
+
const fallbackStyleArray = [avatarStyles.fallback];
|
|
27
|
+
const fallbackProps = getWebProps(fallbackStyleArray);
|
|
28
|
+
|
|
29
|
+
const handleImageError = () => {
|
|
30
|
+
setHasError(true);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div {...avatarProps} data-testid={testID}>
|
|
35
|
+
{src && !hasError ? (
|
|
36
|
+
<img
|
|
37
|
+
src={src}
|
|
38
|
+
alt={alt}
|
|
39
|
+
onError={handleImageError}
|
|
40
|
+
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
|
41
|
+
/>
|
|
42
|
+
) : (
|
|
43
|
+
<span {...fallbackProps}>
|
|
44
|
+
{fallback}
|
|
45
|
+
</span>
|
|
46
|
+
)}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export default Avatar;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ColorVariant } from '../theme/variants';
|
|
2
|
+
|
|
3
|
+
export interface AvatarProps {
|
|
4
|
+
/**
|
|
5
|
+
* Image source (URL or require())
|
|
6
|
+
*/
|
|
7
|
+
src?: string | any;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Alt text for the image
|
|
11
|
+
*/
|
|
12
|
+
alt?: string;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Fallback text (usually initials)
|
|
16
|
+
*/
|
|
17
|
+
fallback?: string;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Size of the avatar
|
|
21
|
+
*/
|
|
22
|
+
size?: 'small' | 'medium' | 'large' | 'xlarge';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Shape of the avatar
|
|
26
|
+
*/
|
|
27
|
+
shape?: 'circle' | 'square';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The color scheme of the avatar (for background when no image)
|
|
31
|
+
*/
|
|
32
|
+
color?: ColorVariant;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Additional styles
|
|
36
|
+
*/
|
|
37
|
+
style?: any;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Test ID for testing
|
|
41
|
+
*/
|
|
42
|
+
testID?: string;
|
|
43
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Text } from 'react-native';
|
|
3
|
+
import { BadgeProps } from './types';
|
|
4
|
+
import { badgeStyles } from './Badge.styles';
|
|
5
|
+
|
|
6
|
+
const Badge: React.FC<BadgeProps> = ({
|
|
7
|
+
children,
|
|
8
|
+
size = 'medium',
|
|
9
|
+
variant = 'filled',
|
|
10
|
+
color = 'blue',
|
|
11
|
+
style,
|
|
12
|
+
testID,
|
|
13
|
+
}) => {
|
|
14
|
+
badgeStyles.useVariants({
|
|
15
|
+
size,
|
|
16
|
+
variant: variant as any,
|
|
17
|
+
color,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
if (variant === 'dot') {
|
|
21
|
+
return (
|
|
22
|
+
<View
|
|
23
|
+
style={[badgeStyles.badge, style]}
|
|
24
|
+
testID={testID}
|
|
25
|
+
accessibilityLabel="status indicator"
|
|
26
|
+
/>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<View
|
|
32
|
+
style={[badgeStyles.badge, style]}
|
|
33
|
+
testID={testID}
|
|
34
|
+
accessibilityLabel="badge"
|
|
35
|
+
>
|
|
36
|
+
<Text style={badgeStyles.text}>
|
|
37
|
+
{children}
|
|
38
|
+
</Text>
|
|
39
|
+
</View>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export default Badge;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
2
|
+
import { generateBadgeCompoundVariants } from '../theme/variantHelpers';
|
|
3
|
+
|
|
4
|
+
export const badgeStyles = StyleSheet.create((theme) => ({
|
|
5
|
+
badge: {
|
|
6
|
+
alignItems: 'center',
|
|
7
|
+
justifyContent: 'center',
|
|
8
|
+
borderRadius: 9999,
|
|
9
|
+
|
|
10
|
+
// Web-specific styles for better text centering
|
|
11
|
+
_web: {
|
|
12
|
+
display: 'flex',
|
|
13
|
+
alignItems: 'center',
|
|
14
|
+
justifyContent: 'center',
|
|
15
|
+
boxSizing: 'border-box',
|
|
16
|
+
// Text styles for web (since text is rendered directly in the container)
|
|
17
|
+
fontSize: 12, // Default medium size
|
|
18
|
+
fontWeight: '600',
|
|
19
|
+
color: '#ffffff',
|
|
20
|
+
lineHeight: 1, // Use ratio instead of fixed value for better centering
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
variants: {
|
|
24
|
+
size: {
|
|
25
|
+
small: {
|
|
26
|
+
minWidth: 16,
|
|
27
|
+
height: 16,
|
|
28
|
+
paddingHorizontal: 4,
|
|
29
|
+
},
|
|
30
|
+
medium: {
|
|
31
|
+
minWidth: 20,
|
|
32
|
+
height: 20,
|
|
33
|
+
paddingHorizontal: 6,
|
|
34
|
+
},
|
|
35
|
+
large: {
|
|
36
|
+
minWidth: 24,
|
|
37
|
+
height: 24,
|
|
38
|
+
paddingHorizontal: 8,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
variant: {
|
|
42
|
+
filled: {
|
|
43
|
+
// Colors handled by compound variants
|
|
44
|
+
},
|
|
45
|
+
outlined: {
|
|
46
|
+
backgroundColor: 'transparent',
|
|
47
|
+
borderWidth: 1,
|
|
48
|
+
},
|
|
49
|
+
dot: {
|
|
50
|
+
minWidth: 8,
|
|
51
|
+
width: 8,
|
|
52
|
+
height: 8,
|
|
53
|
+
paddingHorizontal: 0, // Override size variant padding
|
|
54
|
+
paddingVertical: 0,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
// Dynamically generated color variants (placeholder - actual styling in compound variants)
|
|
58
|
+
color: Object.fromEntries([
|
|
59
|
+
// All palette colors including shade-specific variants
|
|
60
|
+
...(theme.palettes ? Object.entries(theme.palettes).flatMap(([colorKey, palette]) => [
|
|
61
|
+
// Base color
|
|
62
|
+
[colorKey, {}],
|
|
63
|
+
// All shade variants
|
|
64
|
+
...Object.keys(palette).map((shade) => [`${colorKey}.${shade}`, {}])
|
|
65
|
+
]) : [
|
|
66
|
+
// Fallback base colors
|
|
67
|
+
['blue', {}], ['green', {}], ['red', {}], ['amber', {}],
|
|
68
|
+
['gray', {}], ['purple', {}], ['pink', {}], ['cyan', {}]
|
|
69
|
+
]),
|
|
70
|
+
// Semantic colors
|
|
71
|
+
['primary', {}],
|
|
72
|
+
['secondary', {}],
|
|
73
|
+
['disabled', {}],
|
|
74
|
+
['inverse', {}],
|
|
75
|
+
['muted', {}],
|
|
76
|
+
]),
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
// Dynamically generated compound variants plus static size variants
|
|
80
|
+
compoundVariants: [
|
|
81
|
+
...generateBadgeCompoundVariants(theme),
|
|
82
|
+
// Web-specific text size variants
|
|
83
|
+
{
|
|
84
|
+
size: 'small',
|
|
85
|
+
styles: {
|
|
86
|
+
_web: {
|
|
87
|
+
fontSize: 10,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
size: 'medium',
|
|
93
|
+
styles: {
|
|
94
|
+
_web: {
|
|
95
|
+
fontSize: 12,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
size: 'large',
|
|
101
|
+
styles: {
|
|
102
|
+
_web: {
|
|
103
|
+
fontSize: 14,
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
text: {
|
|
111
|
+
color: '#ffffff',
|
|
112
|
+
fontWeight: '600',
|
|
113
|
+
textAlign: 'center',
|
|
114
|
+
|
|
115
|
+
variants: {
|
|
116
|
+
size: {
|
|
117
|
+
small: {
|
|
118
|
+
fontSize: 10,
|
|
119
|
+
lineHeight: 12, // Tight line height for better vertical centering
|
|
120
|
+
},
|
|
121
|
+
medium: {
|
|
122
|
+
fontSize: 12,
|
|
123
|
+
lineHeight: 14,
|
|
124
|
+
},
|
|
125
|
+
large: {
|
|
126
|
+
fontSize: 14,
|
|
127
|
+
lineHeight: 16,
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
variant: {
|
|
131
|
+
filled: {
|
|
132
|
+
color: '#ffffff',
|
|
133
|
+
},
|
|
134
|
+
outlined: {
|
|
135
|
+
// Colors handled by compound variants
|
|
136
|
+
},
|
|
137
|
+
dot: {
|
|
138
|
+
// No text for dot variant
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
compoundVariants: [
|
|
144
|
+
// Outlined variant text colors - dynamically generated
|
|
145
|
+
...Object.entries(theme.palettes || {}).map(([colorKey, palette]: [string, any]) => ({
|
|
146
|
+
variant: 'outlined',
|
|
147
|
+
color: colorKey,
|
|
148
|
+
styles: {
|
|
149
|
+
color: palette?.[500] || '#6b7280',
|
|
150
|
+
},
|
|
151
|
+
})),
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
}));
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { getWebProps } from 'react-native-unistyles/web';
|
|
3
|
+
import { BadgeProps } from './types';
|
|
4
|
+
import { badgeStyles } from './Badge.styles';
|
|
5
|
+
|
|
6
|
+
const Badge: React.FC<BadgeProps> = ({
|
|
7
|
+
children,
|
|
8
|
+
size = 'medium',
|
|
9
|
+
variant = 'filled',
|
|
10
|
+
color = 'blue',
|
|
11
|
+
style,
|
|
12
|
+
testID,
|
|
13
|
+
}) => {
|
|
14
|
+
badgeStyles.useVariants({
|
|
15
|
+
size,
|
|
16
|
+
variant: variant as any,
|
|
17
|
+
color,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const badgeStyleArray = [badgeStyles.badge, style];
|
|
21
|
+
const badgeProps = getWebProps(badgeStyleArray);
|
|
22
|
+
|
|
23
|
+
if (variant === 'dot') {
|
|
24
|
+
return (
|
|
25
|
+
<span
|
|
26
|
+
{...badgeProps}
|
|
27
|
+
data-testid={testID}
|
|
28
|
+
role="status"
|
|
29
|
+
aria-label="status indicator"
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<span
|
|
36
|
+
{...badgeProps}
|
|
37
|
+
data-testid={testID}
|
|
38
|
+
role="status"
|
|
39
|
+
>
|
|
40
|
+
{children}
|
|
41
|
+
</span>
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export default Badge;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import type { DisplayColorVariant } from '../theme/variants';
|
|
3
|
+
|
|
4
|
+
export interface BadgeProps {
|
|
5
|
+
/**
|
|
6
|
+
* The content to display inside the badge
|
|
7
|
+
*/
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The size of the badge
|
|
12
|
+
*/
|
|
13
|
+
size?: 'small' | 'medium' | 'large';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The visual style variant of the badge
|
|
17
|
+
*/
|
|
18
|
+
variant?: 'filled' | 'outlined' | 'dot';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The color scheme of the badge
|
|
22
|
+
*/
|
|
23
|
+
color?: DisplayColorVariant;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Additional styles
|
|
27
|
+
*/
|
|
28
|
+
style?: any;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Test ID for testing
|
|
32
|
+
*/
|
|
33
|
+
testID?: string;
|
|
34
|
+
}
|