@obisoft/ui 0.8.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.
Files changed (61) hide show
  1. package/README.md +45 -0
  2. package/package.json +52 -0
  3. package/src/components/AppHeader.tsx +67 -0
  4. package/src/components/BottomSheet.tsx +110 -0
  5. package/src/components/Button.tsx +117 -0
  6. package/src/components/Card.tsx +46 -0
  7. package/src/components/CenteredState.tsx +35 -0
  8. package/src/components/Chip.tsx +47 -0
  9. package/src/components/ChoiceField.tsx +44 -0
  10. package/src/components/DateRangeFilter.tsx +226 -0
  11. package/src/components/EmptyState.tsx +32 -0
  12. package/src/components/ErrorState.tsx +62 -0
  13. package/src/components/Fab.tsx +36 -0
  14. package/src/components/FormRow.tsx +39 -0
  15. package/src/components/FormWorkspaceShell.tsx +126 -0
  16. package/src/components/GlassTabBar.tsx +39 -0
  17. package/src/components/Icon.tsx +259 -0
  18. package/src/components/IconButton.tsx +73 -0
  19. package/src/components/ListFilterBar.tsx +134 -0
  20. package/src/components/ListRow.tsx +198 -0
  21. package/src/components/ListWorkspace.tsx +76 -0
  22. package/src/components/LoadingState.tsx +37 -0
  23. package/src/components/MetricCard.tsx +74 -0
  24. package/src/components/MetricsGrid.tsx +25 -0
  25. package/src/components/MiniBarChart.tsx +42 -0
  26. package/src/components/NavigationDrawer.tsx +243 -0
  27. package/src/components/QueryState.tsx +82 -0
  28. package/src/components/QueryWorkspace.tsx +74 -0
  29. package/src/components/ScanInputBar.tsx +69 -0
  30. package/src/components/ScannerShell.tsx +76 -0
  31. package/src/components/Screen.tsx +44 -0
  32. package/src/components/SearchArea.tsx +34 -0
  33. package/src/components/SegmentedControl.tsx +64 -0
  34. package/src/components/Skeleton.tsx +42 -0
  35. package/src/components/StatusBadge.tsx +23 -0
  36. package/src/components/SuccessState.tsx +41 -0
  37. package/src/components/TextField.tsx +47 -0
  38. package/src/components/ToggleRow.tsx +35 -0
  39. package/src/components/record-detail/RecordDetail.tsx +60 -0
  40. package/src/components/record-detail/RecordDetailActions.tsx +88 -0
  41. package/src/components/record-detail/RecordDetailField.tsx +43 -0
  42. package/src/components/record-detail/RecordDetailHero.tsx +46 -0
  43. package/src/components/record-detail/RecordDetailLine.tsx +36 -0
  44. package/src/components/record-detail/RecordDetailSection.tsx +36 -0
  45. package/src/components/record-detail/index.ts +7 -0
  46. package/src/components/record-detail/status.ts +16 -0
  47. package/src/gallery/catalog.ts +31 -0
  48. package/src/glass/AmbientBackground.tsx +29 -0
  49. package/src/glass/GlassSurface.tsx +107 -0
  50. package/src/index.ts +66 -0
  51. package/src/layout/Box.tsx +62 -0
  52. package/src/layout/Divider.tsx +8 -0
  53. package/src/layout/Row.tsx +7 -0
  54. package/src/layout/Stack.tsx +7 -0
  55. package/src/provider/ObiUIProvider.tsx +45 -0
  56. package/src/styles/useObiStyles.ts +288 -0
  57. package/src/theme/effects.ts +219 -0
  58. package/src/theme/index.ts +3 -0
  59. package/src/theme/theme.ts +53 -0
  60. package/src/theme/tokens.ts +133 -0
  61. package/src/typography/ObiText.tsx +52 -0
