@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.
@@ -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
+ }
@@ -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
+ }