@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,203 @@
1
+ /**
2
+ * NativeUI Constants
3
+ *
4
+ * Zentralisierte Magic Numbers und Konfigurationswerte.
5
+ * Diese Werte werden von Components verwendet und können
6
+ * über Props überschrieben werden.
7
+ *
8
+ * @example
9
+ * ```tsx
10
+ * import { SHEET_CONSTANTS } from '@nativeui/core';
11
+ *
12
+ * // In SheetContent:
13
+ * if (translationY > height * SHEET_CONSTANTS.closeThreshold) {
14
+ * close();
15
+ * }
16
+ * ```
17
+ */
18
+
19
+ // ============================================
20
+ // Sheet Constants
21
+ // ============================================
22
+
23
+ export const SHEET_CONSTANTS = {
24
+ /** Threshold (0-1) für automatisches Schließen beim Ziehen */
25
+ closeThreshold: 0.3,
26
+ /** Geschwindigkeit (px/s) für Swipe-to-Close */
27
+ velocityThreshold: 500,
28
+ /** Backdrop Fade-In Dauer (ms) */
29
+ backdropFadeInDuration: 200,
30
+ /** Backdrop Fade-Out Dauer (ms) */
31
+ backdropFadeOutDuration: 150,
32
+ /** Maximale Backdrop Opacity */
33
+ backdropMaxOpacity: 0.5,
34
+ /** Handle Breite (px) */
35
+ handleWidth: 36,
36
+ /** Handle Höhe (px) */
37
+ handleHeight: 4,
38
+ /** Handle Container Padding Top (px) */
39
+ handlePaddingTop: 12,
40
+ /** Handle Container Padding Bottom (px) */
41
+ handlePaddingBottom: 8,
42
+ /** Content Horizontal Padding (px) */
43
+ contentPaddingHorizontal: 16,
44
+ } as const;
45
+
46
+ // ============================================
47
+ // Dialog Constants
48
+ // ============================================
49
+
50
+ export const DIALOG_CONSTANTS = {
51
+ /** Screen Margin für Dialog Breite (px) */
52
+ screenMargin: 48,
53
+ /** Close Animation Dauer (ms) */
54
+ closeAnimationDuration: 150,
55
+ /** Start Scale für Enter Animation */
56
+ enterStartScale: 0.95,
57
+ /** Dialog Content Padding (px) */
58
+ contentPadding: 24,
59
+ /** Backdrop Max Opacity */
60
+ backdropMaxOpacity: 0.5,
61
+ } as const;
62
+
63
+ // ============================================
64
+ // Toast Constants
65
+ // ============================================
66
+
67
+ export const TOAST_CONSTANTS = {
68
+ /** Standard Anzeigedauer (ms) */
69
+ defaultDuration: 4000,
70
+ /** Maximale gleichzeitig sichtbare Toasts */
71
+ maxToasts: 3,
72
+ /** Fade-Out Dauer (ms) */
73
+ fadeOutDuration: 150,
74
+ /** Container Top Offset vom SafeArea (px) */
75
+ containerTopOffset: 8,
76
+ /** Toast Width Margin (px) - wird von Screen Width abgezogen */
77
+ widthMargin: 32,
78
+ /** Fallback Farben wenn Theme sie nicht hat */
79
+ fallbackColors: {
80
+ success: '#22c55e',
81
+ successForeground: '#ffffff',
82
+ warning: '#f59e0b',
83
+ warningForeground: '#000000',
84
+ },
85
+ } as const;
86
+
87
+ // ============================================
88
+ // Button Constants
89
+ // ============================================
90
+
91
+ export const BUTTON_CONSTANTS = {
92
+ /** Scale beim Drücken (0-1) */
93
+ pressScale: 0.97,
94
+ /** Opacity bei disabled */
95
+ disabledOpacity: 0.5,
96
+ } as const;
97
+
98
+ // ============================================
99
+ // Accordion Constants
100
+ // ============================================
101
+
102
+ export const ACCORDION_CONSTANTS = {
103
+ /** Animation Dauer (ms) */
104
+ animationDuration: 250,
105
+ /** Trigger Text Font Size */
106
+ triggerFontSize: 15,
107
+ /** Chevron Font Size */
108
+ chevronFontSize: 10,
109
+ /** Chevron Margin Left */
110
+ chevronMarginLeft: 8,
111
+ } as const;
112
+
113
+ // ============================================
114
+ // Swipeable Row Constants
115
+ // ============================================
116
+
117
+ export const SWIPEABLE_CONSTANTS = {
118
+ /** Standard Action Button Breite (px) */
119
+ actionWidth: 80,
120
+ /** Geschwindigkeit für Snap (px/s) */
121
+ velocityThreshold: 500,
122
+ /** Full Swipe Threshold (0-1 von Screen Width) */
123
+ fullSwipeThreshold: 0.5,
124
+ /** Full Swipe Animation Dauer (ms) */
125
+ fullSwipeAnimationDuration: 200,
126
+ /** Resistance Factor am Edge (0-1) */
127
+ resistanceFactor: 0.2,
128
+ /** Active Offset für Gesture Start (px) */
129
+ activeOffset: 10,
130
+ /** Action Icon Margin Bottom (px) */
131
+ actionIconMarginBottom: 4,
132
+ /** Action Button Horizontal Padding (px) */
133
+ actionButtonPaddingHorizontal: 8,
134
+ /** Action Label Font Size */
135
+ actionLabelFontSize: 12,
136
+ } as const;
137
+
138
+ // ============================================
139
+ // Animation Constants (ergänzend zu theme/animations)
140
+ // ============================================
141
+
142
+ export const ANIMATION_CONSTANTS = {
143
+ /** Standard Spring Damping */
144
+ springDamping: 20,
145
+ /** Standard Spring Stiffness */
146
+ springStiffness: 200,
147
+ } as const;
148
+
149
+ // ============================================
150
+ // Slider Constants
151
+ // ============================================
152
+
153
+ export const SLIDER_CONSTANTS = {
154
+ /** Track Höhe (px) */
155
+ trackHeight: 6,
156
+ /** Thumb Größe (px) */
157
+ thumbSize: 24,
158
+ /** Hit Slop für Touch (px) */
159
+ hitSlop: 10,
160
+ } as const;
161
+
162
+ // ============================================
163
+ // Stepper Constants
164
+ // ============================================
165
+
166
+ export const STEPPER_CONSTANTS = {
167
+ /** Button Größe (px) */
168
+ buttonSize: 36,
169
+ /** Long Press Delay (ms) */
170
+ longPressDelay: 500,
171
+ /** Repeat Interval (ms) */
172
+ repeatInterval: 100,
173
+ /** Min Input Width (px) */
174
+ minInputWidth: 48,
175
+ } as const;
176
+
177
+ // ============================================
178
+ // Typography Constants (für Component Styles)
179
+ // ============================================
180
+
181
+ export const TYPOGRAPHY_CONSTANTS = {
182
+ /** Title Font Size für Overlays (Sheet, Dialog) */
183
+ overlayTitleSize: 18,
184
+ /** Description Font Size für Overlays */
185
+ overlayDescriptionSize: 14,
186
+ /** Description Line Height für Dialogs */
187
+ overlayDescriptionLineHeight: 20,
188
+ } as const;
189
+
190
+ // ============================================
191
+ // Export Types
192
+ // ============================================
193
+
194
+ export type SheetConstants = typeof SHEET_CONSTANTS;
195
+ export type DialogConstants = typeof DIALOG_CONSTANTS;
196
+ export type ToastConstants = typeof TOAST_CONSTANTS;
197
+ export type ButtonConstants = typeof BUTTON_CONSTANTS;
198
+ export type AccordionConstants = typeof ACCORDION_CONSTANTS;
199
+ export type SwipeableConstants = typeof SWIPEABLE_CONSTANTS;
200
+ export type AnimationConstants = typeof ANIMATION_CONSTANTS;
201
+ export type SliderConstants = typeof SLIDER_CONSTANTS;
202
+ export type StepperConstants = typeof STEPPER_CONSTANTS;
203
+ export type TypographyConstants = typeof TYPOGRAPHY_CONSTANTS;
package/src/index.ts ADDED
@@ -0,0 +1,144 @@
1
+ // Theme System (primary API)
2
+ export {
3
+ // Colors
4
+ palette,
5
+ lightColors,
6
+ darkColors,
7
+ type ThemeColors,
8
+ type ColorKey,
9
+ // Spacing
10
+ spacing as themeSpacing,
11
+ type SpacingKey as ThemeSpacingKey,
12
+ // Radius
13
+ radius as themeRadius,
14
+ componentRadius,
15
+ createRadius,
16
+ createComponentRadius,
17
+ radiusPresetBase,
18
+ defaultRadiusPreset,
19
+ type RadiusKey as ThemeRadiusKey,
20
+ type RadiusPreset,
21
+ type RadiusTokens,
22
+ // Fonts
23
+ defaultFonts,
24
+ systemFonts,
25
+ createTypography,
26
+ type Fonts,
27
+ type Typography,
28
+ // Typography
29
+ fontSize as themeFontSize,
30
+ fontWeight as themeFontWeight,
31
+ lineHeight as themeLineHeight,
32
+ letterSpacing as themeLetterSpacing,
33
+ fontFamily,
34
+ geistFontFamily,
35
+ typography,
36
+ type FontSizeKey,
37
+ type FontWeightKey,
38
+ type FontFamilyKey,
39
+ type GeistFontFamilyKey,
40
+ type TypographyKey,
41
+ // Shadows
42
+ getShadow,
43
+ getPlatformShadow,
44
+ shadows,
45
+ type ShadowStyle,
46
+ type ShadowSize,
47
+ // Animations
48
+ springs,
49
+ timing,
50
+ pressScale,
51
+ durations,
52
+ subtleAnimations,
53
+ playfulAnimations,
54
+ getAnimationPreset,
55
+ defaultAnimationPreset,
56
+ type SpringConfig,
57
+ type TimingConfig,
58
+ type SpringTokens,
59
+ type TimingTokens,
60
+ type PressScaleTokens,
61
+ type DurationTokens,
62
+ type AnimationTokens,
63
+ type SpringPreset,
64
+ type TimingPreset,
65
+ type PressScalePreset,
66
+ type AnimationPreset,
67
+ // Component Tokens
68
+ components,
69
+ componentHeight,
70
+ iconSize,
71
+ buttonTokens,
72
+ inputTokens,
73
+ checkboxTokens,
74
+ switchTokens,
75
+ badgeTokens,
76
+ avatarTokens,
77
+ cardTokens,
78
+ type ComponentSize,
79
+ // Theme Presets
80
+ themePresets,
81
+ defaultThemePreset,
82
+ getPresetColors,
83
+ getPresetColorsForMode,
84
+ type ThemePreset,
85
+ type PresetColors,
86
+ // Provider & Hooks
87
+ ThemeProvider,
88
+ useTheme,
89
+ useColorSchemeValue,
90
+ useIsDark,
91
+ type Theme,
92
+ type ThemeProviderProps,
93
+ type ColorScheme,
94
+ type ColorSchemePreference,
95
+ } from './theme';
96
+
97
+ // Design Tokens (legacy - prefer theme)
98
+ // Export only non-conflicting tokens
99
+ export {
100
+ colors as legacyColors,
101
+ type Colors as LegacyColors,
102
+ } from './tokens/colors';
103
+ export { spacing, type Spacing, type SpacingKey } from './tokens/spacing';
104
+ export {
105
+ fontFamily as legacyFontFamily,
106
+ fontSize,
107
+ fontWeight,
108
+ lineHeight,
109
+ letterSpacing,
110
+ type FontFamily,
111
+ type FontSize,
112
+ type FontWeight,
113
+ type LineHeight,
114
+ type LetterSpacing,
115
+ } from './tokens/typography';
116
+ export { radius, type Radius, type RadiusKey } from './tokens/radius';
117
+ export {
118
+ shadows as legacyShadows,
119
+ type Shadows as LegacyShadows,
120
+ type ShadowKey,
121
+ } from './tokens/shadows';
122
+
123
+ // Configuration
124
+ export {
125
+ defineConfig,
126
+ resolveConfig,
127
+ ConfigProvider,
128
+ useConfig,
129
+ defaultConfig,
130
+ type NativeUIConfig,
131
+ type ResolvedNativeUIConfig,
132
+ } from './config';
133
+
134
+ // Utilities
135
+ export * from './utils';
136
+
137
+ // Primitives
138
+ export * from './primitives';
139
+
140
+ // Components (Error Boundaries, etc.)
141
+ export * from './components';
142
+
143
+ // Constants (Magic Numbers)
144
+ export * from './constants';
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Portal Primitive
3
+ *
4
+ * Teleports children to a different part of the React tree.
5
+ * Essential for overlays like Dialogs, Toasts, Dropdowns.
6
+ *
7
+ * Usage:
8
+ * 1. Wrap your app with <PortalProvider>
9
+ * 2. Place <PortalHost /> at the root (after other content)
10
+ * 3. Use <Portal> anywhere to render content at the host
11
+ */
12
+
13
+ import React, {
14
+ createContext,
15
+ useContext,
16
+ useState,
17
+ useCallback,
18
+ useMemo,
19
+ useId,
20
+ } from 'react';
21
+ import { View, StyleSheet } from 'react-native';
22
+
23
+ // --- Types ---
24
+
25
+ export interface PortalContextValue {
26
+ /**
27
+ * Register a portal to be rendered at the host
28
+ */
29
+ mount: (key: string, element: React.ReactNode) => void;
30
+ /**
31
+ * Remove a portal from the host
32
+ */
33
+ unmount: (key: string) => void;
34
+ }
35
+
36
+ export interface PortalProviderProps {
37
+ children: React.ReactNode;
38
+ }
39
+
40
+ export interface PortalHostProps {
41
+ /**
42
+ * Name of the host (for multiple hosts)
43
+ * @default 'root'
44
+ */
45
+ name?: string;
46
+ }
47
+
48
+ export interface PortalProps {
49
+ children: React.ReactNode;
50
+ /**
51
+ * Name of the host to render into
52
+ * @default 'root'
53
+ */
54
+ hostName?: string;
55
+ }
56
+
57
+ // --- Context ---
58
+
59
+ const PortalContext = createContext<PortalContextValue | null>(null);
60
+
61
+ const usePortalContext = () => {
62
+ const context = useContext(PortalContext);
63
+ if (!context) {
64
+ throw new Error('Portal must be used within a PortalProvider');
65
+ }
66
+ return context;
67
+ };
68
+
69
+ // --- Components ---
70
+
71
+ /**
72
+ * Provides the portal context to the app.
73
+ * Wrap your root component with this provider.
74
+ */
75
+ export function PortalProvider({ children }: PortalProviderProps) {
76
+ const [portals, setPortals] = useState<Map<string, React.ReactNode>>(
77
+ new Map()
78
+ );
79
+
80
+ const mount = useCallback((key: string, element: React.ReactNode) => {
81
+ setPortals((prev) => {
82
+ const next = new Map(prev);
83
+ next.set(key, element);
84
+ return next;
85
+ });
86
+ }, []);
87
+
88
+ const unmount = useCallback((key: string) => {
89
+ setPortals((prev) => {
90
+ const next = new Map(prev);
91
+ next.delete(key);
92
+ return next;
93
+ });
94
+ }, []);
95
+
96
+ const contextValue = useMemo(() => ({ mount, unmount }), [mount, unmount]);
97
+
98
+ return (
99
+ <PortalContext.Provider value={contextValue}>
100
+ {children}
101
+ <PortalHost />
102
+ </PortalContext.Provider>
103
+ );
104
+ }
105
+
106
+ PortalProvider.displayName = 'PortalProvider';
107
+
108
+ /**
109
+ * The target where portals render their content.
110
+ * Automatically included in PortalProvider, but can be placed manually
111
+ * for custom layouts.
112
+ */
113
+ export function PortalHost({ name = 'root' }: PortalHostProps) {
114
+ const [portals, setPortals] = useState<Map<string, React.ReactNode>>(
115
+ new Map()
116
+ );
117
+
118
+ // Create a separate context for this specific host
119
+ const mount = useCallback((key: string, element: React.ReactNode) => {
120
+ setPortals((prev) => {
121
+ const next = new Map(prev);
122
+ next.set(key, element);
123
+ return next;
124
+ });
125
+ }, []);
126
+
127
+ const unmount = useCallback((key: string) => {
128
+ setPortals((prev) => {
129
+ const next = new Map(prev);
130
+ next.delete(key);
131
+ return next;
132
+ });
133
+ }, []);
134
+
135
+ return (
136
+ <PortalHostContext.Provider value={{ mount, unmount, name }}>
137
+ <View style={styles.host} pointerEvents="box-none">
138
+ {Array.from(portals.values())}
139
+ </View>
140
+ </PortalHostContext.Provider>
141
+ );
142
+ }
143
+
144
+ PortalHost.displayName = 'PortalHost';
145
+
146
+ // Host-specific context for named hosts
147
+ interface PortalHostContextValue extends PortalContextValue {
148
+ name: string;
149
+ }
150
+
151
+ const PortalHostContext = createContext<PortalHostContextValue | null>(null);
152
+
153
+ /**
154
+ * Renders children into the nearest PortalHost.
155
+ */
156
+ export function Portal({ children, hostName = 'root' }: PortalProps) {
157
+ const portalContext = useContext(PortalContext);
158
+ const key = useId();
159
+
160
+ React.useEffect(() => {
161
+ if (!portalContext) {
162
+ console.warn('Portal: No PortalProvider found. Content will not render.');
163
+ return;
164
+ }
165
+
166
+ portalContext.mount(key, children);
167
+
168
+ return () => {
169
+ portalContext.unmount(key);
170
+ };
171
+ }, [portalContext, key, children]);
172
+
173
+ return null;
174
+ }
175
+
176
+ Portal.displayName = 'Portal';
177
+
178
+ // --- Styles ---
179
+
180
+ const styles = StyleSheet.create({
181
+ host: {
182
+ ...StyleSheet.absoluteFillObject,
183
+ zIndex: 9999,
184
+ },
185
+ });
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Pressable Primitive
3
+ *
4
+ * Enhanced Pressable with:
5
+ * - Built-in haptic feedback
6
+ * - Reduce motion support
7
+ * - Configurable press states
8
+ */
9
+
10
+ import React, { useCallback } from 'react';
11
+ import {
12
+ Pressable as RNPressable,
13
+ PressableProps as RNPressableProps,
14
+ ViewStyle,
15
+ AccessibilityInfo,
16
+ } from 'react-native';
17
+ import { haptic } from '../utils/haptics';
18
+
19
+ export interface PressableProps extends Omit<RNPressableProps, 'style'> {
20
+ /**
21
+ * Style or style function receiving pressed state
22
+ */
23
+ style?: ViewStyle | ((state: { pressed: boolean }) => ViewStyle);
24
+
25
+ /**
26
+ * Haptic feedback style on press
27
+ * @default 'light'
28
+ */
29
+ hapticStyle?: 'none' | 'light' | 'medium' | 'heavy' | 'selection';
30
+
31
+ /**
32
+ * Opacity when pressed
33
+ * @default 0.7
34
+ */
35
+ pressedOpacity?: number;
36
+
37
+ /**
38
+ * Scale when pressed (respects reduce motion)
39
+ * @default 1
40
+ */
41
+ pressedScale?: number;
42
+ }
43
+
44
+ export const Pressable = React.forwardRef<
45
+ React.ElementRef<typeof RNPressable>,
46
+ PressableProps
47
+ >(
48
+ (
49
+ {
50
+ style,
51
+ hapticStyle = 'light',
52
+ pressedOpacity = 0.7,
53
+ pressedScale = 1,
54
+ onPressIn,
55
+ disabled,
56
+ children,
57
+ ...props
58
+ },
59
+ ref
60
+ ) => {
61
+ const [reduceMotion, setReduceMotion] = React.useState(false);
62
+
63
+ React.useEffect(() => {
64
+ AccessibilityInfo.isReduceMotionEnabled().then(setReduceMotion);
65
+ const subscription = AccessibilityInfo.addEventListener(
66
+ 'reduceMotionChanged',
67
+ setReduceMotion
68
+ );
69
+ return () => subscription.remove();
70
+ }, []);
71
+
72
+ const handlePressIn = useCallback(
73
+ (event: Parameters<NonNullable<RNPressableProps['onPressIn']>>[0]) => {
74
+ if (hapticStyle !== 'none') {
75
+ haptic(hapticStyle === 'selection' ? 'selection' : hapticStyle);
76
+ }
77
+ onPressIn?.(event);
78
+ },
79
+ [hapticStyle, onPressIn]
80
+ );
81
+
82
+ const getStyle = useCallback(
83
+ ({ pressed }: { pressed: boolean }): ViewStyle => {
84
+ const baseStyle = typeof style === 'function' ? style({ pressed }) : style;
85
+ const pressedStyle: ViewStyle = pressed
86
+ ? {
87
+ opacity: pressedOpacity,
88
+ transform: reduceMotion ? undefined : [{ scale: pressedScale }],
89
+ }
90
+ : {};
91
+
92
+ return {
93
+ ...baseStyle,
94
+ ...pressedStyle,
95
+ };
96
+ },
97
+ [style, pressedOpacity, pressedScale, reduceMotion]
98
+ );
99
+
100
+ return (
101
+ <RNPressable
102
+ ref={ref}
103
+ style={getStyle}
104
+ onPressIn={handlePressIn}
105
+ disabled={disabled}
106
+ {...props}
107
+ >
108
+ {children}
109
+ </RNPressable>
110
+ );
111
+ }
112
+ );
113
+
114
+ Pressable.displayName = 'Pressable';