@bubbasfw/ui 0.1.0 → 0.2.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.
package/dist/tokens.cjs CHANGED
@@ -3,6 +3,7 @@
3
3
  // src/tokens.ts
4
4
  var tokens = {
5
5
  brandVersion: "1.0.0",
6
+ systemVersion: "0.3.0",
6
7
  colors: {
7
8
  blue: "#007CE8",
8
9
  red: "#CC0000",
@@ -19,7 +20,97 @@ var tokens = {
19
20
  },
20
21
  spacing: { 1: 4, 2: 8, 3: 12, 4: 16, 6: 24, 8: 32, 12: 48, 16: 64 },
21
22
  radius: { control: 4, panel: 12 },
22
- touchTarget: { minimum: 44, default: 48 }
23
+ touchTarget: { minimum: 44, default: 48 },
24
+ layout: { maxWidth: 1200, readingWidth: 720, breakpoints: { compact: 640, wide: 960 } },
25
+ motion: { feedback: 140 },
26
+ themes: {
27
+ "light": {
28
+ "canvas": "#F7F8FA",
29
+ "surface": "#FFFFFF",
30
+ "raised": "#FFFFFF",
31
+ "hover": "#F6F7F9",
32
+ "selected": "#ECEEF1",
33
+ "text": "#131B23",
34
+ "heading": "#03256C",
35
+ "muted": "#526075",
36
+ "line": "#E4E6EA",
37
+ "border": "#637086",
38
+ "link": "#03256C",
39
+ "link-hover": "#A80000",
40
+ "focus": "#03256C",
41
+ "focus-inner": "#FFFFFF",
42
+ "accent": "#CC0000",
43
+ "action-text": "#FFFFFF",
44
+ "action-border": "#A80000",
45
+ "disabled-surface": "#F0F2F5",
46
+ "disabled-text": "#6B727E",
47
+ "info": "#03256C",
48
+ "info-surface": "#F1F5FC",
49
+ "info-border": "#03256C",
50
+ "success": "#23633F",
51
+ "success-surface": "#EAF5EF",
52
+ "success-border": "#23633F",
53
+ "warning": "#805500",
54
+ "warning-surface": "#FFF4D6",
55
+ "warning-border": "#805500",
56
+ "error": "#B42318",
57
+ "error-surface": "#FFF0EF",
58
+ "error-border": "#B42318",
59
+ "code-surface": "#171717",
60
+ "code-text": "#FFFFFF",
61
+ "code-muted": "#B4B4B4",
62
+ "selection": "#D9E8FB",
63
+ "selection-text": "#131B23",
64
+ "scrollbar": "#637086",
65
+ "scrollbar-track": "#F7F8FA",
66
+ "shadow-action": "0 3px 8px #CC000024",
67
+ "shadow-panel": "0 2px 12px #131B2308",
68
+ "shadow-dialog": "0 20px 100px #00133345"
69
+ },
70
+ "dark": {
71
+ "canvas": "#171717",
72
+ "surface": "#202020",
73
+ "raised": "#292929",
74
+ "hover": "#303030",
75
+ "selected": "#383838",
76
+ "text": "#FFFFFF",
77
+ "heading": "#FFFFFF",
78
+ "muted": "#B4B4B4",
79
+ "line": "#3D3D3D",
80
+ "border": "#858585",
81
+ "link": "#8DC7FF",
82
+ "link-hover": "#B8DEFF",
83
+ "focus": "#FFFFFF",
84
+ "focus-inner": "#202020",
85
+ "accent": "#FF9F99",
86
+ "action-text": "#FFFFFF",
87
+ "action-border": "#EF817A",
88
+ "disabled-surface": "#242424",
89
+ "disabled-text": "#9A9A9A",
90
+ "info": "#8DC7FF",
91
+ "info-surface": "#1D2D3D",
92
+ "info-border": "#8DC7FF",
93
+ "success": "#8DDBA9",
94
+ "success-surface": "#183323",
95
+ "success-border": "#8DDBA9",
96
+ "warning": "#FED766",
97
+ "warning-surface": "#3A2E12",
98
+ "warning-border": "#FED766",
99
+ "error": "#FFAEA8",
100
+ "error-surface": "#3D2020",
101
+ "error-border": "#FFAEA8",
102
+ "code-surface": "#171717",
103
+ "code-text": "#FFFFFF",
104
+ "code-muted": "#B4B4B4",
105
+ "selection": "#383838",
106
+ "selection-text": "#FFFFFF",
107
+ "scrollbar": "#858585",
108
+ "scrollbar-track": "#171717",
109
+ "shadow-action": "0 3px 8px #00000040",
110
+ "shadow-panel": "0 2px 12px #00000020",
111
+ "shadow-dialog": "0 20px 100px #00000080"
112
+ }
113
+ }
23
114
  };
24
115
 
25
116
  exports.tokens = tokens;
package/dist/tokens.d.cts CHANGED
@@ -1,6 +1,7 @@
1
1
  /** Pinned foundations from Bubba's Brand Studio brand release 1.0.0. */
2
2
  declare const tokens: {
3
3
  readonly brandVersion: "1.0.0";
4
+ readonly systemVersion: "0.3.0";
4
5
  readonly colors: {
5
6
  readonly blue: "#007CE8";
6
7
  readonly red: "#CC0000";
@@ -33,6 +34,105 @@ declare const tokens: {
33
34
  readonly minimum: 44;
34
35
  readonly default: 48;
35
36
  };
37
+ readonly layout: {
38
+ readonly maxWidth: 1200;
39
+ readonly readingWidth: 720;
40
+ readonly breakpoints: {
41
+ readonly compact: 640;
42
+ readonly wide: 960;
43
+ };
44
+ };
45
+ readonly motion: {
46
+ readonly feedback: 140;
47
+ };
48
+ readonly themes: {
49
+ readonly light: {
50
+ readonly canvas: "#F7F8FA";
51
+ readonly surface: "#FFFFFF";
52
+ readonly raised: "#FFFFFF";
53
+ readonly hover: "#F6F7F9";
54
+ readonly selected: "#ECEEF1";
55
+ readonly text: "#131B23";
56
+ readonly heading: "#03256C";
57
+ readonly muted: "#526075";
58
+ readonly line: "#E4E6EA";
59
+ readonly border: "#637086";
60
+ readonly link: "#03256C";
61
+ readonly 'link-hover': "#A80000";
62
+ readonly focus: "#03256C";
63
+ readonly 'focus-inner': "#FFFFFF";
64
+ readonly accent: "#CC0000";
65
+ readonly 'action-text': "#FFFFFF";
66
+ readonly 'action-border': "#A80000";
67
+ readonly 'disabled-surface': "#F0F2F5";
68
+ readonly 'disabled-text': "#6B727E";
69
+ readonly info: "#03256C";
70
+ readonly 'info-surface': "#F1F5FC";
71
+ readonly 'info-border': "#03256C";
72
+ readonly success: "#23633F";
73
+ readonly 'success-surface': "#EAF5EF";
74
+ readonly 'success-border': "#23633F";
75
+ readonly warning: "#805500";
76
+ readonly 'warning-surface': "#FFF4D6";
77
+ readonly 'warning-border': "#805500";
78
+ readonly error: "#B42318";
79
+ readonly 'error-surface': "#FFF0EF";
80
+ readonly 'error-border': "#B42318";
81
+ readonly 'code-surface': "#171717";
82
+ readonly 'code-text': "#FFFFFF";
83
+ readonly 'code-muted': "#B4B4B4";
84
+ readonly selection: "#D9E8FB";
85
+ readonly 'selection-text': "#131B23";
86
+ readonly scrollbar: "#637086";
87
+ readonly 'scrollbar-track': "#F7F8FA";
88
+ readonly 'shadow-action': "0 3px 8px #CC000024";
89
+ readonly 'shadow-panel': "0 2px 12px #131B2308";
90
+ readonly 'shadow-dialog': "0 20px 100px #00133345";
91
+ };
92
+ readonly dark: {
93
+ readonly canvas: "#171717";
94
+ readonly surface: "#202020";
95
+ readonly raised: "#292929";
96
+ readonly hover: "#303030";
97
+ readonly selected: "#383838";
98
+ readonly text: "#FFFFFF";
99
+ readonly heading: "#FFFFFF";
100
+ readonly muted: "#B4B4B4";
101
+ readonly line: "#3D3D3D";
102
+ readonly border: "#858585";
103
+ readonly link: "#8DC7FF";
104
+ readonly 'link-hover': "#B8DEFF";
105
+ readonly focus: "#FFFFFF";
106
+ readonly 'focus-inner': "#202020";
107
+ readonly accent: "#FF9F99";
108
+ readonly 'action-text': "#FFFFFF";
109
+ readonly 'action-border': "#EF817A";
110
+ readonly 'disabled-surface': "#242424";
111
+ readonly 'disabled-text': "#9A9A9A";
112
+ readonly info: "#8DC7FF";
113
+ readonly 'info-surface': "#1D2D3D";
114
+ readonly 'info-border': "#8DC7FF";
115
+ readonly success: "#8DDBA9";
116
+ readonly 'success-surface': "#183323";
117
+ readonly 'success-border': "#8DDBA9";
118
+ readonly warning: "#FED766";
119
+ readonly 'warning-surface': "#3A2E12";
120
+ readonly 'warning-border': "#FED766";
121
+ readonly error: "#FFAEA8";
122
+ readonly 'error-surface': "#3D2020";
123
+ readonly 'error-border': "#FFAEA8";
124
+ readonly 'code-surface': "#171717";
125
+ readonly 'code-text': "#FFFFFF";
126
+ readonly 'code-muted': "#B4B4B4";
127
+ readonly selection: "#383838";
128
+ readonly 'selection-text': "#FFFFFF";
129
+ readonly scrollbar: "#858585";
130
+ readonly 'scrollbar-track': "#171717";
131
+ readonly 'shadow-action': "0 3px 8px #00000040";
132
+ readonly 'shadow-panel': "0 2px 12px #00000020";
133
+ readonly 'shadow-dialog': "0 20px 100px #00000080";
134
+ };
135
+ };
36
136
  };
37
137
  type BubbaTokens = typeof tokens;
38
138
 
package/dist/tokens.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  /** Pinned foundations from Bubba's Brand Studio brand release 1.0.0. */
2
2
  declare const tokens: {
3
3
  readonly brandVersion: "1.0.0";
4
+ readonly systemVersion: "0.3.0";
4
5
  readonly colors: {
5
6
  readonly blue: "#007CE8";
6
7
  readonly red: "#CC0000";
@@ -33,6 +34,105 @@ declare const tokens: {
33
34
  readonly minimum: 44;
34
35
  readonly default: 48;
35
36
  };
37
+ readonly layout: {
38
+ readonly maxWidth: 1200;
39
+ readonly readingWidth: 720;
40
+ readonly breakpoints: {
41
+ readonly compact: 640;
42
+ readonly wide: 960;
43
+ };
44
+ };
45
+ readonly motion: {
46
+ readonly feedback: 140;
47
+ };
48
+ readonly themes: {
49
+ readonly light: {
50
+ readonly canvas: "#F7F8FA";
51
+ readonly surface: "#FFFFFF";
52
+ readonly raised: "#FFFFFF";
53
+ readonly hover: "#F6F7F9";
54
+ readonly selected: "#ECEEF1";
55
+ readonly text: "#131B23";
56
+ readonly heading: "#03256C";
57
+ readonly muted: "#526075";
58
+ readonly line: "#E4E6EA";
59
+ readonly border: "#637086";
60
+ readonly link: "#03256C";
61
+ readonly 'link-hover': "#A80000";
62
+ readonly focus: "#03256C";
63
+ readonly 'focus-inner': "#FFFFFF";
64
+ readonly accent: "#CC0000";
65
+ readonly 'action-text': "#FFFFFF";
66
+ readonly 'action-border': "#A80000";
67
+ readonly 'disabled-surface': "#F0F2F5";
68
+ readonly 'disabled-text': "#6B727E";
69
+ readonly info: "#03256C";
70
+ readonly 'info-surface': "#F1F5FC";
71
+ readonly 'info-border': "#03256C";
72
+ readonly success: "#23633F";
73
+ readonly 'success-surface': "#EAF5EF";
74
+ readonly 'success-border': "#23633F";
75
+ readonly warning: "#805500";
76
+ readonly 'warning-surface': "#FFF4D6";
77
+ readonly 'warning-border': "#805500";
78
+ readonly error: "#B42318";
79
+ readonly 'error-surface': "#FFF0EF";
80
+ readonly 'error-border': "#B42318";
81
+ readonly 'code-surface': "#171717";
82
+ readonly 'code-text': "#FFFFFF";
83
+ readonly 'code-muted': "#B4B4B4";
84
+ readonly selection: "#D9E8FB";
85
+ readonly 'selection-text': "#131B23";
86
+ readonly scrollbar: "#637086";
87
+ readonly 'scrollbar-track': "#F7F8FA";
88
+ readonly 'shadow-action': "0 3px 8px #CC000024";
89
+ readonly 'shadow-panel': "0 2px 12px #131B2308";
90
+ readonly 'shadow-dialog': "0 20px 100px #00133345";
91
+ };
92
+ readonly dark: {
93
+ readonly canvas: "#171717";
94
+ readonly surface: "#202020";
95
+ readonly raised: "#292929";
96
+ readonly hover: "#303030";
97
+ readonly selected: "#383838";
98
+ readonly text: "#FFFFFF";
99
+ readonly heading: "#FFFFFF";
100
+ readonly muted: "#B4B4B4";
101
+ readonly line: "#3D3D3D";
102
+ readonly border: "#858585";
103
+ readonly link: "#8DC7FF";
104
+ readonly 'link-hover': "#B8DEFF";
105
+ readonly focus: "#FFFFFF";
106
+ readonly 'focus-inner': "#202020";
107
+ readonly accent: "#FF9F99";
108
+ readonly 'action-text': "#FFFFFF";
109
+ readonly 'action-border': "#EF817A";
110
+ readonly 'disabled-surface': "#242424";
111
+ readonly 'disabled-text': "#9A9A9A";
112
+ readonly info: "#8DC7FF";
113
+ readonly 'info-surface': "#1D2D3D";
114
+ readonly 'info-border': "#8DC7FF";
115
+ readonly success: "#8DDBA9";
116
+ readonly 'success-surface': "#183323";
117
+ readonly 'success-border': "#8DDBA9";
118
+ readonly warning: "#FED766";
119
+ readonly 'warning-surface': "#3A2E12";
120
+ readonly 'warning-border': "#FED766";
121
+ readonly error: "#FFAEA8";
122
+ readonly 'error-surface': "#3D2020";
123
+ readonly 'error-border': "#FFAEA8";
124
+ readonly 'code-surface': "#171717";
125
+ readonly 'code-text': "#FFFFFF";
126
+ readonly 'code-muted': "#B4B4B4";
127
+ readonly selection: "#383838";
128
+ readonly 'selection-text': "#FFFFFF";
129
+ readonly scrollbar: "#858585";
130
+ readonly 'scrollbar-track': "#171717";
131
+ readonly 'shadow-action': "0 3px 8px #00000040";
132
+ readonly 'shadow-panel': "0 2px 12px #00000020";
133
+ readonly 'shadow-dialog': "0 20px 100px #00000080";
134
+ };
135
+ };
36
136
  };
37
137
  type BubbaTokens = typeof tokens;
38
138
 
package/dist/tokens.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // src/tokens.ts
2
2
  var tokens = {
3
3
  brandVersion: "1.0.0",
4
+ systemVersion: "0.3.0",
4
5
  colors: {
5
6
  blue: "#007CE8",
6
7
  red: "#CC0000",
@@ -17,7 +18,97 @@ var tokens = {
17
18
  },
18
19
  spacing: { 1: 4, 2: 8, 3: 12, 4: 16, 6: 24, 8: 32, 12: 48, 16: 64 },
19
20
  radius: { control: 4, panel: 12 },
20
- touchTarget: { minimum: 44, default: 48 }
21
+ touchTarget: { minimum: 44, default: 48 },
22
+ layout: { maxWidth: 1200, readingWidth: 720, breakpoints: { compact: 640, wide: 960 } },
23
+ motion: { feedback: 140 },
24
+ themes: {
25
+ "light": {
26
+ "canvas": "#F7F8FA",
27
+ "surface": "#FFFFFF",
28
+ "raised": "#FFFFFF",
29
+ "hover": "#F6F7F9",
30
+ "selected": "#ECEEF1",
31
+ "text": "#131B23",
32
+ "heading": "#03256C",
33
+ "muted": "#526075",
34
+ "line": "#E4E6EA",
35
+ "border": "#637086",
36
+ "link": "#03256C",
37
+ "link-hover": "#A80000",
38
+ "focus": "#03256C",
39
+ "focus-inner": "#FFFFFF",
40
+ "accent": "#CC0000",
41
+ "action-text": "#FFFFFF",
42
+ "action-border": "#A80000",
43
+ "disabled-surface": "#F0F2F5",
44
+ "disabled-text": "#6B727E",
45
+ "info": "#03256C",
46
+ "info-surface": "#F1F5FC",
47
+ "info-border": "#03256C",
48
+ "success": "#23633F",
49
+ "success-surface": "#EAF5EF",
50
+ "success-border": "#23633F",
51
+ "warning": "#805500",
52
+ "warning-surface": "#FFF4D6",
53
+ "warning-border": "#805500",
54
+ "error": "#B42318",
55
+ "error-surface": "#FFF0EF",
56
+ "error-border": "#B42318",
57
+ "code-surface": "#171717",
58
+ "code-text": "#FFFFFF",
59
+ "code-muted": "#B4B4B4",
60
+ "selection": "#D9E8FB",
61
+ "selection-text": "#131B23",
62
+ "scrollbar": "#637086",
63
+ "scrollbar-track": "#F7F8FA",
64
+ "shadow-action": "0 3px 8px #CC000024",
65
+ "shadow-panel": "0 2px 12px #131B2308",
66
+ "shadow-dialog": "0 20px 100px #00133345"
67
+ },
68
+ "dark": {
69
+ "canvas": "#171717",
70
+ "surface": "#202020",
71
+ "raised": "#292929",
72
+ "hover": "#303030",
73
+ "selected": "#383838",
74
+ "text": "#FFFFFF",
75
+ "heading": "#FFFFFF",
76
+ "muted": "#B4B4B4",
77
+ "line": "#3D3D3D",
78
+ "border": "#858585",
79
+ "link": "#8DC7FF",
80
+ "link-hover": "#B8DEFF",
81
+ "focus": "#FFFFFF",
82
+ "focus-inner": "#202020",
83
+ "accent": "#FF9F99",
84
+ "action-text": "#FFFFFF",
85
+ "action-border": "#EF817A",
86
+ "disabled-surface": "#242424",
87
+ "disabled-text": "#9A9A9A",
88
+ "info": "#8DC7FF",
89
+ "info-surface": "#1D2D3D",
90
+ "info-border": "#8DC7FF",
91
+ "success": "#8DDBA9",
92
+ "success-surface": "#183323",
93
+ "success-border": "#8DDBA9",
94
+ "warning": "#FED766",
95
+ "warning-surface": "#3A2E12",
96
+ "warning-border": "#FED766",
97
+ "error": "#FFAEA8",
98
+ "error-surface": "#3D2020",
99
+ "error-border": "#FFAEA8",
100
+ "code-surface": "#171717",
101
+ "code-text": "#FFFFFF",
102
+ "code-muted": "#B4B4B4",
103
+ "selection": "#383838",
104
+ "selection-text": "#FFFFFF",
105
+ "scrollbar": "#858585",
106
+ "scrollbar-track": "#171717",
107
+ "shadow-action": "0 3px 8px #00000040",
108
+ "shadow-panel": "0 2px 12px #00000020",
109
+ "shadow-dialog": "0 20px 100px #00000080"
110
+ }
111
+ }
21
112
  };
22
113
 
23
114
  export { tokens };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bubbasfw/ui",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Bubba\u2019s universal React Native and web design system, styled with NativeWind.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -54,7 +54,7 @@
54
54
  "url": "https://github.com/handh-dev/bubbas-ui.git",
55
55
  "directory": "packages/ui"
56
56
  },
57
- "homepage": "https://github.com/handh-dev/bubbas-ui#readme",
57
+ "homepage": "https://handh-dev.github.io/bubbas-ui/",
58
58
  "keywords": [
59
59
  "react-native",
60
60
  "react-native-web",
@@ -2,8 +2,8 @@
2
2
 
3
3
  import { forwardRef, type ElementRef, type ReactNode } from 'react';
4
4
  import { ActivityIndicator, Pressable, Text, type PressableProps } from 'react-native';
5
- import { twMerge } from 'tailwind-merge';
6
- import { tokens } from '../tokens';
5
+ import { cn, focusRing } from '../internal/utils';
6
+ import { useTheme } from '../theme';
7
7
 
8
8
  export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost';
9
9
  export type ButtonSize = 'sm' | 'md' | 'lg';
@@ -20,16 +20,16 @@ export interface ButtonProps extends Omit<PressableProps, 'children' | 'classNam
20
20
  }
21
21
 
22
22
  const containerVariants: Record<ButtonVariant, string> = {
23
- primary: 'border-bubba-red bg-bubba-red',
24
- secondary: 'border-bubba-navy bg-bubba-navy',
25
- outline: 'border-bubba-navy bg-transparent',
23
+ primary: 'border-ui-action-border bg-bubba-red',
24
+ secondary: 'border-ui-border bg-ui-selected',
25
+ outline: 'border-ui-border bg-transparent',
26
26
  ghost: 'border-transparent bg-transparent',
27
27
  };
28
28
  const textVariants: Record<ButtonVariant, string> = {
29
- primary: 'text-bubba-white',
30
- secondary: 'text-bubba-white',
31
- outline: 'text-bubba-navy',
32
- ghost: 'text-bubba-navy',
29
+ primary: 'text-ui-action-text',
30
+ secondary: 'text-ui-text',
31
+ outline: 'text-ui-text',
32
+ ghost: 'text-ui-link',
33
33
  };
34
34
  const containerSizes: Record<ButtonSize, string> = {
35
35
  sm: 'min-h-[44px] px-3 py-2',
@@ -61,14 +61,13 @@ export const Button = forwardRef<ElementRef<typeof Pressable>, ButtonProps>(
61
61
  },
62
62
  ref,
63
63
  ) {
64
+ const { colors } = useTheme();
64
65
  const explicitlyDisabled = disabled || (ariaDisabled ?? accessibilityState?.disabled ?? false);
65
66
  const isDisabled = explicitlyDisabled || loading;
66
67
  const isBusy = loading || (ariaBusy ?? accessibilityState?.busy ?? false);
67
68
  const spinnerColor = explicitlyDisabled
68
- ? tokens.colors.disabledText
69
- : variant === 'primary' || variant === 'secondary'
70
- ? tokens.colors.white
71
- : tokens.colors.navy;
69
+ ? colors['disabled-text']
70
+ : variant === 'primary' ? colors['action-text'] : colors.text;
72
71
 
73
72
  return (
74
73
  <Pressable
@@ -79,21 +78,22 @@ export const Button = forwardRef<ElementRef<typeof Pressable>, ButtonProps>(
79
78
  aria-busy={isBusy}
80
79
  aria-disabled={isDisabled}
81
80
  disabled={isDisabled}
82
- className={twMerge(
83
- 'min-w-[44px] flex-row items-center justify-center gap-2 rounded-[4px] border active:opacity-80 web:focus-visible:outline web:focus-visible:outline-2 web:focus-visible:outline-offset-2 web:focus-visible:outline-bubba-navy',
81
+ className={cn(
82
+ 'min-w-[44px] flex-row items-center justify-center gap-2 rounded-[4px] border active:opacity-80',
83
+ focusRing,
84
84
  containerVariants[variant],
85
85
  containerSizes[size],
86
- explicitlyDisabled && 'border-bubba-disabled bg-bubba-disabled',
86
+ explicitlyDisabled && 'border-ui-disabled-surface bg-ui-disabled-surface',
87
87
  className,
88
88
  )}
89
89
  >
90
90
  {loading ? <ActivityIndicator size="small" color={spinnerColor} accessible={false} /> : null}
91
91
  <Text
92
- className={twMerge(
92
+ className={cn(
93
93
  'font-bold text-center',
94
94
  textVariants[variant],
95
95
  textSizes[size],
96
- explicitlyDisabled && 'text-bubba-disabledText',
96
+ explicitlyDisabled && 'text-ui-disabled-text',
97
97
  textClassName,
98
98
  )}
99
99
  >
@@ -0,0 +1,39 @@
1
+ 'use client';
2
+
3
+ import { forwardRef, type ElementRef, type ReactNode } from 'react';
4
+ import { Linking, Platform, Pressable, type PressableProps } from 'react-native';
5
+ import { cn, focusRing } from '../internal/utils';
6
+ import { Text } from './typography';
7
+
8
+ export interface IconButtonProps extends Omit<PressableProps, 'children' | 'accessibilityLabel'> {
9
+ accessibilityLabel: string;
10
+ children: ReactNode;
11
+ variant?: 'primary' | 'outline' | 'ghost';
12
+ className?: string;
13
+ }
14
+ const iconVariants = { primary: 'bg-bubba-red border-bubba-red', outline: 'border-ui-border bg-transparent', ghost: 'border-transparent bg-transparent' };
15
+ export const IconButton = forwardRef<ElementRef<typeof Pressable>, IconButtonProps>(function IconButton({ accessibilityLabel, children, variant = 'ghost', className, disabled, accessibilityState, 'aria-disabled': ariaDisabled, ...props }, ref) {
16
+ const isDisabled = disabled || (ariaDisabled ?? accessibilityState?.disabled ?? false);
17
+ return <Pressable {...props} ref={ref} accessibilityRole="button" accessibilityLabel={accessibilityLabel} disabled={isDisabled} aria-disabled={isDisabled} accessibilityState={{ ...accessibilityState, disabled: isDisabled }} className={cn('h-12 w-12 items-center justify-center rounded border active:opacity-80', focusRing, iconVariants[variant], isDisabled && 'opacity-50', className)}>{children}</Pressable>;
18
+ });
19
+
20
+ export interface LinkProps extends Omit<PressableProps, 'children'> {
21
+ href: string;
22
+ children: ReactNode;
23
+ external?: boolean;
24
+ className?: string;
25
+ textClassName?: string;
26
+ /** Called when the operating system cannot open a native URL. */
27
+ onOpenError?: (error: unknown) => void;
28
+ }
29
+ type WebLinkProps = PressableProps & { href?: string; hrefAttrs?: { target?: string; rel?: string } };
30
+ const LinkPressable = Pressable as typeof Pressable & ((props: WebLinkProps) => ReactNode);
31
+
32
+ export const Link = forwardRef<ElementRef<typeof Pressable>, LinkProps>(function Link({ href, children, external = false, className, textClassName, disabled, onPress, onOpenError, accessibilityState, 'aria-disabled': ariaDisabled, ...props }, ref) {
33
+ const isDisabled = disabled || (ariaDisabled ?? accessibilityState?.disabled ?? false);
34
+ const webProps = Platform.OS === 'web' && !isDisabled ? { href, hrefAttrs: external ? { target: '_blank', rel: 'noopener noreferrer' } : undefined } : {};
35
+ return <LinkPressable {...props} {...webProps} ref={ref} accessibilityRole="link" disabled={isDisabled} aria-disabled={isDisabled} accessibilityState={{ ...accessibilityState, disabled: isDisabled }} className={cn('min-h-[44px] min-w-[44px] justify-center self-start', focusRing, isDisabled && 'opacity-50', className)} onPress={(event) => {
36
+ onPress?.(event);
37
+ if (Platform.OS !== 'web' && !event.isDefaultPrevented()) void Linking.openURL(href).catch((error: unknown) => onOpenError?.(error));
38
+ }}><Text className={cn('text-ui-link underline', textClassName)}>{children}</Text></LinkPressable>;
39
+ });
@@ -0,0 +1,55 @@
1
+ 'use client';
2
+
3
+ import { forwardRef, useState, type ElementRef, type ReactNode } from 'react';
4
+ import { Image, Pressable, View, type ImageSourcePropType, type ViewProps, type PressableProps } from 'react-native';
5
+ import { cn, focusRing } from '../internal/utils';
6
+ import { Text } from './typography';
7
+
8
+ export type StatusTone = 'neutral' | 'info' | 'success' | 'warning' | 'error';
9
+ const toneClasses = {
10
+ neutral: 'bg-ui-selected', info: 'bg-ui-info-surface', success: 'bg-ui-success-surface', warning: 'bg-ui-warning-surface', error: 'bg-ui-error-surface',
11
+ };
12
+ const toneText = { neutral: 'text-ui-text', info: 'text-ui-info', success: 'text-ui-success', warning: 'text-ui-warning', error: 'text-ui-error' };
13
+ export interface BadgeProps extends ViewProps { children: ReactNode; tone?: StatusTone; className?: string; textClassName?: string }
14
+ export function Badge({ children, tone = 'neutral', className, textClassName, ...props }: BadgeProps) {
15
+ return <View {...props} className={cn('self-start rounded-full px-3 py-1', toneClasses[tone], className)}><Text variant="small" className={cn('font-semibold', toneText[tone], textClassName)}>{children}</Text></View>;
16
+ }
17
+
18
+ export interface AvatarProps extends ViewProps {
19
+ source?: ImageSourcePropType;
20
+ name: string;
21
+ size?: 'sm' | 'md' | 'lg';
22
+ className?: string;
23
+ }
24
+ const avatarSizes = { sm: 'h-8 w-8', md: 'h-12 w-12', lg: 'h-16 w-16' };
25
+ export function Avatar({ source, name, size = 'md', className, ...props }: AvatarProps) {
26
+ const [failedSource, setFailedSource] = useState<ImageSourcePropType>();
27
+ const initials = name.trim().split(/\s+/).slice(0, 2).map((part) => part[0]).join('').toUpperCase() || '?';
28
+ return <View {...props} accessible accessibilityRole="image" accessibilityLabel={name} className={cn('items-center justify-center overflow-hidden rounded-full bg-ui-selected', avatarSizes[size], className)}>
29
+ {source && source !== failedSource ? <Image source={source} resizeMode="cover" accessible={false} className="h-full w-full" onError={() => setFailedSource(source)} /> : <Text accessible={false} className="font-bold">{initials}</Text>}
30
+ </View>;
31
+ }
32
+
33
+ export interface EmptyStateProps extends ViewProps { title: string; description?: string; children?: ReactNode; className?: string }
34
+ export function EmptyState({ title, description, children, className, ...props }: EmptyStateProps) {
35
+ return <View {...props} className={cn('items-start gap-3 py-8', className)}>
36
+ <Text className="text-lg font-bold">{title}</Text>
37
+ {description ? <Text tone="muted">{description}</Text> : null}
38
+ {children}
39
+ </View>;
40
+ }
41
+
42
+ export interface ListItemProps extends Omit<ViewProps, 'children'> {
43
+ title: string;
44
+ description?: string;
45
+ leading?: ReactNode;
46
+ trailing?: ReactNode;
47
+ className?: string;
48
+ onPress?: PressableProps['onPress'];
49
+ disabled?: boolean;
50
+ }
51
+ export const ListItem = forwardRef<ElementRef<typeof View>, ListItemProps>(function ListItem({ title, description, leading, trailing, className, disabled = false, onPress, accessibilityState, ...props }, ref) {
52
+ const content = <>{leading}<View className="min-w-0 flex-1 gap-1"><Text className="font-semibold">{title}</Text>{description ? <Text variant="small" tone="muted">{description}</Text> : null}</View>{trailing}</>;
53
+ const classes = cn('min-h-[48px] min-w-[44px] flex-row items-center gap-3 border-b border-ui-line py-3', onPress && focusRing, disabled && 'opacity-50', className);
54
+ return onPress ? <Pressable {...props} ref={ref} onPress={onPress} disabled={disabled} aria-disabled={disabled} accessibilityRole="button" accessibilityState={{ ...accessibilityState, disabled }} className={classes}>{content}</Pressable> : <View {...props} ref={ref} accessibilityState={accessibilityState} className={classes}>{content}</View>;
55
+ });