@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,288 @@
1
+ import { useMemo } from 'react';
2
+ import { StyleSheet } from 'react-native';
3
+ import { useObiTheme } from '../provider/ObiUIProvider';
4
+ import { shadow } from '../theme/effects';
5
+ import type { ObiTheme } from '../theme/theme';
6
+
7
+ /**
8
+ * Semantic utility layer for application screens.
9
+ *
10
+ * This is the React Native equivalent of the layout/utility layer provided by
11
+ * web frameworks such as Bootstrap. Components should be preferred when a
12
+ * semantic component exists, while these styles cover composition, form
13
+ * rhythm and screen-level layout without duplicating StyleSheet objects in
14
+ * every application.
15
+ */
16
+ export function createObiStyles(theme: ObiTheme) {
17
+ const { colors, glass, radius, spacing, type } = theme;
18
+
19
+ return StyleSheet.create({
20
+ flex1: { flex: 1 },
21
+ pressed: { opacity: 0.75 },
22
+
23
+ formScroll: {
24
+ padding: spacing.lg,
25
+ gap: spacing.lg,
26
+ maxWidth: 840,
27
+ width: '100%',
28
+ alignSelf: 'center',
29
+ },
30
+ formScrollCompact: {
31
+ padding: spacing.lg,
32
+ gap: spacing.md,
33
+ maxWidth: 840,
34
+ width: '100%',
35
+ alignSelf: 'center',
36
+ },
37
+ wideScroll: {
38
+ padding: spacing.lg,
39
+ gap: spacing.md,
40
+ maxWidth: 1100,
41
+ width: '100%',
42
+ alignSelf: 'center',
43
+ },
44
+ authScroll: {
45
+ flexGrow: 1,
46
+ padding: spacing.xl,
47
+ justifyContent: 'center',
48
+ width: '100%',
49
+ maxWidth: 540,
50
+ alignSelf: 'center',
51
+ gap: spacing.xl,
52
+ },
53
+
54
+ cardFlush: { padding: 0, overflow: 'hidden' },
55
+ alertError: {
56
+ borderColor: colors.dangerBorder,
57
+ backgroundColor: colors.dangerSoft,
58
+ },
59
+ metricsRow: { flexDirection: 'row', gap: spacing.md },
60
+ gapSm: { gap: spacing.sm },
61
+ gapMd: { gap: spacing.md },
62
+ formSection: { gap: spacing.md },
63
+ formHero: { gap: spacing.md },
64
+ chipWrap: { flexDirection: 'row', flexWrap: 'wrap', gap: spacing.sm },
65
+ formRow: {
66
+ flexDirection: 'row',
67
+ alignItems: 'center',
68
+ justifyContent: 'space-between',
69
+ gap: spacing.md,
70
+ },
71
+ formFields: { flexDirection: 'row', gap: spacing.md },
72
+ formLineSelected: { borderColor: colors.primary, borderWidth: 1 },
73
+ headingRow: { flexDirection: 'row', alignItems: 'center', gap: spacing.md },
74
+ paginationRow: {
75
+ flexDirection: 'row',
76
+ alignItems: 'center',
77
+ justifyContent: 'center',
78
+ gap: spacing.md,
79
+ },
80
+ toggleRow: {
81
+ flexDirection: 'row',
82
+ alignItems: 'center',
83
+ justifyContent: 'space-between',
84
+ gap: spacing.lg,
85
+ },
86
+ toggleCopy: { flex: 1, gap: spacing.xs },
87
+
88
+ paymentRow: {
89
+ flexDirection: 'row',
90
+ alignItems: 'center',
91
+ gap: spacing.md,
92
+ minHeight: 48,
93
+ paddingHorizontal: spacing.md,
94
+ paddingVertical: spacing.sm,
95
+ borderRadius: radius.md,
96
+ backgroundColor: glass.surfaceStrong,
97
+ borderWidth: StyleSheet.hairlineWidth,
98
+ borderColor: glass.border,
99
+ ...shadow.glassSm,
100
+ },
101
+ paymentLabel: { flex: 1 },
102
+ amountField: {
103
+ width: '100%',
104
+ minHeight: 40,
105
+ margin: 0,
106
+ paddingHorizontal: spacing.sm,
107
+ textAlign: 'right',
108
+ fontWeight: '700',
109
+ color: colors.text,
110
+ backgroundColor: glass.input,
111
+ borderWidth: StyleSheet.hairlineWidth,
112
+ borderColor: glass.borderSoft,
113
+ borderRadius: radius.sm,
114
+ },
115
+ input: {
116
+ minHeight: 48,
117
+ borderWidth: StyleSheet.hairlineWidth,
118
+ borderColor: glass.border,
119
+ borderRadius: radius.md,
120
+ backgroundColor: glass.input,
121
+ paddingHorizontal: spacing.md,
122
+ color: colors.text,
123
+ fontSize: type.body,
124
+ ...shadow.glassSm,
125
+ },
126
+ sectionLabel: {
127
+ fontSize: 11,
128
+ fontWeight: '700',
129
+ color: colors.textMuted,
130
+ letterSpacing: 0.8,
131
+ textTransform: 'uppercase',
132
+ },
133
+
134
+ listItemPressed: { backgroundColor: colors.surfacePressed },
135
+ emptyState: {
136
+ flex: 1,
137
+ alignItems: 'center',
138
+ justifyContent: 'center',
139
+ padding: spacing.xxl,
140
+ gap: spacing.md,
141
+ },
142
+ filterRow: {
143
+ flexDirection: 'row',
144
+ flexWrap: 'wrap',
145
+ gap: spacing.sm,
146
+ },
147
+ headerActions: { flexDirection: 'row', gap: spacing.md, alignItems: 'center' },
148
+ headerLink: { color: colors.primary, fontWeight: '700', fontSize: 13 },
149
+ headerLinkDisabled: { opacity: 0.45 },
150
+
151
+ filterWrap: { gap: spacing.sm },
152
+ filterRowInner: {
153
+ flexDirection: 'row',
154
+ gap: spacing.sm,
155
+ alignItems: 'center',
156
+ },
157
+ filterSearch: { flex: 1 },
158
+ filterBtn: {
159
+ width: 48,
160
+ height: 48,
161
+ borderRadius: radius.md,
162
+ borderWidth: StyleSheet.hairlineWidth,
163
+ borderColor: glass.border,
164
+ backgroundColor: glass.control,
165
+ alignItems: 'center',
166
+ justifyContent: 'center',
167
+ ...shadow.glassSm,
168
+ },
169
+ filterBtnActive: {
170
+ backgroundColor: glass.primaryTintStrong,
171
+ borderColor: colors.primary,
172
+ },
173
+ filterIcon: { gap: 3, alignItems: 'flex-start' },
174
+ filterBarMark: {
175
+ height: 2,
176
+ borderRadius: 1,
177
+ backgroundColor: colors.textMuted,
178
+ },
179
+ filterBarMarkActive: { backgroundColor: colors.primary },
180
+ dateRangeField: { justifyContent: 'center', paddingVertical: 0 },
181
+ dateRangeFieldText: {
182
+ color: colors.text,
183
+ fontSize: type.body,
184
+ fontWeight: '600',
185
+ },
186
+ dateRangeSheetSection: { marginTop: spacing.md, marginBottom: spacing.xs },
187
+ dateRangeSheetCaption: {
188
+ fontSize: type.caption,
189
+ color: colors.textMuted,
190
+ fontWeight: '700',
191
+ marginBottom: 2,
192
+ textTransform: 'uppercase',
193
+ letterSpacing: 0.6,
194
+ },
195
+ filterSheetContent: {
196
+ gap: spacing.xs,
197
+ paddingTop: spacing.sm,
198
+ paddingBottom: spacing.lg,
199
+ flexGrow: 1,
200
+ },
201
+ sheetOption: {
202
+ flexDirection: 'row',
203
+ alignItems: 'center',
204
+ justifyContent: 'space-between',
205
+ paddingVertical: spacing.lg,
206
+ minHeight: 56,
207
+ borderBottomWidth: StyleSheet.hairlineWidth,
208
+ borderBottomColor: glass.separator,
209
+ },
210
+ sheetOptionLabel: { fontSize: type.body, color: colors.text, fontWeight: '600' },
211
+ sheetOptionLabelSelected: { color: colors.primary, fontWeight: '800' },
212
+
213
+ loginBrand: { gap: spacing.sm },
214
+ loginLogo: {
215
+ width: 68,
216
+ height: 68,
217
+ borderRadius: 18,
218
+ backgroundColor: colors.primary,
219
+ alignItems: 'center',
220
+ justifyContent: 'center',
221
+ ...shadow.floating,
222
+ },
223
+ detailCenter: {
224
+ alignItems: 'center',
225
+ justifyContent: 'center',
226
+ gap: spacing.lg,
227
+ padding: spacing.xl,
228
+ },
229
+ scanInputWrap: { flex: 1, gap: spacing.xs },
230
+ scanInputRow: {
231
+ flexDirection: 'row',
232
+ alignItems: 'center',
233
+ gap: spacing.sm,
234
+ flex: 1,
235
+ },
236
+ printerDevice: {
237
+ flexDirection: 'row',
238
+ alignItems: 'center',
239
+ justifyContent: 'space-between',
240
+ gap: spacing.md,
241
+ paddingVertical: spacing.sm,
242
+ },
243
+ tenantCard: {
244
+ flexDirection: 'row',
245
+ alignItems: 'center',
246
+ gap: spacing.md,
247
+ },
248
+
249
+ scannerRoot: { flex: 1, backgroundColor: '#000' },
250
+ scannerPermission: {
251
+ flex: 1,
252
+ alignItems: 'center',
253
+ justifyContent: 'center',
254
+ gap: spacing.lg,
255
+ padding: spacing.xl,
256
+ backgroundColor: colors.background,
257
+ },
258
+ scannerText: { textAlign: 'center', maxWidth: 420 },
259
+ scannerScrim: {
260
+ paddingTop: spacing.xl,
261
+ paddingHorizontal: spacing.lg,
262
+ paddingBottom: spacing.xl,
263
+ backgroundColor: 'rgba(0,0,0,0.38)',
264
+ gap: spacing.sm,
265
+ },
266
+ scannerWhite: { color: colors.onPrimary, textAlign: 'center' },
267
+ scannerSheet: {
268
+ position: 'absolute',
269
+ left: spacing.md,
270
+ right: spacing.md,
271
+ bottom: spacing.md,
272
+ backgroundColor: glass.chromeStrong,
273
+ borderRadius: radius.xl,
274
+ padding: spacing.lg,
275
+ gap: spacing.md,
276
+ borderWidth: StyleSheet.hairlineWidth,
277
+ borderColor: glass.border,
278
+ ...shadow.lg,
279
+ },
280
+ });
281
+ }
282
+
283
+ export type ObiStyles = ReturnType<typeof createObiStyles>;
284
+
285
+ export function useObiStyles(): ObiStyles {
286
+ const theme = useObiTheme();
287
+ return useMemo(() => createObiStyles(theme), [theme]);
288
+ }
@@ -0,0 +1,219 @@
1
+ import type { AppColors } from "./tokens";
2
+ import { Platform, type ViewStyle } from "react-native";
3
+
4
+ /**
5
+ * Liquid Glass material tokens.
6
+ *
7
+ * These values intentionally use lower alpha than a traditional
8
+ * glassmorphism theme. Native blur/refraction should provide most of the
9
+ * perceived surface; tint, border and elevation only define its edge.
10
+ */
11
+ function isDarkPalette(colors: AppColors) {
12
+ const hex = colors.background.replace("#", "");
13
+ if (hex.length !== 6) return false;
14
+
15
+ const r = parseInt(hex.slice(0, 2), 16) / 255;
16
+ const g = parseInt(hex.slice(2, 4), 16) / 255;
17
+ const b = parseInt(hex.slice(4, 6), 16) / 255;
18
+
19
+ const linearize = (value: number) =>
20
+ value <= 0.03928 ? value / 12.92 : Math.pow((value + 0.055) / 1.055, 2.4);
21
+
22
+ const luminance =
23
+ 0.2126 * linearize(r) +
24
+ 0.7152 * linearize(g) +
25
+ 0.0722 * linearize(b);
26
+
27
+ return luminance < 0.22;
28
+ }
29
+
30
+ export function createGlass(colors: AppColors) {
31
+ if (isDarkPalette(colors)) {
32
+ return {
33
+ surface: "rgba(25,30,42,0.30)",
34
+ surfaceStrong: "rgba(25,30,42,0.40)",
35
+ surfaceDense: "rgba(25,30,42,0.58)",
36
+ surfaceSoft: "rgba(39,45,61,0.20)",
37
+
38
+ chrome: "rgba(18,22,32,0.18)",
39
+ chromeStrong: "rgba(18,22,32,0.30)",
40
+ control: "rgba(49,56,74,0.22)",
41
+ controlPressed: "rgba(71,79,102,0.34)",
42
+ input: "rgba(28,34,47,0.34)",
43
+
44
+ border: "rgba(255,255,255,0.15)",
45
+ borderSoft: "rgba(255,255,255,0.09)",
46
+ innerStroke: "rgba(255,255,255,0.10)",
47
+ highlight: "rgba(255,255,255,0.16)",
48
+ highlightSoft: "rgba(255,255,255,0.075)",
49
+ separator: "rgba(255,255,255,0.075)",
50
+
51
+ backdrop: "rgba(3,7,18,0.24)",
52
+ backdropStrong: "rgba(3,7,18,0.40)",
53
+
54
+ /** Paneles laterales (sidebar / checkout) — casi opacos, sin BlurView. */
55
+ panel: "rgba(18,22,32,0.96)",
56
+
57
+ primaryTint: "rgba(123,124,240,0.14)",
58
+ primaryTintStrong: "rgba(123,124,240,0.24)",
59
+ dangerTint: "rgba(249,112,102,0.12)",
60
+ successTint: "rgba(60,203,127,0.11)",
61
+ darkSurface: "rgba(6,9,15,0.66)",
62
+ darkBorder: "rgba(255,255,255,0.14)",
63
+ } as const;
64
+ }
65
+
66
+ return {
67
+ surface: "rgba(255,255,255,0.28)",
68
+ surfaceStrong: "rgba(255,255,255,0.38)",
69
+ surfaceDense: "rgba(255,255,255,0.52)",
70
+ surfaceSoft: "rgba(255,255,255,0.16)",
71
+
72
+ chrome: "rgba(255,255,255,0.22)",
73
+ chromeStrong: "rgba(255,255,255,0.36)",
74
+ control: "rgba(255,255,255,0.28)",
75
+ controlPressed: "rgba(255,255,255,0.42)",
76
+ input: "rgba(255,255,255,0.40)",
77
+
78
+ border: "rgba(255,255,255,0.70)",
79
+ borderSoft: "rgba(255,255,255,0.42)",
80
+ innerStroke: "rgba(255,255,255,0.55)",
81
+ highlight: "rgba(255,255,255,0.85)",
82
+ highlightSoft: "rgba(255,255,255,0.45)",
83
+ separator: "rgba(15,23,42,0.07)",
84
+
85
+ backdrop: "rgba(15,23,42,0.28)",
86
+ backdropStrong: "rgba(15,23,42,0.42)",
87
+
88
+ /** Paneles laterales (sidebar / checkout) — casi opacos, sin BlurView. */
89
+ panel: "rgba(248,250,255,0.96)",
90
+
91
+ primaryTint: "rgba(91,92,230,0.10)",
92
+ primaryTintStrong: "rgba(91,92,230,0.18)",
93
+ dangerTint: "rgba(217,45,32,0.09)",
94
+ successTint: "rgba(22,132,91,0.09)",
95
+ darkSurface: "rgba(17,24,39,0.64)",
96
+ darkBorder: "rgba(255,255,255,0.14)",
97
+ } as const;
98
+ }
99
+
100
+ export const liquidGlassPresets = {
101
+ chrome: {
102
+ glassType: "clear" as const,
103
+ glassOpacity: 0.10,
104
+ blurAmount: 30,
105
+ },
106
+ dock: {
107
+ glassType: "clear" as const,
108
+ glassOpacity: 0.12,
109
+ blurAmount: 34,
110
+ },
111
+ control: {
112
+ glassType: "clear" as const,
113
+ glassOpacity: 0.08,
114
+ blurAmount: 22,
115
+ },
116
+ surface: {
117
+ glassType: "regular" as const,
118
+ glassOpacity: 0.14,
119
+ blurAmount: 30,
120
+ },
121
+ sheet: {
122
+ glassType: "regular" as const,
123
+ glassOpacity: 0.20,
124
+ blurAmount: 38,
125
+ },
126
+ modal: {
127
+ glassType: "regular" as const,
128
+ glassOpacity: 0.22,
129
+ blurAmount: 40,
130
+ },
131
+ scanner: {
132
+ glassType: "clear" as const,
133
+ glassOpacity: 0.14,
134
+ blurAmount: 34,
135
+ },
136
+ } as const;
137
+
138
+ /**
139
+ * Android `elevation` pinta una silueta opaca detrás de views translúcidos
140
+ * y rompe el glass. Usamos boxShadow (RN moderno) en ambas plataformas;
141
+ * en iOS también shadow* por compatibilidad visual.
142
+ */
143
+ function glassDepth(
144
+ ios: Pick<ViewStyle, "shadowColor" | "shadowOffset" | "shadowOpacity" | "shadowRadius">,
145
+ box: string,
146
+ ): ViewStyle {
147
+ return Platform.select<ViewStyle>({
148
+ ios: { ...ios },
149
+ android: {
150
+ elevation: 0,
151
+ // @ts-expect-error boxShadow is supported in RN 0.76+
152
+ boxShadow: box,
153
+ },
154
+ default: { elevation: 0 },
155
+ })!;
156
+ }
157
+
158
+ export const shadow = {
159
+ xs: {
160
+ shadowColor: "#0F172A",
161
+ shadowOffset: { width: 0, height: 1 },
162
+ shadowOpacity: 0.025,
163
+ shadowRadius: 2,
164
+ elevation: 1,
165
+ },
166
+ sm: {
167
+ shadowColor: "#0F172A",
168
+ shadowOffset: { width: 0, height: 3 },
169
+ shadowOpacity: 0.05,
170
+ shadowRadius: 8,
171
+ elevation: 2,
172
+ },
173
+ md: {
174
+ shadowColor: "#0F172A",
175
+ shadowOffset: { width: 0, height: 7 },
176
+ shadowOpacity: 0.075,
177
+ shadowRadius: 18,
178
+ elevation: 4,
179
+ },
180
+ lg: {
181
+ shadowColor: "#0F172A",
182
+ shadowOffset: { width: 0, height: 16 },
183
+ shadowOpacity: 0.115,
184
+ shadowRadius: 34,
185
+ elevation: 8,
186
+ },
187
+ floating: {
188
+ shadowColor: "#25245F",
189
+ shadowOffset: { width: 0, height: 10 },
190
+ shadowOpacity: 0.16,
191
+ shadowRadius: 24,
192
+ elevation: 9,
193
+ },
194
+ top: {
195
+ shadowColor: "#0F172A",
196
+ shadowOffset: { width: 0, height: -6 },
197
+ shadowOpacity: 0.06,
198
+ shadowRadius: 18,
199
+ elevation: 7,
200
+ },
201
+ glass: glassDepth(
202
+ {
203
+ shadowColor: "#0F172A",
204
+ shadowOffset: { width: 0, height: 8 },
205
+ shadowOpacity: 0.12,
206
+ shadowRadius: 18,
207
+ },
208
+ "0px 6px 18px rgba(15, 23, 42, 0.12)",
209
+ ),
210
+ glassSm: glassDepth(
211
+ {
212
+ shadowColor: "#0F172A",
213
+ shadowOffset: { width: 0, height: 4 },
214
+ shadowOpacity: 0.08,
215
+ shadowRadius: 12,
216
+ },
217
+ "0px 3px 12px rgba(15, 23, 42, 0.09)",
218
+ ),
219
+ } as const;
@@ -0,0 +1,3 @@
1
+ export * from './tokens';
2
+ export * from './effects';
3
+ export * from './theme';
@@ -0,0 +1,53 @@
1
+ import { createGlass } from './effects';
2
+ import {
3
+ darkColors,
4
+ lightColors,
5
+ radius,
6
+ spacing,
7
+ type,
8
+ type AppColors,
9
+ } from './tokens';
10
+
11
+ export type ObiThemeMode = 'light' | 'dark';
12
+ export type ObiSpacingKey = keyof typeof spacing;
13
+ export type ObiRadiusKey = keyof typeof radius;
14
+
15
+ export type ObiTheme = {
16
+ mode: ObiThemeMode;
17
+ isDark: boolean;
18
+ colors: AppColors;
19
+ spacing: typeof spacing;
20
+ radius: typeof radius;
21
+ type: typeof type;
22
+ glass: ReturnType<typeof createGlass>;
23
+ navigation: {
24
+ bottomBarHeight: number;
25
+ headerMinHeight: number;
26
+ };
27
+ };
28
+
29
+ export type ObiThemeOverrides = {
30
+ colors?: Partial<AppColors>;
31
+ navigation?: Partial<ObiTheme['navigation']>;
32
+ };
33
+
34
+ export function createObiTheme(mode: ObiThemeMode, overrides?: ObiThemeOverrides): ObiTheme {
35
+ const baseColors = mode === 'dark' ? darkColors : lightColors;
36
+ const colors: AppColors = { ...baseColors, ...(overrides?.colors ?? {}) };
37
+ const navigation = {
38
+ bottomBarHeight: 56,
39
+ headerMinHeight: 56,
40
+ ...(overrides?.navigation ?? {}),
41
+ };
42
+
43
+ return {
44
+ mode,
45
+ isDark: mode === 'dark',
46
+ colors,
47
+ spacing,
48
+ radius,
49
+ type,
50
+ glass: createGlass(colors),
51
+ navigation,
52
+ };
53
+ }
@@ -0,0 +1,133 @@
1
+ export type AppColors = {
2
+ primary: string;
3
+ primaryDark: string;
4
+ primarySoft: string;
5
+ primaryFaint: string;
6
+ secondary: string;
7
+ secondarySoft: string;
8
+
9
+ background: string;
10
+ backgroundElevated: string;
11
+ surface: string;
12
+ surfaceMuted: string;
13
+ surfacePressed: string;
14
+
15
+ text: string;
16
+ textStrong: string;
17
+ textMuted: string;
18
+ textSubtle: string;
19
+
20
+ border: string;
21
+ borderStrong: string;
22
+ separator: string;
23
+
24
+ danger: string;
25
+ success: string;
26
+ warning: string;
27
+ info: string;
28
+
29
+ onPrimary: string;
30
+ onPrimaryMuted: string;
31
+ onPrimarySoft: string;
32
+
33
+ warningSoft: string;
34
+ warningBorder: string;
35
+ dangerSoft: string;
36
+ dangerBorder: string;
37
+ };
38
+
39
+ export const lightColors: AppColors = {
40
+ primary: "#5B5CE6",
41
+ primaryDark: "#4446C8",
42
+ primarySoft: "#EEEDFF",
43
+ primaryFaint: "#F6F5FF",
44
+ secondary: "#0EA596",
45
+ secondarySoft: "#EAFBF7",
46
+
47
+ background: "#EEF3FA",
48
+ backgroundElevated: "#F8F9FC",
49
+ surface: "#FFFFFF",
50
+ surfaceMuted: "#F6F7FA",
51
+ surfacePressed: "#EEF0F5",
52
+
53
+ text: "#111827",
54
+ textStrong: "#0B1220",
55
+ textMuted: "#667085",
56
+ textSubtle: "#98A2B3",
57
+
58
+ border: "#E3E7EE",
59
+ borderStrong: "#D5DAE3",
60
+ separator: "rgba(15, 23, 42, 0.075)",
61
+
62
+ danger: "#D92D20",
63
+ success: "#16845B",
64
+ warning: "#B54708",
65
+ info: "#2563EB",
66
+
67
+ onPrimary: "#FFFFFF",
68
+ onPrimaryMuted: "rgba(255,255,255,0.78)",
69
+ onPrimarySoft: "rgba(255,255,255,0.16)",
70
+
71
+ warningSoft: "#FFFAEB",
72
+ warningBorder: "#FEC84B",
73
+ dangerSoft: "#FEF3F2",
74
+ dangerBorder: "#FDA29B",
75
+ };
76
+
77
+ export const darkColors: AppColors = {
78
+ primary: "#7B7CF0",
79
+ primaryDark: "#6263DC",
80
+ primarySoft: "#29294D",
81
+ primaryFaint: "#1B1B2E",
82
+ secondary: "#2DD4BF",
83
+ secondarySoft: "#123733",
84
+
85
+ background: "#0F1219",
86
+ backgroundElevated: "#131823",
87
+ surface: "#1A1F2B",
88
+ surfaceMuted: "#242A38",
89
+ surfacePressed: "#2B3241",
90
+
91
+ text: "#F3F5F9",
92
+ textStrong: "#FFFFFF",
93
+ textMuted: "#9AA3B5",
94
+ textSubtle: "#727D91",
95
+
96
+ border: "#2E3545",
97
+ borderStrong: "#3C4558",
98
+ separator: "rgba(255,255,255,0.075)",
99
+
100
+ danger: "#F97066",
101
+ success: "#3CCB7F",
102
+ warning: "#FDB022",
103
+ info: "#60A5FA",
104
+
105
+ onPrimary: "#FFFFFF",
106
+ onPrimaryMuted: "rgba(255,255,255,0.78)",
107
+ onPrimarySoft: "rgba(255,255,255,0.16)",
108
+
109
+ warningSoft: "#2A2110",
110
+ warningBorder: "#854D0E",
111
+ dangerSoft: "#2A1215",
112
+ dangerBorder: "#912018",
113
+ };
114
+
115
+ export const spacing = {
116
+ xs: 4,
117
+ sm: 8,
118
+ md: 12,
119
+ lg: 16,
120
+ xl: 24,
121
+ xxl: 32,
122
+ } as const;
123
+
124
+ // Rounded, but restrained so the POS keeps a professional, non-generic look.
125
+ export const radius = { sm: 8, md: 12, lg: 16, xl: 22, pill: 999 } as const;
126
+
127
+ export const type = {
128
+ caption: 12,
129
+ body: 15,
130
+ subtitle: 18,
131
+ title: 24,
132
+ display: 32,
133
+ } as const;