@@ -0,0 +1,46 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+ import { useObiTheme } from '../../provider/ObiUIProvider';
4
+ import { ObiText } from '../../typography/ObiText';
5
+ import { StatusBadge, type StatusTone } from '../StatusBadge';
6
+
7
+ export function RecordDetailHero({
8
+ kicker,
9
+ title,
10
+ subtitle,
11
+ amount,
12
+ badge,
13
+ }: {
14
+ kicker?: string;
15
+ title: string;
16
+ subtitle?: string;
17
+ amount?: string;
18
+ badge?: { label: string; tone?: StatusTone };
19
+ }) {
20
+ const theme = useObiTheme();
21
+ return (
22
+ <View
23
+ style={{
24
+ alignItems: 'center',
25
+ paddingHorizontal: theme.spacing.xl,
26
+ paddingTop: theme.spacing.xl,
27
+ paddingBottom: theme.spacing.md,
28
+ gap: theme.spacing.sm,
29
+ }}
30
+ >
31
+ {kicker ? (
32
+ <ObiText variant="caption" weight="700" muted style={{ letterSpacing: 1.4, textTransform: 'uppercase' }}>
33
+ {kicker}
34
+ </ObiText>
35
+ ) : null}
36
+ <ObiText variant="title" weight="800" style={{ textAlign: 'center', fontSize: 22, lineHeight: 28 }}>{title}</ObiText>
37
+ {subtitle ? <ObiText muted style={{ textAlign: 'center', fontSize: 14 }}>{subtitle}</ObiText> : null}
38
+ {badge ? <StatusBadge label={badge.label} tone={badge.tone} /> : null}
39
+ {amount ? (
40
+ <ObiText variant="display" weight="800" style={{ marginTop: theme.spacing.xs, letterSpacing: -0.6, textAlign: 'center' }}>
41
+ {amount}
42
+ </ObiText>
43
+ ) : null}
44
+ </View>
45
+ );
46
+ }
@@ -0,0 +1,36 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+ import { useObiTheme } from '../../provider/ObiUIProvider';
4
+ import { ObiText } from '../../typography/ObiText';
5
+
6
+ export function RecordDetailLine({
7
+ title,
8
+ caption,
9
+ amount,
10
+ last,
11
+ }: {
12
+ title: string;
13
+ caption?: string;
14
+ amount?: string;
15
+ last?: boolean;
16
+ }) {
17
+ const theme = useObiTheme();
18
+ return (
19
+ <View
20
+ style={{
21
+ flexDirection: 'row',
22
+ alignItems: 'center',
23
+ gap: theme.spacing.md,
24
+ paddingVertical: theme.spacing.md,
25
+ borderBottomWidth: last ? 0 : 1,
26
+ borderBottomColor: theme.colors.border,
27
+ }}
28
+ >
29
+ <View style={{ flex: 1, gap: 2 }}>
30
+ <ObiText weight="700" numberOfLines={2}>{title}</ObiText>
31
+ {caption ? <ObiText variant="caption" muted>{caption}</ObiText> : null}
32
+ </View>
33
+ {amount ? <ObiText weight="800" style={{ textAlign: 'right' }}>{amount}</ObiText> : null}
34
+ </View>
35
+ );
36
+ }
@@ -0,0 +1,36 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+ import { useObiTheme } from '../../provider/ObiUIProvider';
4
+ import { shadow } from '../../theme/effects';
5
+ import { ObiText } from '../../typography/ObiText';
6
+
7
+ export function RecordDetailSection({ title, children }: { title?: string; children: React.ReactNode }) {
8
+ const theme = useObiTheme();
9
+ return (
10
+ <View style={{ marginHorizontal: theme.spacing.lg, marginBottom: theme.spacing.md, gap: theme.spacing.sm }}>
11
+ {title ? (
12
+ <ObiText
13
+ variant="caption"
14
+ weight="700"
15
+ muted
16
+ style={{ letterSpacing: 0.9, textTransform: 'uppercase', paddingHorizontal: theme.spacing.xs }}
17
+ >
18
+ {title}
19
+ </ObiText>
20
+ ) : null}
21
+ <View
22
+ style={{
23
+ backgroundColor: theme.glass.surfaceStrong,
24
+ borderRadius: theme.radius.lg,
25
+ borderWidth: 1,
26
+ borderColor: theme.glass.border,
27
+ paddingHorizontal: theme.spacing.lg,
28
+ overflow: 'hidden',
29
+ ...shadow.xs,
30
+ }}
31
+ >
32
+ {children}
33
+ </View>
34
+ </View>
35
+ );
36
+ }
@@ -0,0 +1,7 @@
1
+ export { RecordDetail, RecordDetailNote } from './RecordDetail';
2
+ export { RecordDetailActions, type RecordAction } from './RecordDetailActions';
3
+ export { RecordDetailHero } from './RecordDetailHero';
4
+ export { RecordDetailSection } from './RecordDetailSection';
5
+ export { RecordDetailField } from './RecordDetailField';
6
+ export { RecordDetailLine } from './RecordDetailLine';
7
+ export { toneFromStatus, prettyStatus } from './status';
@@ -0,0 +1,16 @@
1
+ import type { StatusTone } from '../StatusBadge';
2
+
3
+ export function toneFromStatus(status: string): StatusTone {
4
+ const value = (status ?? '').toLowerCase();
5
+ if (/pagad|aprob|activ|finaliz|acept|confirm|emitid/.test(value)) return 'success';
6
+ if (/pend|proces|borrad|cola/.test(value)) return 'warning';
7
+ if (/anul|inactiv|rechaz|vencid|error/.test(value)) return 'danger';
8
+ if (/convert/.test(value)) return 'primary';
9
+ return 'neutral';
10
+ }
11
+
12
+ export function prettyStatus(status: string) {
13
+ const trimmed = (status ?? '').trim();
14
+ if (!trimmed) return trimmed;
15
+ return trimmed.charAt(0).toUpperCase() + trimmed.slice(1);
16
+ }
@@ -0,0 +1,31 @@
1
+ export type ObiCatalogEntry = {
2
+ name: string;
3
+ group: 'foundation' | 'forms' | 'lists' | 'metrics' | 'feedback' | 'scanner' | 'navigation';
4
+ description: string;
5
+ exportName: string;
6
+ };
7
+
8
+ /** Public surface catalog for gallery / docs. Keep in sync with package exports. */
9
+ export const OBI_UI_CATALOG: ObiCatalogEntry[] = [
10
+ { name: 'ObiUIProvider', group: 'foundation', description: 'Theme provider (light/dark/system)', exportName: 'ObiUIProvider' },
11
+ { name: 'useObiStyles', group: 'foundation', description: 'Semantic utility styles reactive to theme', exportName: 'useObiStyles' },
12
+ { name: 'Box / Stack / Row', group: 'foundation', description: 'Layout primitives', exportName: 'Box' },
13
+ { name: 'Button', group: 'forms', description: 'Primary actions', exportName: 'Button' },
14
+ { name: 'TextField', group: 'forms', description: 'Text input', exportName: 'TextField' },
15
+ { name: 'FormWorkspaceShell', group: 'forms', description: 'CRUD form chrome with dock actions', exportName: 'FormWorkspaceShell' },
16
+ { name: 'FormRow', group: 'forms', description: 'Multi-field form row', exportName: 'FormRow' },
17
+ { name: 'ChoiceField', group: 'forms', description: 'Selectable options', exportName: 'ChoiceField' },
18
+ { name: 'DateRangeFilter', group: 'forms', description: 'Date range + presets sheet', exportName: 'DateRangeFilter' },
19
+ { name: 'ListWorkspace', group: 'lists', description: 'List screen chrome with query states + optional footer/FAB', exportName: 'ListWorkspace' },
20
+ { name: 'QueryWorkspace', group: 'lists', description: 'Scroll/metrics query chrome with filter + QueryState + footer', exportName: 'QueryWorkspace' },
21
+ { name: 'ListFilterBar', group: 'lists', description: 'Search + filter icon sheet', exportName: 'ListFilterBar' },
22
+ { name: 'ListRow', group: 'lists', description: 'Swipeable list row', exportName: 'ListRow' },
23
+ { name: 'MetricCard / MetricsGrid', group: 'metrics', description: 'KPI tiles in 2×2 grid', exportName: 'MetricsGrid' },
24
+ { name: 'QueryState', group: 'feedback', description: 'Loading / error / empty orchestration', exportName: 'QueryState' },
25
+ { name: 'LoadingState', group: 'feedback', description: 'Centered spinner', exportName: 'LoadingState' },
26
+ { name: 'ErrorState', group: 'feedback', description: 'Error with retry', exportName: 'ErrorState' },
27
+ { name: 'EmptyState', group: 'feedback', description: 'Empty placeholder with CTA', exportName: 'EmptyState' },
28
+ { name: 'ScannerShell', group: 'scanner', description: 'Scanner chrome (camera slot in app)', exportName: 'ScannerShell' },
29
+ { name: 'ScanInputBar', group: 'scanner', description: 'Manual barcode entry bar', exportName: 'ScanInputBar' },
30
+ { name: 'NavigationDrawer', group: 'navigation', description: 'Presentational drawer menu', exportName: 'NavigationDrawer' },
31
+ ];
@@ -0,0 +1,29 @@
1
+ import React from 'react';
2
+ import { StyleSheet, View } from 'react-native';
3
+ import LinearGradient from 'react-native-linear-gradient';
4
+ import { useObiTheme } from '../provider/ObiUIProvider';
5
+
6
+ export function AmbientBackground({ children }: { children?: React.ReactNode }) {
7
+ const theme = useObiTheme();
8
+ return (
9
+ <View style={[styles.root, { backgroundColor: theme.isDark ? '#0D1118' : '#EEF3FA' }]}>
10
+ <LinearGradient
11
+ pointerEvents="none"
12
+ colors={theme.isDark ? ['#0D1118', '#121A26', '#111522'] : ['#EDF3FB', '#F4F3FF', '#EEF8F6']}
13
+ start={{ x: 0.05, y: 0 }}
14
+ end={{ x: 0.95, y: 1 }}
15
+ style={StyleSheet.absoluteFill}
16
+ />
17
+ <View pointerEvents="none" style={[styles.glow, styles.glowPrimary, { backgroundColor: theme.isDark ? 'rgba(123,124,240,0.16)' : 'rgba(91,92,230,0.14)' }]} />
18
+ <View pointerEvents="none" style={[styles.glow, styles.glowMint, { backgroundColor: theme.isDark ? 'rgba(45,212,191,0.12)' : 'rgba(14,165,150,0.11)' }]} />
19
+ {children}
20
+ </View>
21
+ );
22
+ }
23
+
24
+ const styles = StyleSheet.create({
25
+ root: { flex: 1 },
26
+ glow: { position: 'absolute', borderRadius: 999 },
27
+ glowPrimary: { width: 340, height: 340, top: -120, right: -120 },
28
+ glowMint: { width: 300, height: 300, bottom: 80, left: -150 },
29
+ });
@@ -0,0 +1,107 @@
1
+ import React from 'react';
2
+ import { Platform, StyleSheet, View, type StyleProp, type ViewStyle } from 'react-native';
3
+ import { BlurView, LiquidGlassView } from '@sbaiahmed1/react-native-blur';
4
+ import { useObiTheme } from '../provider/ObiUIProvider';
5
+ import { liquidGlassPresets } from '../theme/effects';
6
+
7
+ type Variant = keyof typeof liquidGlassPresets;
8
+
9
+ type Props = {
10
+ variant?: Variant;
11
+ interactive?: boolean;
12
+ style?: StyleProp<ViewStyle>;
13
+ children?: React.ReactNode;
14
+ };
15
+
16
+ function AndroidFrost({ variant, style, children }: Required<Pick<Props, 'variant'>> & Pick<Props, 'style' | 'children'>) {
17
+ const theme = useObiTheme();
18
+ const preset = liquidGlassPresets[variant];
19
+ const overlay = theme.isDark
20
+ ? 'rgba(22,27,38,0.30)'
21
+ : variant === 'dock' || variant === 'chrome'
22
+ ? 'rgba(255,255,255,0.14)'
23
+ : variant === 'sheet' || variant === 'modal'
24
+ ? 'rgba(255,255,255,0.26)'
25
+ : 'rgba(255,255,255,0.18)';
26
+
27
+ return (
28
+ <BlurView
29
+ blurType={theme.isDark ? 'dark' : 'xlight'}
30
+ blurAmount={Math.min(preset.blurAmount, 24)}
31
+ blurRounds={3}
32
+ overlayColor={overlay}
33
+ reducedTransparencyFallbackColor={theme.isDark ? 'rgba(25,30,42,0.78)' : 'rgba(255,255,255,0.72)'}
34
+ style={[styles.clip, { borderColor: theme.glass.borderSoft }, style]}
35
+ >
36
+ {children}
37
+ </BlurView>
38
+ );
39
+ }
40
+
41
+ export function GlassSurface({ variant = 'surface', interactive = false, style, children }: Props) {
42
+ const theme = useObiTheme();
43
+ const preset = liquidGlassPresets[variant];
44
+
45
+ if (Platform.OS === 'android') {
46
+ return <AndroidFrost variant={variant} style={style}>{children}</AndroidFrost>;
47
+ }
48
+
49
+ return (
50
+ <LiquidGlassView
51
+ glassType={preset.glassType}
52
+ glassTintColor={theme.isDark ? '#171C27' : '#FFFFFF'}
53
+ glassOpacity={preset.glassOpacity}
54
+ isInteractive={interactive}
55
+ reducedTransparencyFallbackColor={theme.isDark ? 'rgba(25,30,42,0.72)' : 'rgba(255,255,255,0.72)'}
56
+ style={[styles.clip, { borderColor: theme.glass.borderSoft }, style]}
57
+ >
58
+ {children}
59
+ </LiquidGlassView>
60
+ );
61
+ }
62
+
63
+ export function FrostedSurface({ variant = 'chrome', style, children }: Props) {
64
+ const theme = useObiTheme();
65
+ if (Platform.OS === 'android') {
66
+ return <AndroidFrost variant={variant} style={[{ borderColor: 'transparent' }, style]}>{children}</AndroidFrost>;
67
+ }
68
+ const preset = liquidGlassPresets[variant];
69
+ return (
70
+ <BlurView
71
+ blurType={theme.isDark ? 'systemMaterialDark' : 'systemMaterialLight'}
72
+ blurAmount={preset.blurAmount}
73
+ overlayColor={theme.isDark ? 'rgba(20,25,36,0.12)' : 'rgba(255,255,255,0.16)'}
74
+ reducedTransparencyFallbackColor={theme.isDark ? 'rgba(25,30,42,0.72)' : 'rgba(255,255,255,0.72)'}
75
+ style={[styles.clip, { borderColor: 'transparent' }, style]}
76
+ >
77
+ {children}
78
+ </BlurView>
79
+ );
80
+ }
81
+
82
+ export function SoftGlassPanel({ style, children, dense = false }: Props & { dense?: boolean }) {
83
+ const theme = useObiTheme();
84
+ return (
85
+ <View
86
+ style={[
87
+ styles.clip,
88
+ {
89
+ borderColor: theme.glass.borderSoft,
90
+ backgroundColor: theme.isDark
91
+ ? dense ? 'rgba(22,27,38,0.62)' : 'rgba(22,27,38,0.45)'
92
+ : dense ? 'rgba(255,255,255,0.38)' : 'rgba(255,255,255,0.24)',
93
+ },
94
+ style,
95
+ ]}
96
+ >
97
+ {children}
98
+ </View>
99
+ );
100
+ }
101
+
102
+ const styles = StyleSheet.create({
103
+ clip: {
104
+ overflow: 'hidden',
105
+ borderWidth: StyleSheet.hairlineWidth,
106
+ },
107
+ });
package/src/index.ts ADDED
@@ -0,0 +1,66 @@
1
+ export { ObiUIProvider, useObiUI, useObiTheme, type ObiThemePreference } from './provider/ObiUIProvider';
2
+ export { createObiTheme, type ObiTheme, type ObiThemeMode, type ObiSpacingKey, type ObiRadiusKey, type ObiThemeOverrides } from './theme/theme';
3
+ export { lightColors, darkColors, radius, spacing, type, type AppColors } from './theme/tokens';
4
+ export { createGlass, liquidGlassPresets, shadow } from './theme/effects';
5
+
6
+ export { Box, type BoxProps } from './layout/Box';
7
+ export { Stack } from './layout/Stack';
8
+ export { Row } from './layout/Row';
9
+ export { Divider } from './layout/Divider';
10
+
11
+ export { ObiText, AppText, type ObiTextProps } from './typography/ObiText';
12
+ export { Button, type ButtonProps } from './components/Button';
13
+ export { Card } from './components/Card';
14
+ export { TextField } from './components/TextField';
15
+ export { Screen } from './components/Screen';
16
+
17
+ export { GlassSurface, FrostedSurface, SoftGlassPanel } from './glass/GlassSurface';
18
+ export { AmbientBackground } from './glass/AmbientBackground';
19
+
20
+ export { Chip } from './components/Chip';
21
+ export { SegmentedControl, type SegmentedOption } from './components/SegmentedControl';
22
+ export { Skeleton } from './components/Skeleton';
23
+ export { ToggleRow } from './components/ToggleRow';
24
+ export { StatusBadge, type StatusTone } from './components/StatusBadge';
25
+ export { Fab } from './components/Fab';
26
+ export { BottomSheet } from './components/BottomSheet';
27
+ export { ChoiceField, type ChoiceOption } from './components/ChoiceField';
28
+
29
+ export { Icon, type IconName, type IconProps } from './components/Icon';
30
+ export { IconButton } from './components/IconButton';
31
+ export { SearchArea } from './components/SearchArea';
32
+ export { ListFilterBar, type ListFilterBarProps, type FilterOption } from './components/ListFilterBar';
33
+ export { ListRow, type ListRowProps, type SwipeAction, type SwipeActionTone } from './components/ListRow';
34
+ export { FormWorkspaceShell, FormBlock, type FormWorkspaceShellProps, type FormBlockProps } from './components/FormWorkspaceShell';
35
+ export { AppHeader, type AppHeaderProps } from './components/AppHeader';
36
+ export { GlassTabBarBackground, glassTabBarBaseStyle } from './components/GlassTabBar';
37
+ export { MetricCard, type MetricCardProps, type MetricFormat } from './components/MetricCard';
38
+ export { MetricsGrid } from './components/MetricsGrid';
39
+ export { MiniBarChart, type MiniBarChartItem } from './components/MiniBarChart';
40
+ export { EmptyState } from './components/EmptyState';
41
+ export {
42
+ RecordDetail,
43
+ RecordDetailNote,
44
+ RecordDetailActions,
45
+ RecordDetailHero,
46
+ RecordDetailSection,
47
+ RecordDetailField,
48
+ RecordDetailLine,
49
+ toneFromStatus,
50
+ prettyStatus,
51
+ type RecordAction,
52
+ } from './components/record-detail';
53
+ export { NavigationDrawer, type NavigationDrawerProps, type NavigationDrawerEntry, type NavigationDrawerLeaf, type NavigationDrawerGroup, type NavigationDrawerDivider } from './components/NavigationDrawer';
54
+ export { SuccessState } from './components/SuccessState';
55
+ export { LoadingState, type LoadingStateProps } from './components/LoadingState';
56
+ export { ErrorState, type ErrorStateProps } from './components/ErrorState';
57
+ export { QueryState, type QueryStateProps } from './components/QueryState';
58
+ export { ListWorkspace, type ListWorkspaceProps } from './components/ListWorkspace';
59
+ export { QueryWorkspace, type QueryWorkspaceProps } from './components/QueryWorkspace';
60
+ export { FormRow, type FormRowProps } from './components/FormRow';
61
+ export { createObiStyles, useObiStyles, type ObiStyles } from './styles/useObiStyles';
62
+ export { DateRangeFilter, defaultDateRange, type DateRangeValue, type DatePreset, type DateFilterOption, type DateRangeFilterProps } from './components/DateRangeFilter';
63
+ export { CenteredState, type CenteredStateProps } from './components/CenteredState';
64
+ export { ScanInputBar, type ScanInputBarProps } from './components/ScanInputBar';
65
+ export { ScannerShell, type ScannerShellProps } from './components/ScannerShell';
66
+ export { OBI_UI_CATALOG, type ObiCatalogEntry } from './gallery/catalog';
@@ -0,0 +1,62 @@
1
+ import React from 'react';
2
+ import { View, type StyleProp, type ViewProps, type ViewStyle } from 'react-native';
3
+ import { useObiTheme } from '../provider/ObiUIProvider';
4
+ import type { ObiRadiusKey, ObiSpacingKey } from '../theme/theme';
5
+
6
+ type Space = ObiSpacingKey | number;
7
+
8
+ function resolveSpace(value: Space | undefined, values: Record<string, number>) {
9
+ if (value == null) return undefined;
10
+ return typeof value === 'number' ? value : values[value];
11
+ }
12
+
13
+ export type BoxProps = ViewProps & {
14
+ padding?: Space;
15
+ paddingX?: Space;
16
+ paddingY?: Space;
17
+ margin?: Space;
18
+ gap?: Space;
19
+ radius?: ObiRadiusKey | number;
20
+ flex?: number;
21
+ backgroundColor?: string;
22
+ style?: StyleProp<ViewStyle>;
23
+ };
24
+
25
+ export function Box({
26
+ padding,
27
+ paddingX,
28
+ paddingY,
29
+ margin,
30
+ gap,
31
+ radius,
32
+ flex,
33
+ backgroundColor,
34
+ style,
35
+ ...props
36
+ }: BoxProps) {
37
+ const theme = useObiTheme();
38
+ const resolvedRadius = typeof radius === 'number'
39
+ ? radius
40
+ : radius
41
+ ? theme.radius[radius]
42
+ : undefined;
43
+
44
+ return (
45
+ <View
46
+ {...props}
47
+ style={[
48
+ {
49
+ padding: resolveSpace(padding, theme.spacing),
50
+ paddingHorizontal: resolveSpace(paddingX, theme.spacing),
51
+ paddingVertical: resolveSpace(paddingY, theme.spacing),
52
+ margin: resolveSpace(margin, theme.spacing),
53
+ gap: resolveSpace(gap, theme.spacing),
54
+ borderRadius: resolvedRadius,
55
+ flex,
56
+ backgroundColor,
57
+ },
58
+ style,
59
+ ]}
60
+ />
61
+ );
62
+ }
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import { View, type StyleProp, type ViewStyle } from 'react-native';
3
+ import { useObiTheme } from '../provider/ObiUIProvider';
4
+
5
+ export function Divider({ style }: { style?: StyleProp<ViewStyle> }) {
6
+ const { colors } = useObiTheme();
7
+ return <View style={[{ height: 1, backgroundColor: colors.separator }, style]} />;
8
+ }
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ import type { ViewStyle } from 'react-native';
3
+ import { Box, type BoxProps } from './Box';
4
+
5
+ export function Row({ style, ...props }: BoxProps) {
6
+ return <Box {...props} style={[{ flexDirection: 'row', alignItems: 'center' } satisfies ViewStyle, style]} />;
7
+ }
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ import type { ViewStyle } from 'react-native';
3
+ import { Box, type BoxProps } from './Box';
4
+
5
+ export function Stack({ style, ...props }: BoxProps) {
6
+ return <Box {...props} style={[{ flexDirection: 'column' } satisfies ViewStyle, style]} />;
7
+ }
@@ -0,0 +1,45 @@
1
+ import React, { createContext, useContext, useMemo } from 'react';
2
+ import { useColorScheme } from 'react-native';
3
+ import { createObiTheme, type ObiTheme, type ObiThemeMode, type ObiThemeOverrides } from '../theme/theme';
4
+
5
+ export type ObiThemePreference = ObiThemeMode | 'system';
6
+
7
+ type ObiUIContextValue = {
8
+ theme: ObiTheme;
9
+ mode: ObiThemeMode;
10
+ };
11
+
12
+ const ObiUIContext = createContext<ObiUIContextValue | null>(null);
13
+
14
+ export function ObiUIProvider({
15
+ children,
16
+ mode = 'system',
17
+ overrides,
18
+ }: {
19
+ children: React.ReactNode;
20
+ mode?: ObiThemePreference;
21
+ overrides?: ObiThemeOverrides;
22
+ }) {
23
+ const systemMode = useColorScheme();
24
+ const resolvedMode: ObiThemeMode =
25
+ mode === 'system' ? (systemMode === 'dark' ? 'dark' : 'light') : mode;
26
+
27
+ const value = useMemo<ObiUIContextValue>(() => ({
28
+ mode: resolvedMode,
29
+ theme: createObiTheme(resolvedMode, overrides),
30
+ }), [resolvedMode, overrides]);
31
+
32
+ return <ObiUIContext.Provider value={value}>{children}</ObiUIContext.Provider>;
33
+ }
34
+
35
+ export function useObiUI() {
36
+ const value = useContext(ObiUIContext);
37
+ if (!value) {
38
+ throw new Error('useObiUI debe utilizarse dentro de <ObiUIProvider>.');
39
+ }
40
+ return value;
41
+ }
42
+
43
+ export function useObiTheme() {
44
+ return useObiUI().theme;
45
+ }