@timmbr/native-ui 1.0.0 → 1.0.1
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 +3 -3
- package/src/native/index.tsx +324 -104
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timmbr/native-ui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Unbundled Gluestack UI v2 primitives with shared Timmbr design tokens for React Native and Web",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"class-variance-authority": "^0.7.1",
|
|
36
36
|
"@timmbr/icons": "1.7.0",
|
|
37
|
-
"@timmbr/
|
|
38
|
-
"@timmbr/
|
|
37
|
+
"@timmbr/utils": "1.6.0",
|
|
38
|
+
"@timmbr/theme": "1.3.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"react": ">=18.0.0",
|
package/src/native/index.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { createContext, useContext, useState, useMemo } from 'react';
|
|
1
|
+
import React, { createContext, useCallback, useContext, useState, useMemo } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
4
|
Text as RNText,
|
|
@@ -65,16 +65,16 @@ export const TimmbrNativeProvider: React.FC<TimmbrNativeProviderProps> = ({
|
|
|
65
65
|
return currentMode;
|
|
66
66
|
}, [currentMode, systemScheme]);
|
|
67
67
|
|
|
68
|
-
const setMode = (newMode: ThemeMode) => {
|
|
68
|
+
const setMode = useCallback((newMode: ThemeMode) => {
|
|
69
69
|
setUncontrolledMode(newMode);
|
|
70
|
-
};
|
|
70
|
+
}, []);
|
|
71
71
|
|
|
72
|
-
const toggleTheme = () => {
|
|
72
|
+
const toggleTheme = useCallback(() => {
|
|
73
73
|
setUncontrolledMode((prev) => {
|
|
74
74
|
const active = prev === 'system' ? (systemScheme === 'dark' ? 'dark' : 'light') : prev;
|
|
75
75
|
return active === 'dark' ? 'light' : 'dark';
|
|
76
76
|
});
|
|
77
|
-
};
|
|
77
|
+
}, [systemScheme]);
|
|
78
78
|
|
|
79
79
|
const contextValue = useMemo(
|
|
80
80
|
() => ({
|
|
@@ -83,7 +83,7 @@ export const TimmbrNativeProvider: React.FC<TimmbrNativeProviderProps> = ({
|
|
|
83
83
|
setMode,
|
|
84
84
|
toggleTheme,
|
|
85
85
|
}),
|
|
86
|
-
[currentMode, resolvedTheme]
|
|
86
|
+
[currentMode, resolvedTheme, setMode, toggleTheme]
|
|
87
87
|
);
|
|
88
88
|
|
|
89
89
|
return (
|
|
@@ -193,7 +193,7 @@ const headingSizeMap: Record<string, string> = {
|
|
|
193
193
|
};
|
|
194
194
|
|
|
195
195
|
export const Heading: React.FC<HeadingProps> = ({
|
|
196
|
-
size = 'lg'
|
|
196
|
+
size = 'lg' as NonNullable<HeadingProps['size']>,
|
|
197
197
|
className = '',
|
|
198
198
|
children,
|
|
199
199
|
...props
|
|
@@ -208,11 +208,35 @@ export const Heading: React.FC<HeadingProps> = ({
|
|
|
208
208
|
);
|
|
209
209
|
};
|
|
210
210
|
|
|
211
|
+
const NON_COLOR_TEXT_UTILITY =
|
|
212
|
+
/^(?:dark:)?text-(xs|sm|base|lg|xl|2xl|3xl|4xl|left|center|right|justify|auto|\[\d+(px|rem|em)?\])$/;
|
|
213
|
+
|
|
214
|
+
function hasExplicitTextColorClass(className: string): boolean {
|
|
215
|
+
return className
|
|
216
|
+
.split(/\s+/)
|
|
217
|
+
.filter(Boolean)
|
|
218
|
+
.some((token) => /^(?:dark:)?text-/.test(token) && !NON_COLOR_TEXT_UTILITY.test(token));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export type ButtonVariant = 'solid' | 'outline' | 'link';
|
|
222
|
+
export type ButtonAction = 'primary' | 'secondary' | 'positive' | 'negative' | 'default';
|
|
223
|
+
export type ButtonSize = 'sm' | 'md' | 'lg';
|
|
224
|
+
|
|
225
|
+
interface ButtonStyleContextValue {
|
|
226
|
+
variant: ButtonVariant;
|
|
227
|
+
action: ButtonAction;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const ButtonStyleContext = createContext<ButtonStyleContextValue>({
|
|
231
|
+
variant: 'solid',
|
|
232
|
+
action: 'primary',
|
|
233
|
+
});
|
|
234
|
+
|
|
211
235
|
export interface ButtonProps extends Omit<PressableProps, 'children'> {
|
|
212
236
|
className?: string;
|
|
213
|
-
variant?:
|
|
214
|
-
action?:
|
|
215
|
-
size?:
|
|
237
|
+
variant?: ButtonVariant;
|
|
238
|
+
action?: ButtonAction;
|
|
239
|
+
size?: ButtonSize;
|
|
216
240
|
isDisabled?: boolean;
|
|
217
241
|
isLoading?: boolean;
|
|
218
242
|
children?: React.ReactNode;
|
|
@@ -242,6 +266,47 @@ const buttonVariantStyles: Record<string, Record<string, string>> = {
|
|
|
242
266
|
},
|
|
243
267
|
};
|
|
244
268
|
|
|
269
|
+
const buttonTextColorStyles: Record<string, Record<string, string>> = {
|
|
270
|
+
solid: {
|
|
271
|
+
primary: 'text-primary-foreground',
|
|
272
|
+
secondary: 'text-secondary-foreground',
|
|
273
|
+
positive: 'text-primary-foreground',
|
|
274
|
+
negative: 'text-primary-foreground',
|
|
275
|
+
default: 'text-secondary-foreground',
|
|
276
|
+
},
|
|
277
|
+
outline: {
|
|
278
|
+
primary: 'text-primary',
|
|
279
|
+
secondary: 'text-foreground',
|
|
280
|
+
positive: 'text-emerald-600 dark:text-emerald-400',
|
|
281
|
+
negative: 'text-destructive',
|
|
282
|
+
default: 'text-foreground',
|
|
283
|
+
},
|
|
284
|
+
link: {
|
|
285
|
+
primary: 'text-primary',
|
|
286
|
+
secondary: 'text-foreground',
|
|
287
|
+
positive: 'text-emerald-600 dark:text-emerald-400',
|
|
288
|
+
negative: 'text-destructive',
|
|
289
|
+
default: 'text-primary',
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
function resolveButtonSpinnerColor(
|
|
294
|
+
variant: ButtonVariant,
|
|
295
|
+
action: ButtonAction,
|
|
296
|
+
resolvedTheme: ResolvedTheme
|
|
297
|
+
): string {
|
|
298
|
+
if (variant === 'solid' && action !== 'secondary' && action !== 'default') {
|
|
299
|
+
return '#FFFFFF';
|
|
300
|
+
}
|
|
301
|
+
if (variant === 'solid') {
|
|
302
|
+
return resolvedTheme === 'dark' ? '#F5EDE0' : '#60321D';
|
|
303
|
+
}
|
|
304
|
+
if (action === 'primary') return '#C0643A';
|
|
305
|
+
if (action === 'positive') return '#059669';
|
|
306
|
+
if (action === 'negative') return resolvedTheme === 'dark' ? '#EF4444' : '#DC2626';
|
|
307
|
+
return resolvedTheme === 'dark' ? '#F7F1E6' : '#0B0B0B';
|
|
308
|
+
}
|
|
309
|
+
|
|
245
310
|
const buttonSizeStyles: Record<string, string> = {
|
|
246
311
|
sm: 'px-3 py-1.5 rounded-md min-h-[36px]',
|
|
247
312
|
md: 'px-4 py-2.5 rounded-lg min-h-[44px]',
|
|
@@ -249,9 +314,9 @@ const buttonSizeStyles: Record<string, string> = {
|
|
|
249
314
|
};
|
|
250
315
|
|
|
251
316
|
export const Button: React.FC<ButtonProps> = ({
|
|
252
|
-
variant = 'solid',
|
|
253
|
-
action = 'primary',
|
|
254
|
-
size = 'md',
|
|
317
|
+
variant = 'solid' as ButtonVariant,
|
|
318
|
+
action = 'primary' as ButtonAction,
|
|
319
|
+
size = 'md' as ButtonSize,
|
|
255
320
|
isDisabled = false,
|
|
256
321
|
isLoading = false,
|
|
257
322
|
disabled,
|
|
@@ -259,20 +324,25 @@ export const Button: React.FC<ButtonProps> = ({
|
|
|
259
324
|
children,
|
|
260
325
|
...props
|
|
261
326
|
}) => {
|
|
327
|
+
const { resolvedTheme } = useTimmbrNative();
|
|
262
328
|
const isActuallyDisabled = isDisabled || isLoading || disabled;
|
|
263
329
|
const variantClass = buttonVariantStyles[variant]?.[action] || buttonVariantStyles.solid.primary;
|
|
264
330
|
const sizeClass = buttonSizeStyles[size] || buttonSizeStyles.md;
|
|
265
331
|
const disabledClass = isActuallyDisabled ? 'opacity-50' : 'active:opacity-85';
|
|
332
|
+
const spinnerColor = resolveButtonSpinnerColor(variant, action, resolvedTheme);
|
|
333
|
+
const styleValue = useMemo(() => ({ variant, action }), [variant, action]);
|
|
266
334
|
|
|
267
335
|
return (
|
|
268
|
-
<
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
336
|
+
<ButtonStyleContext.Provider value={styleValue}>
|
|
337
|
+
<RNPressable
|
|
338
|
+
disabled={isActuallyDisabled}
|
|
339
|
+
className={`flex-row items-center justify-center border ${variantClass} ${sizeClass} ${disabledClass} ${className}`}
|
|
340
|
+
{...props}
|
|
341
|
+
>
|
|
342
|
+
{isLoading && <ActivityIndicator color={spinnerColor} className="mr-2" size="small" />}
|
|
343
|
+
{children}
|
|
344
|
+
</RNPressable>
|
|
345
|
+
</ButtonStyleContext.Provider>
|
|
276
346
|
);
|
|
277
347
|
};
|
|
278
348
|
|
|
@@ -282,33 +352,64 @@ export interface ButtonTextProps extends TextPropsWithClass {
|
|
|
282
352
|
}
|
|
283
353
|
|
|
284
354
|
export const ButtonText: React.FC<ButtonTextProps> = ({
|
|
355
|
+
variant: variantProp,
|
|
356
|
+
action: actionProp,
|
|
285
357
|
className = '',
|
|
286
358
|
children,
|
|
287
359
|
...props
|
|
288
|
-
}) =>
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
360
|
+
}) => {
|
|
361
|
+
const parentStyle = useContext(ButtonStyleContext);
|
|
362
|
+
const variant = variantProp ?? parentStyle.variant;
|
|
363
|
+
const action = actionProp ?? parentStyle.action;
|
|
364
|
+
const defaultColorClass = hasExplicitTextColorClass(className)
|
|
365
|
+
? ''
|
|
366
|
+
: buttonTextColorStyles[variant]?.[action] || 'text-primary-foreground';
|
|
367
|
+
|
|
368
|
+
return (
|
|
369
|
+
<RNText
|
|
370
|
+
className={`text-center font-semibold font-sans text-sm ${defaultColorClass} ${className}`}
|
|
371
|
+
{...props}
|
|
372
|
+
>
|
|
373
|
+
{children}
|
|
374
|
+
</RNText>
|
|
375
|
+
);
|
|
376
|
+
};
|
|
296
377
|
|
|
297
378
|
export interface ButtonSpinnerProps extends ActivityIndicatorProps {
|
|
298
379
|
className?: string;
|
|
299
380
|
}
|
|
300
381
|
|
|
301
382
|
export const ButtonSpinner: React.FC<ButtonSpinnerProps> = ({
|
|
302
|
-
color
|
|
383
|
+
color,
|
|
303
384
|
size = 'small',
|
|
304
385
|
className = 'mr-2',
|
|
305
386
|
...props
|
|
306
|
-
}) =>
|
|
387
|
+
}) => {
|
|
388
|
+
const { resolvedTheme } = useTimmbrNative();
|
|
389
|
+
const parentStyle = useContext(ButtonStyleContext);
|
|
390
|
+
const spinnerColor =
|
|
391
|
+
color ?? resolveButtonSpinnerColor(parentStyle.variant, parentStyle.action, resolvedTheme);
|
|
392
|
+
|
|
393
|
+
return <ActivityIndicator color={spinnerColor} size={size} className={className} {...props} />;
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
export type BadgeVariant = 'solid' | 'outline';
|
|
397
|
+
export type BadgeAction = 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info' | 'muted';
|
|
398
|
+
|
|
399
|
+
interface BadgeStyleContextValue {
|
|
400
|
+
variant: BadgeVariant;
|
|
401
|
+
action: BadgeAction;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const BadgeStyleContext = createContext<BadgeStyleContextValue>({
|
|
405
|
+
variant: 'solid',
|
|
406
|
+
action: 'primary',
|
|
407
|
+
});
|
|
307
408
|
|
|
308
409
|
export interface BadgeProps extends ViewProps {
|
|
309
410
|
className?: string;
|
|
310
|
-
variant?:
|
|
311
|
-
action?:
|
|
411
|
+
variant?: BadgeVariant;
|
|
412
|
+
action?: BadgeAction;
|
|
312
413
|
}
|
|
313
414
|
|
|
314
415
|
const badgeActionStyles: Record<string, Record<string, string>> = {
|
|
@@ -332,21 +433,46 @@ const badgeActionStyles: Record<string, Record<string, string>> = {
|
|
|
332
433
|
},
|
|
333
434
|
};
|
|
334
435
|
|
|
436
|
+
const badgeTextColorStyles: Record<string, Record<string, string>> = {
|
|
437
|
+
solid: {
|
|
438
|
+
primary: 'text-primary-foreground',
|
|
439
|
+
secondary: 'text-secondary-foreground',
|
|
440
|
+
success: 'text-primary-foreground',
|
|
441
|
+
warning: 'text-primary-foreground',
|
|
442
|
+
error: 'text-primary-foreground',
|
|
443
|
+
info: 'text-primary-foreground',
|
|
444
|
+
muted: 'text-foreground',
|
|
445
|
+
},
|
|
446
|
+
outline: {
|
|
447
|
+
primary: 'text-primary',
|
|
448
|
+
secondary: 'text-foreground',
|
|
449
|
+
success: 'text-emerald-600 dark:text-emerald-400',
|
|
450
|
+
warning: 'text-amber-600 dark:text-amber-400',
|
|
451
|
+
error: 'text-destructive',
|
|
452
|
+
info: 'text-blue-600 dark:text-blue-400',
|
|
453
|
+
muted: 'text-muted-foreground',
|
|
454
|
+
},
|
|
455
|
+
};
|
|
456
|
+
|
|
335
457
|
export const Badge: React.FC<BadgeProps> = ({
|
|
336
|
-
variant = 'solid',
|
|
337
|
-
action = 'primary',
|
|
458
|
+
variant = 'solid' as BadgeVariant,
|
|
459
|
+
action = 'primary' as BadgeAction,
|
|
338
460
|
className = '',
|
|
339
461
|
children,
|
|
340
462
|
...props
|
|
341
463
|
}) => {
|
|
342
464
|
const badgeClass = badgeActionStyles[variant]?.[action] || badgeActionStyles.solid.primary;
|
|
465
|
+
const styleValue = useMemo(() => ({ variant, action }), [variant, action]);
|
|
466
|
+
|
|
343
467
|
return (
|
|
344
|
-
<
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
468
|
+
<BadgeStyleContext.Provider value={styleValue}>
|
|
469
|
+
<View
|
|
470
|
+
className={`px-2.5 py-0.5 rounded-full border items-center justify-center flex-row ${badgeClass} ${className}`}
|
|
471
|
+
{...props}
|
|
472
|
+
>
|
|
473
|
+
{children}
|
|
474
|
+
</View>
|
|
475
|
+
</BadgeStyleContext.Provider>
|
|
350
476
|
);
|
|
351
477
|
};
|
|
352
478
|
|
|
@@ -354,14 +480,21 @@ export const BadgeText: React.FC<TextPropsWithClass> = ({
|
|
|
354
480
|
className = '',
|
|
355
481
|
children,
|
|
356
482
|
...props
|
|
357
|
-
}) =>
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
483
|
+
}) => {
|
|
484
|
+
const parentStyle = useContext(BadgeStyleContext);
|
|
485
|
+
const defaultColorClass = hasExplicitTextColorClass(className)
|
|
486
|
+
? ''
|
|
487
|
+
: badgeTextColorStyles[parentStyle.variant]?.[parentStyle.action] || 'text-primary-foreground';
|
|
488
|
+
|
|
489
|
+
return (
|
|
490
|
+
<RNText
|
|
491
|
+
className={`text-xs font-semibold font-sans ${defaultColorClass} ${className}`}
|
|
492
|
+
{...props}
|
|
493
|
+
>
|
|
494
|
+
{children}
|
|
495
|
+
</RNText>
|
|
496
|
+
);
|
|
497
|
+
};
|
|
365
498
|
|
|
366
499
|
export const Card: React.FC<BoxProps> = ({ className = '', children, ...props }) => (
|
|
367
500
|
<View
|
|
@@ -406,15 +539,19 @@ export interface InputFieldProps extends TextInputProps {
|
|
|
406
539
|
|
|
407
540
|
export const InputField: React.FC<InputFieldProps> = ({
|
|
408
541
|
className = '',
|
|
409
|
-
placeholderTextColor
|
|
542
|
+
placeholderTextColor,
|
|
410
543
|
...props
|
|
411
|
-
}) =>
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
544
|
+
}) => {
|
|
545
|
+
const { resolvedTheme } = useTimmbrNative();
|
|
546
|
+
const defaultPlaceholderColor = resolvedTheme === 'dark' ? '#9D9D9D' : '#6D6D6D';
|
|
547
|
+
return (
|
|
548
|
+
<TextInput
|
|
549
|
+
className={`flex-1 text-foreground placeholder:text-muted-foreground font-sans text-base py-1 ${className}`}
|
|
550
|
+
placeholderTextColor={placeholderTextColor ?? defaultPlaceholderColor}
|
|
551
|
+
{...props}
|
|
552
|
+
/>
|
|
553
|
+
);
|
|
554
|
+
};
|
|
418
555
|
|
|
419
556
|
export const Textarea: React.FC<InputProps> = ({
|
|
420
557
|
isInvalid = false,
|
|
@@ -440,17 +577,21 @@ export const Textarea: React.FC<InputProps> = ({
|
|
|
440
577
|
|
|
441
578
|
export const TextareaInput: React.FC<InputFieldProps> = ({
|
|
442
579
|
className = '',
|
|
443
|
-
placeholderTextColor
|
|
580
|
+
placeholderTextColor,
|
|
444
581
|
...props
|
|
445
|
-
}) =>
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
582
|
+
}) => {
|
|
583
|
+
const { resolvedTheme } = useTimmbrNative();
|
|
584
|
+
const defaultPlaceholderColor = resolvedTheme === 'dark' ? '#9D9D9D' : '#6D6D6D';
|
|
585
|
+
return (
|
|
586
|
+
<TextInput
|
|
587
|
+
multiline
|
|
588
|
+
textAlignVertical="top"
|
|
589
|
+
className={`flex-1 text-foreground placeholder:text-muted-foreground font-sans text-base ${className}`}
|
|
590
|
+
placeholderTextColor={placeholderTextColor ?? defaultPlaceholderColor}
|
|
591
|
+
{...props}
|
|
592
|
+
/>
|
|
593
|
+
);
|
|
594
|
+
};
|
|
454
595
|
|
|
455
596
|
interface TabsContextValue {
|
|
456
597
|
activeTab: string;
|
|
@@ -570,18 +711,21 @@ export interface ActionsheetProps {
|
|
|
570
711
|
children: React.ReactNode;
|
|
571
712
|
}
|
|
572
713
|
|
|
573
|
-
export const Actionsheet: React.FC<ActionsheetProps> = ({ isOpen, onClose, children }) =>
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
714
|
+
export const Actionsheet: React.FC<ActionsheetProps> = ({ isOpen, onClose, children }) => {
|
|
715
|
+
const { resolvedTheme } = useTimmbrNative();
|
|
716
|
+
return (
|
|
717
|
+
<RNModal
|
|
718
|
+
visible={isOpen}
|
|
719
|
+
transparent
|
|
720
|
+
animationType="fade"
|
|
721
|
+
onRequestClose={onClose}
|
|
722
|
+
>
|
|
723
|
+
<View className={`flex-1 justify-end ${resolvedTheme === 'dark' ? 'dark' : ''}`}>
|
|
724
|
+
{children}
|
|
725
|
+
</View>
|
|
726
|
+
</RNModal>
|
|
727
|
+
);
|
|
728
|
+
};
|
|
585
729
|
|
|
586
730
|
export const ActionsheetBackdrop: React.FC<PressableProps & { className?: string }> = ({
|
|
587
731
|
onPress,
|
|
@@ -652,39 +796,54 @@ export const Modal: React.FC<CustomModalProps> = ({
|
|
|
652
796
|
isOpen = false,
|
|
653
797
|
visible,
|
|
654
798
|
onClose,
|
|
799
|
+
className = '',
|
|
655
800
|
children,
|
|
656
801
|
...props
|
|
657
|
-
}) =>
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
802
|
+
}) => {
|
|
803
|
+
const { resolvedTheme } = useTimmbrNative();
|
|
804
|
+
return (
|
|
805
|
+
<RNModal
|
|
806
|
+
visible={isOpen || visible}
|
|
807
|
+
transparent
|
|
808
|
+
animationType="fade"
|
|
809
|
+
onRequestClose={onClose}
|
|
810
|
+
{...props}
|
|
811
|
+
>
|
|
812
|
+
<View
|
|
813
|
+
className={`flex-1 items-center justify-center bg-black/60 p-4 ${
|
|
814
|
+
resolvedTheme === 'dark' ? 'dark' : ''
|
|
815
|
+
} ${className}`}
|
|
816
|
+
>
|
|
817
|
+
{children}
|
|
818
|
+
</View>
|
|
819
|
+
</RNModal>
|
|
820
|
+
);
|
|
821
|
+
};
|
|
670
822
|
|
|
671
823
|
export interface CustomSwitchProps extends SwitchProps {
|
|
672
824
|
className?: string;
|
|
673
825
|
}
|
|
674
826
|
|
|
675
827
|
export const Switch: React.FC<CustomSwitchProps> = ({
|
|
676
|
-
trackColor
|
|
828
|
+
trackColor,
|
|
677
829
|
thumbColor = '#FFFFFF',
|
|
678
830
|
className = '',
|
|
679
831
|
...props
|
|
680
|
-
}) =>
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
832
|
+
}) => {
|
|
833
|
+
const { resolvedTheme } = useTimmbrNative();
|
|
834
|
+
const defaultTrackColor = {
|
|
835
|
+
false: resolvedTheme === 'dark' ? '#3C3C3C' : '#DDD5CA',
|
|
836
|
+
true: '#C0643A',
|
|
837
|
+
};
|
|
838
|
+
return (
|
|
839
|
+
<RNSwitch
|
|
840
|
+
trackColor={trackColor ?? defaultTrackColor}
|
|
841
|
+
thumbColor={thumbColor}
|
|
842
|
+
className={className}
|
|
843
|
+
{...props}
|
|
844
|
+
/>
|
|
845
|
+
);
|
|
846
|
+
};
|
|
688
847
|
|
|
689
848
|
export const Spinner: React.FC<ButtonSpinnerProps> = ({
|
|
690
849
|
color = '#C0643A',
|
|
@@ -780,20 +939,81 @@ export const Toast: React.FC<ToastProps> = ({
|
|
|
780
939
|
};
|
|
781
940
|
|
|
782
941
|
export interface IconProps {
|
|
783
|
-
as?: React.
|
|
942
|
+
as?: React.ElementType;
|
|
784
943
|
size?: number | string;
|
|
785
944
|
color?: string;
|
|
786
945
|
className?: string;
|
|
787
946
|
}
|
|
788
947
|
|
|
948
|
+
function getSemanticIconColorMap(resolvedTheme: ResolvedTheme): Record<string, string> {
|
|
949
|
+
const isDark = resolvedTheme === 'dark';
|
|
950
|
+
return {
|
|
951
|
+
'text-foreground': isDark ? '#F7F1E6' : '#0B0B0B',
|
|
952
|
+
'text-card-foreground': isDark ? '#F7F1E6' : '#0B0B0B',
|
|
953
|
+
'text-muted-foreground': isDark ? '#9D9D9D' : '#6D6D6D',
|
|
954
|
+
'text-secondary-foreground': isDark ? '#F5EDE0' : '#60321D',
|
|
955
|
+
'text-accent-foreground': isDark ? '#F5EDE0' : '#60321D',
|
|
956
|
+
'text-primary': '#C0643A',
|
|
957
|
+
'text-primary-foreground': '#FFFFFF',
|
|
958
|
+
'text-white': '#FFFFFF',
|
|
959
|
+
'text-destructive': isDark ? '#EF4444' : '#DC2626',
|
|
960
|
+
'text-destructive-foreground': isDark ? '#F7F1E6' : '#FFFFFF',
|
|
961
|
+
'text-success': '#059669',
|
|
962
|
+
'text-emerald-500': '#10B981',
|
|
963
|
+
'text-emerald-600': '#059669',
|
|
964
|
+
'text-warning': '#D97706',
|
|
965
|
+
'text-amber-400': '#FBBF24',
|
|
966
|
+
'text-amber-500': '#F59E0B',
|
|
967
|
+
'text-amber-600': '#D97706',
|
|
968
|
+
'text-info': '#2563EB',
|
|
969
|
+
'text-blue-500': '#3B82F6',
|
|
970
|
+
'text-blue-600': '#2563EB',
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
function resolveIconColor(
|
|
975
|
+
explicitColor: string | undefined,
|
|
976
|
+
className: string,
|
|
977
|
+
resolvedTheme: ResolvedTheme
|
|
978
|
+
): string {
|
|
979
|
+
if (explicitColor && explicitColor !== 'currentColor') {
|
|
980
|
+
return explicitColor;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
const tokens = className.split(/\s+/).filter(Boolean);
|
|
984
|
+
const colorMap = getSemanticIconColorMap(resolvedTheme);
|
|
985
|
+
|
|
986
|
+
if (resolvedTheme === 'dark') {
|
|
987
|
+
for (const token of tokens) {
|
|
988
|
+
if (token.startsWith('dark:')) {
|
|
989
|
+
const unprefixed = token.slice('dark:'.length);
|
|
990
|
+
const hexMatch = /^text-\[(#[0-9a-fA-F]{3,8})\]$/.exec(unprefixed);
|
|
991
|
+
if (hexMatch?.[1]) return hexMatch[1];
|
|
992
|
+
if (colorMap[unprefixed]) return colorMap[unprefixed];
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
for (const token of tokens) {
|
|
998
|
+
if (token.startsWith('dark:')) continue;
|
|
999
|
+
const hexMatch = /^text-\[(#[0-9a-fA-F]{3,8})\]$/.exec(token);
|
|
1000
|
+
if (hexMatch?.[1]) return hexMatch[1];
|
|
1001
|
+
if (colorMap[token]) return colorMap[token];
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
return resolvedTheme === 'dark' ? '#F7F1E6' : '#0B0B0B';
|
|
1005
|
+
}
|
|
1006
|
+
|
|
789
1007
|
export const Icon: React.FC<IconProps> = ({
|
|
790
1008
|
as: Component,
|
|
791
1009
|
size = 20,
|
|
792
|
-
color
|
|
1010
|
+
color,
|
|
793
1011
|
className = '',
|
|
794
1012
|
}) => {
|
|
1013
|
+
const { resolvedTheme } = useTimmbrNative();
|
|
795
1014
|
if (!Component) return null;
|
|
796
1015
|
const numSize = typeof size === 'number' ? size : parseInt(size, 10) || 20;
|
|
797
|
-
|
|
1016
|
+
const resolvedColor = resolveIconColor(color, className, resolvedTheme);
|
|
1017
|
+
return <Component size={numSize} color={resolvedColor} className={className} />;
|
|
798
1018
|
};
|
|
799
1019
|
|