@metacells/mcellui-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +50 -0
- package/src/components/ErrorBoundary.tsx +188 -0
- package/src/components/index.ts +6 -0
- package/src/config/ConfigProvider.tsx +117 -0
- package/src/config/defineConfig.ts +75 -0
- package/src/config/index.ts +42 -0
- package/src/config/types.ts +185 -0
- package/src/constants.ts +203 -0
- package/src/index.ts +144 -0
- package/src/primitives/Portal.tsx +185 -0
- package/src/primitives/Pressable.tsx +114 -0
- package/src/primitives/Slot.tsx +177 -0
- package/src/primitives/index.ts +19 -0
- package/src/theme/ThemeProvider.tsx +475 -0
- package/src/theme/animations.ts +379 -0
- package/src/theme/colors.ts +266 -0
- package/src/theme/components.ts +267 -0
- package/src/theme/index.ts +130 -0
- package/src/theme/presets.ts +341 -0
- package/src/theme/radius.ts +175 -0
- package/src/theme/shadows.ts +166 -0
- package/src/theme/spacing.ts +41 -0
- package/src/theme/typography.ts +389 -0
- package/src/tokens/colors.ts +67 -0
- package/src/tokens/index.ts +29 -0
- package/src/tokens/radius.ts +18 -0
- package/src/tokens/shadows.ts +38 -0
- package/src/tokens/spacing.ts +45 -0
- package/src/tokens/typography.ts +70 -0
- package/src/utils/accessibility.ts +112 -0
- package/src/utils/cn.ts +58 -0
- package/src/utils/haptics.ts +125 -0
- package/src/utils/index.ts +28 -0
- package/src/utils/platform.ts +66 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accessibility Utilities
|
|
3
|
+
*
|
|
4
|
+
* Helpers for building accessible components
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { AccessibilityRole, AccessibilityState } from 'react-native';
|
|
8
|
+
|
|
9
|
+
export interface A11yProps {
|
|
10
|
+
accessible?: boolean;
|
|
11
|
+
accessibilityLabel?: string;
|
|
12
|
+
accessibilityHint?: string;
|
|
13
|
+
accessibilityRole?: AccessibilityRole;
|
|
14
|
+
accessibilityState?: AccessibilityState;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Creates accessibility props for a button
|
|
19
|
+
*/
|
|
20
|
+
export function buttonA11y(
|
|
21
|
+
label: string,
|
|
22
|
+
options?: {
|
|
23
|
+
hint?: string;
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
selected?: boolean;
|
|
26
|
+
}
|
|
27
|
+
): A11yProps {
|
|
28
|
+
return {
|
|
29
|
+
accessible: true,
|
|
30
|
+
accessibilityRole: 'button',
|
|
31
|
+
accessibilityLabel: label,
|
|
32
|
+
accessibilityHint: options?.hint,
|
|
33
|
+
accessibilityState: {
|
|
34
|
+
disabled: options?.disabled,
|
|
35
|
+
selected: options?.selected,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Creates accessibility props for a checkbox/switch
|
|
42
|
+
*/
|
|
43
|
+
export function toggleA11y(
|
|
44
|
+
label: string,
|
|
45
|
+
checked: boolean,
|
|
46
|
+
options?: {
|
|
47
|
+
hint?: string;
|
|
48
|
+
disabled?: boolean;
|
|
49
|
+
}
|
|
50
|
+
): A11yProps {
|
|
51
|
+
return {
|
|
52
|
+
accessible: true,
|
|
53
|
+
accessibilityRole: 'switch',
|
|
54
|
+
accessibilityLabel: label,
|
|
55
|
+
accessibilityHint: options?.hint,
|
|
56
|
+
accessibilityState: {
|
|
57
|
+
checked,
|
|
58
|
+
disabled: options?.disabled,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Creates accessibility props for a link
|
|
65
|
+
*/
|
|
66
|
+
export function linkA11y(
|
|
67
|
+
label: string,
|
|
68
|
+
options?: {
|
|
69
|
+
hint?: string;
|
|
70
|
+
}
|
|
71
|
+
): A11yProps {
|
|
72
|
+
return {
|
|
73
|
+
accessible: true,
|
|
74
|
+
accessibilityRole: 'link',
|
|
75
|
+
accessibilityLabel: label,
|
|
76
|
+
accessibilityHint: options?.hint ?? 'Double tap to open',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Creates accessibility props for an image
|
|
82
|
+
*/
|
|
83
|
+
export function imageA11y(
|
|
84
|
+
label: string,
|
|
85
|
+
options?: {
|
|
86
|
+
decorative?: boolean;
|
|
87
|
+
}
|
|
88
|
+
): A11yProps {
|
|
89
|
+
if (options?.decorative) {
|
|
90
|
+
return {
|
|
91
|
+
accessible: false,
|
|
92
|
+
accessibilityLabel: undefined,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
accessible: true,
|
|
98
|
+
accessibilityRole: 'image',
|
|
99
|
+
accessibilityLabel: label,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Creates accessibility props for a heading
|
|
105
|
+
*/
|
|
106
|
+
export function headingA11y(label: string): A11yProps {
|
|
107
|
+
return {
|
|
108
|
+
accessible: true,
|
|
109
|
+
accessibilityRole: 'header',
|
|
110
|
+
accessibilityLabel: label,
|
|
111
|
+
};
|
|
112
|
+
}
|
package/src/utils/cn.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cn() - Class Name / Style Merger
|
|
3
|
+
*
|
|
4
|
+
* The core utility for merging styles in nativeui.
|
|
5
|
+
* Inspired by shadcn/ui's cn() but adapted for React Native StyleSheet.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { StyleSheet, ViewStyle, TextStyle, ImageStyle } from 'react-native';
|
|
9
|
+
|
|
10
|
+
type Style = ViewStyle | TextStyle | ImageStyle;
|
|
11
|
+
type StyleInput = Style | Style[] | null | undefined | false;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Merges multiple style objects into a single flattened style.
|
|
15
|
+
* Handles undefined, null, false, and nested arrays.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* const styles = cn(
|
|
20
|
+
* baseStyles.container,
|
|
21
|
+
* isActive && activeStyles,
|
|
22
|
+
* { padding: 16 }
|
|
23
|
+
* );
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function cn(...inputs: StyleInput[]): Style {
|
|
27
|
+
const styles: Style[] = [];
|
|
28
|
+
|
|
29
|
+
for (const input of inputs) {
|
|
30
|
+
if (!input) continue;
|
|
31
|
+
|
|
32
|
+
if (Array.isArray(input)) {
|
|
33
|
+
const flattened = StyleSheet.flatten(input);
|
|
34
|
+
if (flattened) styles.push(flattened);
|
|
35
|
+
} else {
|
|
36
|
+
styles.push(input);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return StyleSheet.flatten(styles);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Creates a style object from conditional styles.
|
|
45
|
+
* More explicit alternative to cn() for complex conditions.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```tsx
|
|
49
|
+
* const styles = mergeStyles({
|
|
50
|
+
* base: baseStyle,
|
|
51
|
+
* active: isActive && activeStyle,
|
|
52
|
+
* disabled: isDisabled && disabledStyle,
|
|
53
|
+
* });
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export function mergeStyles(styles: Record<string, StyleInput>): Style {
|
|
57
|
+
return cn(...Object.values(styles));
|
|
58
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Haptics Utilities
|
|
3
|
+
*
|
|
4
|
+
* Unified haptic feedback interface.
|
|
5
|
+
* Falls back gracefully when expo-haptics is not available.
|
|
6
|
+
*
|
|
7
|
+
* Design Philosophy:
|
|
8
|
+
* - Every interactive element should provide tactile feedback
|
|
9
|
+
* - Haptics should be subtle and purposeful
|
|
10
|
+
* - Respect user's haptic preferences
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type HapticStyle = 'light' | 'medium' | 'heavy' | 'success' | 'warning' | 'error' | 'selection';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Haptic presets for common interactions
|
|
17
|
+
*/
|
|
18
|
+
export const hapticPresets = {
|
|
19
|
+
/** For button presses, checkbox toggles */
|
|
20
|
+
buttonPress: 'light' as HapticStyle,
|
|
21
|
+
/** For switch toggles */
|
|
22
|
+
toggle: 'medium' as HapticStyle,
|
|
23
|
+
/** For successful actions */
|
|
24
|
+
success: 'success' as HapticStyle,
|
|
25
|
+
/** For errors */
|
|
26
|
+
error: 'error' as HapticStyle,
|
|
27
|
+
/** For selection changes (radio, picker) */
|
|
28
|
+
selection: 'selection' as HapticStyle,
|
|
29
|
+
/** For destructive actions */
|
|
30
|
+
destructive: 'warning' as HapticStyle,
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
let Haptics: typeof import('expo-haptics') | null = null;
|
|
34
|
+
|
|
35
|
+
// Try to load expo-haptics (optional dependency)
|
|
36
|
+
try {
|
|
37
|
+
Haptics = require('expo-haptics');
|
|
38
|
+
} catch {
|
|
39
|
+
// expo-haptics not available, haptics will be no-ops
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
43
|
+
// Global Haptics State
|
|
44
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
let hapticsEnabled = true;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Enable or disable haptic feedback globally.
|
|
50
|
+
* Use this to respect user preferences or accessibility settings.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```tsx
|
|
54
|
+
* // In ThemeProvider or app setup
|
|
55
|
+
* setHapticsEnabled(false);
|
|
56
|
+
*
|
|
57
|
+
* // Or via config
|
|
58
|
+
* <ThemeProvider haptics={false}>
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export function setHapticsEnabled(enabled: boolean): void {
|
|
62
|
+
hapticsEnabled = enabled;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Check if haptics are currently enabled
|
|
67
|
+
*/
|
|
68
|
+
export function isHapticsEnabled(): boolean {
|
|
69
|
+
return hapticsEnabled && Haptics !== null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Trigger haptic feedback
|
|
74
|
+
*
|
|
75
|
+
* Respects the global haptics enabled state. Disabled via:
|
|
76
|
+
* - `setHapticsEnabled(false)`
|
|
77
|
+
* - `<ThemeProvider haptics={false}>`
|
|
78
|
+
* - `nativeui.config.ts` with `haptics: false`
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```tsx
|
|
82
|
+
* onPress={() => {
|
|
83
|
+
* haptic('light');
|
|
84
|
+
* // ... rest of handler
|
|
85
|
+
* }}
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export async function haptic(style: HapticStyle = 'light'): Promise<void> {
|
|
89
|
+
if (!hapticsEnabled || !Haptics) return;
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
switch (style) {
|
|
93
|
+
case 'light':
|
|
94
|
+
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
|
95
|
+
break;
|
|
96
|
+
case 'medium':
|
|
97
|
+
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
|
98
|
+
break;
|
|
99
|
+
case 'heavy':
|
|
100
|
+
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy);
|
|
101
|
+
break;
|
|
102
|
+
case 'success':
|
|
103
|
+
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
|
104
|
+
break;
|
|
105
|
+
case 'warning':
|
|
106
|
+
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
|
|
107
|
+
break;
|
|
108
|
+
case 'error':
|
|
109
|
+
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
|
110
|
+
break;
|
|
111
|
+
case 'selection':
|
|
112
|
+
await Haptics.selectionAsync();
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
// Silently fail if haptics not supported
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Check if haptics are available
|
|
122
|
+
*/
|
|
123
|
+
export function hapticsAvailable(): boolean {
|
|
124
|
+
return Haptics !== null;
|
|
125
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export { cn, mergeStyles } from './cn';
|
|
2
|
+
export {
|
|
3
|
+
isIOS,
|
|
4
|
+
isAndroid,
|
|
5
|
+
isWeb,
|
|
6
|
+
iosVersion,
|
|
7
|
+
androidApiLevel,
|
|
8
|
+
isTablet,
|
|
9
|
+
pixelRatio,
|
|
10
|
+
roundToNearestPixel,
|
|
11
|
+
getFontScale,
|
|
12
|
+
} from './platform';
|
|
13
|
+
export {
|
|
14
|
+
buttonA11y,
|
|
15
|
+
toggleA11y,
|
|
16
|
+
linkA11y,
|
|
17
|
+
imageA11y,
|
|
18
|
+
headingA11y,
|
|
19
|
+
type A11yProps,
|
|
20
|
+
} from './accessibility';
|
|
21
|
+
export {
|
|
22
|
+
haptic,
|
|
23
|
+
hapticsAvailable,
|
|
24
|
+
setHapticsEnabled,
|
|
25
|
+
isHapticsEnabled,
|
|
26
|
+
hapticPresets,
|
|
27
|
+
type HapticStyle,
|
|
28
|
+
} from './haptics';
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform Utilities
|
|
3
|
+
*
|
|
4
|
+
* Helpers for platform-specific behavior
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Platform, Dimensions, PixelRatio } from 'react-native';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Check if running on iOS
|
|
11
|
+
*/
|
|
12
|
+
export const isIOS = Platform.OS === 'ios';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Check if running on Android
|
|
16
|
+
*/
|
|
17
|
+
export const isAndroid = Platform.OS === 'android';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if running on web
|
|
21
|
+
*/
|
|
22
|
+
export const isWeb = Platform.OS === 'web';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get iOS version (returns 0 on non-iOS)
|
|
26
|
+
*/
|
|
27
|
+
export const iosVersion = isIOS
|
|
28
|
+
? parseInt(String(Platform.Version), 10)
|
|
29
|
+
: 0;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get Android API level (returns 0 on non-Android)
|
|
33
|
+
*/
|
|
34
|
+
export const androidApiLevel = isAndroid
|
|
35
|
+
? (Platform.Version as number)
|
|
36
|
+
: 0;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if device is a tablet (rough heuristic)
|
|
40
|
+
*/
|
|
41
|
+
export function isTablet(): boolean {
|
|
42
|
+
const { width, height } = Dimensions.get('window');
|
|
43
|
+
const aspectRatio = Math.max(width, height) / Math.min(width, height);
|
|
44
|
+
const isLargeScreen = Math.min(width, height) >= 600;
|
|
45
|
+
|
|
46
|
+
return isLargeScreen && aspectRatio < 1.6;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get pixel ratio for crisp rendering
|
|
51
|
+
*/
|
|
52
|
+
export const pixelRatio = PixelRatio.get();
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Round to nearest pixel for crisp lines
|
|
56
|
+
*/
|
|
57
|
+
export function roundToNearestPixel(value: number): number {
|
|
58
|
+
return PixelRatio.roundToNearestPixel(value);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get safe font scale (clamped for accessibility)
|
|
63
|
+
*/
|
|
64
|
+
export function getFontScale(maxScale = 1.3): number {
|
|
65
|
+
return Math.min(PixelRatio.getFontScale(), maxScale);
|
|
66
|
+
}
|