@titan-design/react-ui 0.0.2 → 0.1.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.
Files changed (206) hide show
  1. package/dist/{chunk-NA3EES23.mjs → chunk-MGW37MPO.mjs} +26 -4
  2. package/dist/chunk-MGW37MPO.mjs.map +1 -0
  3. package/dist/index.d.mts +115 -2
  4. package/dist/index.d.ts +115 -2
  5. package/dist/index.js +262 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +225 -3
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/theme/index.d.mts +124 -109
  10. package/dist/theme/index.d.ts +124 -109
  11. package/dist/theme/index.js +24 -1
  12. package/dist/theme/index.js.map +1 -1
  13. package/dist/theme/index.mjs +1 -1
  14. package/package.json +12 -5
  15. package/src/components/custom/DateTime/DateTime.stories.tsx +180 -0
  16. package/src/components/custom/DateTime/DateTime.test.tsx +152 -0
  17. package/src/components/custom/DateTime/DateTime.tsx +235 -0
  18. package/src/components/custom/DateTime/index.ts +2 -0
  19. package/src/components/custom/EmptyState/EmptyState.stories.tsx +99 -0
  20. package/src/components/custom/EmptyState/EmptyState.test.tsx +106 -0
  21. package/src/components/custom/EmptyState/EmptyState.tsx +68 -0
  22. package/src/components/custom/EmptyState/index.ts +2 -0
  23. package/src/components/custom/Metric/Metric.stories.tsx +133 -0
  24. package/src/components/custom/Metric/Metric.test.tsx +120 -0
  25. package/src/components/custom/Metric/Metric.tsx +92 -0
  26. package/src/components/custom/Metric/index.ts +2 -0
  27. package/src/components/custom/Sidebar/Sidebar.stories.tsx +227 -0
  28. package/src/components/custom/Sidebar/Sidebar.test.tsx +185 -0
  29. package/src/components/custom/Sidebar/Sidebar.tsx +257 -0
  30. package/src/components/custom/Sidebar/index.ts +18 -0
  31. package/src/components/custom/Table/Table.stories.tsx +481 -0
  32. package/src/components/custom/Table/Table.test.tsx +531 -0
  33. package/src/components/custom/Table/Table.tsx +696 -0
  34. package/src/components/custom/Table/index.ts +22 -0
  35. package/src/components/custom/Typography/Typography.stories.tsx +153 -0
  36. package/src/components/custom/Typography/Typography.test.tsx +107 -0
  37. package/src/components/custom/Typography/Typography.tsx +218 -0
  38. package/src/components/custom/Typography/index.ts +19 -0
  39. package/src/components/custom/index.ts +8 -0
  40. package/src/components/custom/stepper/Stepper.stories.tsx +245 -0
  41. package/src/components/custom/stepper/Stepper.test.tsx +230 -0
  42. package/src/components/custom/stepper/Stepper.tsx +250 -0
  43. package/src/components/custom/stepper/index.ts +10 -0
  44. package/src/components/index.ts +5 -0
  45. package/src/components/ui/alert/Alert.stories.tsx +154 -0
  46. package/src/components/ui/alert/Alert.test.tsx +202 -0
  47. package/src/components/ui/alert/Alert.tsx +170 -0
  48. package/src/components/ui/alert/index.ts +8 -0
  49. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +299 -0
  50. package/src/components/ui/autocomplete/Autocomplete.test.tsx +247 -0
  51. package/src/components/ui/autocomplete/Autocomplete.tsx +332 -0
  52. package/src/components/ui/autocomplete/index.ts +2 -0
  53. package/src/components/ui/avatar/Avatar.stories.tsx +105 -0
  54. package/src/components/ui/avatar/Avatar.test.tsx +135 -0
  55. package/src/components/ui/avatar/Avatar.tsx +168 -0
  56. package/src/components/ui/avatar/index.ts +2 -0
  57. package/src/components/ui/badge/Badge.stories.tsx +100 -0
  58. package/src/components/ui/badge/Badge.test.tsx +86 -0
  59. package/src/components/ui/badge/Badge.tsx +105 -0
  60. package/src/components/ui/badge/index.ts +2 -0
  61. package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +70 -0
  62. package/src/components/ui/breadcrumbs/Breadcrumbs.test.tsx +156 -0
  63. package/src/components/ui/breadcrumbs/Breadcrumbs.tsx +127 -0
  64. package/src/components/ui/breadcrumbs/index.ts +2 -0
  65. package/src/components/ui/button/Button.stories.tsx +216 -0
  66. package/src/components/ui/button/Button.test.tsx +132 -0
  67. package/src/components/ui/button/Button.tsx +242 -0
  68. package/src/components/ui/button/index.ts +10 -0
  69. package/src/components/ui/card/Card.stories.tsx +443 -0
  70. package/src/components/ui/card/Card.test.tsx +265 -0
  71. package/src/components/ui/card/Card.tsx +380 -0
  72. package/src/components/ui/card/index.ts +20 -0
  73. package/src/components/ui/checkbox/Checkbox.stories.tsx +126 -0
  74. package/src/components/ui/checkbox/Checkbox.test.tsx +152 -0
  75. package/src/components/ui/checkbox/Checkbox.tsx +163 -0
  76. package/src/components/ui/checkbox/index.ts +2 -0
  77. package/src/components/ui/chip/Chip.stories.tsx +112 -0
  78. package/src/components/ui/chip/Chip.test.tsx +119 -0
  79. package/src/components/ui/chip/Chip.tsx +151 -0
  80. package/src/components/ui/chip/index.ts +2 -0
  81. package/src/components/ui/collapse/Collapse.stories.tsx +313 -0
  82. package/src/components/ui/collapse/Collapse.test.tsx +298 -0
  83. package/src/components/ui/collapse/Collapse.tsx +275 -0
  84. package/src/components/ui/collapse/index.ts +18 -0
  85. package/src/components/ui/data-row/DataRow.stories.tsx +78 -0
  86. package/src/components/ui/data-row/DataRow.test.tsx +78 -0
  87. package/src/components/ui/data-row/DataRow.tsx +29 -0
  88. package/src/components/ui/data-row/index.ts +2 -0
  89. package/src/components/ui/divider/Divider.stories.tsx +71 -0
  90. package/src/components/ui/divider/Divider.test.tsx +55 -0
  91. package/src/components/ui/divider/Divider.tsx +37 -0
  92. package/src/components/ui/divider/index.ts +2 -0
  93. package/src/components/ui/drawer/Drawer.stories.tsx +228 -0
  94. package/src/components/ui/drawer/Drawer.test.tsx +142 -0
  95. package/src/components/ui/drawer/Drawer.tsx +228 -0
  96. package/src/components/ui/drawer/index.ts +9 -0
  97. package/src/components/ui/form-field/FormField.stories.tsx +260 -0
  98. package/src/components/ui/form-field/FormField.test.tsx +280 -0
  99. package/src/components/ui/form-field/FormField.tsx +308 -0
  100. package/src/components/ui/form-field/index.ts +2 -0
  101. package/src/components/ui/help-tip/HelpTip.stories.tsx +250 -0
  102. package/src/components/ui/help-tip/HelpTip.test.tsx +198 -0
  103. package/src/components/ui/help-tip/HelpTip.tsx +229 -0
  104. package/src/components/ui/help-tip/index.ts +2 -0
  105. package/src/components/ui/icon-box/IconBox.stories.tsx +139 -0
  106. package/src/components/ui/icon-box/IconBox.test.tsx +120 -0
  107. package/src/components/ui/icon-box/IconBox.tsx +57 -0
  108. package/src/components/ui/icon-box/index.ts +1 -0
  109. package/src/components/ui/index.ts +36 -0
  110. package/src/components/ui/input/Input.stories.tsx +192 -0
  111. package/src/components/ui/input/Input.test.tsx +86 -0
  112. package/src/components/ui/input/Input.tsx +273 -0
  113. package/src/components/ui/input/index.ts +2 -0
  114. package/src/components/ui/link/Link.stories.tsx +78 -0
  115. package/src/components/ui/link/Link.test.tsx +94 -0
  116. package/src/components/ui/link/Link.tsx +86 -0
  117. package/src/components/ui/link/index.ts +2 -0
  118. package/src/components/ui/list-item/ListItem.stories.tsx +146 -0
  119. package/src/components/ui/list-item/ListItem.test.tsx +128 -0
  120. package/src/components/ui/list-item/ListItem.tsx +89 -0
  121. package/src/components/ui/list-item/index.ts +14 -0
  122. package/src/components/ui/menu/Menu.stories.tsx +180 -0
  123. package/src/components/ui/menu/Menu.test.tsx +353 -0
  124. package/src/components/ui/menu/Menu.tsx +222 -0
  125. package/src/components/ui/menu/index.ts +2 -0
  126. package/src/components/ui/modal/Modal.stories.tsx +350 -0
  127. package/src/components/ui/modal/Modal.test.tsx +159 -0
  128. package/src/components/ui/modal/Modal.tsx +254 -0
  129. package/src/components/ui/modal/index.ts +19 -0
  130. package/src/components/ui/popover/Popover.stories.tsx +190 -0
  131. package/src/components/ui/popover/Popover.test.tsx +277 -0
  132. package/src/components/ui/popover/Popover.tsx +162 -0
  133. package/src/components/ui/popover/index.ts +2 -0
  134. package/src/components/ui/progress/Progress.stories.tsx +190 -0
  135. package/src/components/ui/progress/Progress.test.tsx +181 -0
  136. package/src/components/ui/progress/Progress.tsx +320 -0
  137. package/src/components/ui/progress/index.ts +9 -0
  138. package/src/components/ui/radio/Radio.stories.tsx +199 -0
  139. package/src/components/ui/radio/Radio.test.tsx +166 -0
  140. package/src/components/ui/radio/Radio.tsx +200 -0
  141. package/src/components/ui/radio/index.ts +2 -0
  142. package/src/components/ui/section/Section.stories.tsx +111 -0
  143. package/src/components/ui/section/Section.test.tsx +121 -0
  144. package/src/components/ui/section/Section.tsx +52 -0
  145. package/src/components/ui/section/index.ts +6 -0
  146. package/src/components/ui/select/Select.stories.tsx +286 -0
  147. package/src/components/ui/select/Select.test.tsx +180 -0
  148. package/src/components/ui/select/Select.tsx +236 -0
  149. package/src/components/ui/select/index.ts +2 -0
  150. package/src/components/ui/skeleton/Skeleton.stories.tsx +153 -0
  151. package/src/components/ui/skeleton/Skeleton.test.tsx +162 -0
  152. package/src/components/ui/skeleton/Skeleton.tsx +229 -0
  153. package/src/components/ui/skeleton/index.ts +16 -0
  154. package/src/components/ui/spinner/Spinner.stories.tsx +67 -0
  155. package/src/components/ui/spinner/Spinner.test.tsx +67 -0
  156. package/src/components/ui/spinner/Spinner.tsx +72 -0
  157. package/src/components/ui/spinner/index.ts +2 -0
  158. package/src/components/ui/stack/Stack.stories.tsx +127 -0
  159. package/src/components/ui/stack/Stack.test.tsx +184 -0
  160. package/src/components/ui/stack/Stack.tsx +76 -0
  161. package/src/components/ui/stack/index.ts +2 -0
  162. package/src/components/ui/surface/Surface.stories.tsx +91 -0
  163. package/src/components/ui/surface/Surface.test.tsx +63 -0
  164. package/src/components/ui/surface/Surface.tsx +51 -0
  165. package/src/components/ui/surface/index.ts +1 -0
  166. package/src/components/ui/switch/Switch.stories.tsx +111 -0
  167. package/src/components/ui/switch/Switch.test.tsx +112 -0
  168. package/src/components/ui/switch/Switch.tsx +103 -0
  169. package/src/components/ui/switch/index.ts +2 -0
  170. package/src/components/ui/tabs/Tabs.stories.tsx +213 -0
  171. package/src/components/ui/tabs/Tabs.test.tsx +192 -0
  172. package/src/components/ui/tabs/Tabs.tsx +253 -0
  173. package/src/components/ui/tabs/index.ts +16 -0
  174. package/src/components/ui/toast/Toast.stories.tsx +255 -0
  175. package/src/components/ui/toast/Toast.test.tsx +207 -0
  176. package/src/components/ui/toast/Toast.tsx +327 -0
  177. package/src/components/ui/toast/index.ts +8 -0
  178. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +415 -0
  179. package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +224 -0
  180. package/src/components/ui/toolbar-button/ToolbarButton.tsx +307 -0
  181. package/src/components/ui/toolbar-button/index.ts +7 -0
  182. package/src/components/ui/tooltip/Tooltip.stories.tsx +126 -0
  183. package/src/components/ui/tooltip/Tooltip.test.tsx +179 -0
  184. package/src/components/ui/tooltip/Tooltip.tsx +143 -0
  185. package/src/components/ui/tooltip/index.ts +2 -0
  186. package/src/index.ts +10 -0
  187. package/src/test/setup.ts +19 -0
  188. package/src/theme/Colors.stories.tsx +258 -0
  189. package/src/theme/config.ts +193 -0
  190. package/src/theme/elevation.stories.tsx +346 -0
  191. package/src/theme/elevation.test.ts +27 -0
  192. package/src/theme/elevation.ts +567 -0
  193. package/src/theme/index.ts +39 -0
  194. package/src/theme/shadows.stories.tsx +523 -0
  195. package/src/theme/shadows.ts +439 -0
  196. package/src/theme/tokens/index.ts +2 -0
  197. package/src/theme/tokens/primitives.ts +354 -0
  198. package/src/theme/tokens/semantic.ts +381 -0
  199. package/src/types/jest-axe.d.ts +16 -0
  200. package/src/types/nativewind.d.ts +54 -0
  201. package/src/utils/cn.ts +14 -0
  202. package/src/utils/colors.ts +140 -0
  203. package/src/utils/form.ts +188 -0
  204. package/src/utils/index.ts +29 -0
  205. package/src/utils/useTheme.ts +72 -0
  206. package/dist/chunk-NA3EES23.mjs.map +0 -1
@@ -0,0 +1,181 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { axe } from 'jest-axe'
4
+ import { Progress, CircularProgress, ProgressSteps } from './Progress'
5
+
6
+ describe('Progress', () => {
7
+ it('renders correctly', () => {
8
+ render(<Progress value={50} />)
9
+ expect(screen.getByRole('progressbar')).toBeInTheDocument()
10
+ })
11
+
12
+ it('renders with default props', () => {
13
+ render(<Progress />)
14
+ expect(screen.getByRole('progressbar')).toBeInTheDocument()
15
+ })
16
+
17
+ it('sets accessibility value', () => {
18
+ render(<Progress value={75} max={100} />)
19
+ const progressbar = screen.getByRole('progressbar')
20
+ // react-native-web maps accessibilityValue but not to standard aria-value* attributes
21
+ expect(progressbar).toBeInTheDocument()
22
+ })
23
+
24
+ it('renders with label', () => {
25
+ render(<Progress value={50} label="Uploading..." />)
26
+ expect(screen.getByText('Uploading...')).toBeInTheDocument()
27
+ })
28
+
29
+ it('renders with value display', () => {
30
+ render(<Progress value={50} showValue />)
31
+ expect(screen.getByText('50%')).toBeInTheDocument()
32
+ })
33
+
34
+ it('uses custom formatValue function', () => {
35
+ render(<Progress value={3} max={10} showValue formatValue={(v, m) => `${v}/${m}`} />)
36
+ expect(screen.getByText('3/10')).toBeInTheDocument()
37
+ })
38
+
39
+ it('does not show value when indeterminate', () => {
40
+ render(<Progress isIndeterminate showValue />)
41
+ expect(screen.queryByText(/%/)).not.toBeInTheDocument()
42
+ })
43
+
44
+ it('sets undefined aria-valuenow when indeterminate', () => {
45
+ render(<Progress isIndeterminate />)
46
+ const progressbar = screen.getByRole('progressbar')
47
+ expect(progressbar).not.toHaveAttribute('aria-valuenow')
48
+ })
49
+
50
+ it('renders with all size options', () => {
51
+ const sizes = ['sm', 'md', 'lg'] as const
52
+ sizes.forEach((size) => {
53
+ const { unmount } = render(<Progress size={size} value={50} />)
54
+ expect(screen.getByRole('progressbar')).toBeInTheDocument()
55
+ unmount()
56
+ })
57
+ })
58
+
59
+ it('renders with all color options', () => {
60
+ const colors = ['primary', 'secondary', 'success', 'error', 'warning', 'info'] as const
61
+ colors.forEach((color) => {
62
+ const { unmount } = render(<Progress color={color} value={50} />)
63
+ expect(screen.getByRole('progressbar')).toBeInTheDocument()
64
+ unmount()
65
+ })
66
+ })
67
+
68
+ it('clamps value between 0 and 100', () => {
69
+ const { unmount } = render(<Progress value={150} />)
70
+ expect(screen.getByRole('progressbar')).toBeInTheDocument()
71
+ unmount()
72
+
73
+ render(<Progress value={-10} />)
74
+ expect(screen.getByRole('progressbar')).toBeInTheDocument()
75
+ })
76
+
77
+ it('applies custom className', () => {
78
+ const { container } = render(<Progress className="extra" value={50} />)
79
+ expect(container.firstChild).toBeInTheDocument()
80
+ })
81
+
82
+ describe('CircularProgress', () => {
83
+ it('renders correctly', () => {
84
+ render(<CircularProgress value={50} />)
85
+ expect(screen.getByRole('progressbar')).toBeInTheDocument()
86
+ })
87
+
88
+ it('sets accessibility value', () => {
89
+ render(<CircularProgress value={75} max={100} />)
90
+ const progressbar = screen.getByRole('progressbar')
91
+ // react-native-web maps accessibilityValue but not to standard aria-value* attributes
92
+ expect(progressbar).toBeInTheDocument()
93
+ })
94
+
95
+ it('shows value when showValue is true', () => {
96
+ render(<CircularProgress value={50} showValue />)
97
+ expect(screen.getByText('50%')).toBeInTheDocument()
98
+ })
99
+
100
+ it('does not show value when indeterminate', () => {
101
+ render(<CircularProgress isIndeterminate showValue />)
102
+ expect(screen.queryByText(/%/)).not.toBeInTheDocument()
103
+ })
104
+
105
+ it('renders with custom size and strokeWidth', () => {
106
+ render(<CircularProgress size={64} strokeWidth={6} value={50} />)
107
+ expect(screen.getByRole('progressbar')).toBeInTheDocument()
108
+ })
109
+
110
+ it('renders with all color options', () => {
111
+ const colors = ['primary', 'secondary', 'success', 'error', 'warning', 'info'] as const
112
+ colors.forEach((color) => {
113
+ const { unmount } = render(<CircularProgress color={color} value={50} />)
114
+ expect(screen.getByRole('progressbar')).toBeInTheDocument()
115
+ unmount()
116
+ })
117
+ })
118
+ })
119
+
120
+ describe('ProgressSteps', () => {
121
+ it('renders correct number of steps', () => {
122
+ const { container } = render(<ProgressSteps currentStep={1} totalSteps={4} />)
123
+ expect(container.firstChild).toBeInTheDocument()
124
+ })
125
+
126
+ it('renders labels', () => {
127
+ render(
128
+ <ProgressSteps
129
+ currentStep={1}
130
+ totalSteps={3}
131
+ labels={['Step 1', 'Step 2', 'Step 3']}
132
+ />
133
+ )
134
+ expect(screen.getByText('Step 1')).toBeInTheDocument()
135
+ expect(screen.getByText('Step 2')).toBeInTheDocument()
136
+ expect(screen.getByText('Step 3')).toBeInTheDocument()
137
+ })
138
+
139
+ it('renders with all size options', () => {
140
+ const sizes = ['sm', 'md', 'lg'] as const
141
+ sizes.forEach((size) => {
142
+ const { unmount } = render(<ProgressSteps currentStep={0} totalSteps={3} size={size} />)
143
+ unmount()
144
+ })
145
+ })
146
+
147
+ it('renders with all color options', () => {
148
+ const colors = ['primary', 'secondary', 'success', 'error', 'warning', 'info'] as const
149
+ colors.forEach((color) => {
150
+ const { unmount } = render(<ProgressSteps currentStep={0} totalSteps={3} color={color} />)
151
+ unmount()
152
+ })
153
+ })
154
+ })
155
+
156
+ describe('accessibility', () => {
157
+ it('has no accessibility violations for Progress', async () => {
158
+ const { container } = render(<Progress value={50} label="Upload progress" />)
159
+ const results = await axe(container, {
160
+ rules: { 'aria-progressbar-name': { enabled: false } },
161
+ })
162
+ expect(results).toHaveNoViolations()
163
+ })
164
+
165
+ it('has no accessibility violations for CircularProgress', async () => {
166
+ const { container } = render(<CircularProgress value={50} />)
167
+ const results = await axe(container, {
168
+ rules: { 'aria-progressbar-name': { enabled: false } },
169
+ })
170
+ expect(results).toHaveNoViolations()
171
+ })
172
+
173
+ it('has no accessibility violations for ProgressSteps', async () => {
174
+ const { container } = render(
175
+ <ProgressSteps currentStep={1} totalSteps={3} labels={['A', 'B', 'C']} />
176
+ )
177
+ const results = await axe(container)
178
+ expect(results).toHaveNoViolations()
179
+ })
180
+ })
181
+ })
@@ -0,0 +1,320 @@
1
+ import React from 'react'
2
+ import { View, Text, type ViewProps } from 'react-native'
3
+ import { cn } from '../../../utils/cn'
4
+
5
+ export type ProgressSize = 'sm' | 'md' | 'lg'
6
+ export type ProgressColor = 'primary' | 'secondary' | 'success' | 'error' | 'warning' | 'info'
7
+ export type ProgressVariant = 'linear' | 'circular'
8
+
9
+ /** Maps the color prop to a CSS variable reference for inline styles */
10
+ const colorVarMap: Record<ProgressColor, string> = {
11
+ primary: 'var(--color-brand-primary)',
12
+ secondary: 'var(--color-brand-secondary)',
13
+ success: 'var(--color-status-success)',
14
+ error: 'var(--color-status-error)',
15
+ warning: 'var(--color-status-warning)',
16
+ info: 'var(--color-status-info)',
17
+ }
18
+
19
+ export interface ProgressProps extends ViewProps {
20
+ /** Current value (0-100) */
21
+ value?: number
22
+ /** Maximum value */
23
+ max?: number
24
+ /** Whether the progress is indeterminate */
25
+ isIndeterminate?: boolean
26
+ /** Size of the progress bar */
27
+ size?: ProgressSize
28
+ /** Color of the progress */
29
+ color?: ProgressColor
30
+ /** Whether to show the value label */
31
+ showValue?: boolean
32
+ /** Custom value format function */
33
+ formatValue?: (value: number, max: number) => string
34
+ /** Label text */
35
+ label?: string
36
+ /** Additional className */
37
+ className?: string
38
+ }
39
+
40
+ const sizeStyles: Record<ProgressSize, string> = {
41
+ sm: 'h-1',
42
+ md: 'h-2',
43
+ lg: 'h-3',
44
+ }
45
+
46
+ const colorStyles: Record<ProgressColor, string> = {
47
+ primary: 'bg-brand-primary',
48
+ secondary: 'bg-brand-secondary',
49
+ success: 'bg-status-success',
50
+ error: 'bg-status-error',
51
+ warning: 'bg-status-warning',
52
+ info: 'bg-status-info',
53
+ }
54
+
55
+ const trackColorStyles: Record<ProgressColor, string> = {
56
+ primary: 'bg-brand-primary/20',
57
+ secondary: 'bg-brand-secondary/20',
58
+ success: 'bg-status-success/20',
59
+ error: 'bg-status-error/20',
60
+ warning: 'bg-status-warning/20',
61
+ info: 'bg-status-info/20',
62
+ }
63
+
64
+ /**
65
+ * Linear progress bar component.
66
+ *
67
+ * @example
68
+ * // Determinate progress
69
+ * <Progress value={75} />
70
+ *
71
+ * @example
72
+ * // With label and value
73
+ * <Progress value={45} label="Uploading..." showValue />
74
+ *
75
+ * @example
76
+ * // Indeterminate progress
77
+ * <Progress isIndeterminate />
78
+ */
79
+ export function Progress({
80
+ value = 0,
81
+ max = 100,
82
+ isIndeterminate = false,
83
+ size = 'md',
84
+ color = 'primary',
85
+ showValue = false,
86
+ formatValue = (v, m) => `${Math.round((v / m) * 100)}%`,
87
+ label,
88
+ className,
89
+ ...props
90
+ }: ProgressProps) {
91
+ const percentage = Math.min(Math.max((value / max) * 100, 0), 100)
92
+
93
+ return (
94
+ <View className={cn('w-full', className)} {...props}>
95
+ {/* Label and Value Row */}
96
+ {(label || showValue) && (
97
+ <View className="flex-row justify-between items-center mb-1">
98
+ {label && (
99
+ <Text className="text-sm text-text-secondary">{label}</Text>
100
+ )}
101
+ {showValue && !isIndeterminate && (
102
+ <Text className="text-sm font-medium text-text-primary">
103
+ {formatValue(value, max)}
104
+ </Text>
105
+ )}
106
+ </View>
107
+ )}
108
+
109
+ {/* Progress Track */}
110
+ <View
111
+ className={cn(
112
+ 'w-full rounded-full overflow-hidden',
113
+ trackColorStyles[color],
114
+ sizeStyles[size]
115
+ )}
116
+ accessibilityRole="progressbar"
117
+ accessibilityValue={{
118
+ min: 0,
119
+ max: max,
120
+ now: isIndeterminate ? undefined : value,
121
+ }}
122
+ >
123
+ {/* Progress Fill */}
124
+ <View
125
+ className={cn(
126
+ 'h-full rounded-full',
127
+ colorStyles[color],
128
+ isIndeterminate && 'animate-pulse w-1/3'
129
+ )}
130
+ style={isIndeterminate ? undefined : { width: `${percentage}%` }}
131
+ />
132
+ </View>
133
+ </View>
134
+ )
135
+ }
136
+
137
+ export interface CircularProgressProps extends ViewProps {
138
+ /** Current value (0-100) */
139
+ value?: number
140
+ /** Maximum value */
141
+ max?: number
142
+ /** Whether the progress is indeterminate */
143
+ isIndeterminate?: boolean
144
+ /** Size of the progress circle (diameter) */
145
+ size?: number
146
+ /** Stroke width */
147
+ strokeWidth?: number
148
+ /** Color of the progress */
149
+ color?: ProgressColor
150
+ /** Whether to show the value in center */
151
+ showValue?: boolean
152
+ /** Custom value format function */
153
+ formatValue?: (value: number, max: number) => string
154
+ /** Additional className */
155
+ className?: string
156
+ }
157
+
158
+ /**
159
+ * Circular progress indicator component.
160
+ *
161
+ * @example
162
+ * <CircularProgress value={75} showValue />
163
+ */
164
+ export function CircularProgress({
165
+ value = 0,
166
+ max = 100,
167
+ isIndeterminate = false,
168
+ size = 48,
169
+ strokeWidth = 4,
170
+ color = 'primary',
171
+ showValue = false,
172
+ formatValue = (v, m) => `${Math.round((v / m) * 100)}%`,
173
+ className,
174
+ ...props
175
+ }: CircularProgressProps) {
176
+ const percentage = Math.min(Math.max((value / max) * 100, 0), 100)
177
+ const radius = (size - strokeWidth) / 2
178
+ const circumference = 2 * Math.PI * radius
179
+ const strokeDashoffset = circumference - (percentage / 100) * circumference
180
+
181
+ // For web, we use SVG. For native, we use a simplified view-based approach.
182
+ return (
183
+ <View
184
+ className={cn('items-center justify-center', className)}
185
+ style={{ width: size, height: size }}
186
+ accessibilityRole="progressbar"
187
+ accessibilityValue={{
188
+ min: 0,
189
+ max: max,
190
+ now: isIndeterminate ? undefined : value,
191
+ }}
192
+ {...props}
193
+ >
194
+ {/* Background circle */}
195
+ <View
196
+ className={cn(
197
+ 'absolute rounded-full border-4',
198
+ 'border-border-default'
199
+ )}
200
+ style={{
201
+ width: size,
202
+ height: size,
203
+ borderWidth: strokeWidth,
204
+ }}
205
+ />
206
+
207
+ {/* Progress arc (simplified) */}
208
+ <View
209
+ className={cn(
210
+ 'absolute rounded-full',
211
+ colorStyles[color],
212
+ isIndeterminate && 'animate-spin'
213
+ )}
214
+ style={{
215
+ width: size,
216
+ height: size,
217
+ borderWidth: strokeWidth,
218
+ borderColor: 'transparent',
219
+ borderTopColor: colorVarMap[color],
220
+ borderRightColor: percentage > 25 ? colorVarMap[color] : 'transparent',
221
+ borderBottomColor: percentage > 50 ? colorVarMap[color] : 'transparent',
222
+ borderLeftColor: percentage > 75 ? colorVarMap[color] : 'transparent',
223
+ }}
224
+ />
225
+
226
+ {/* Center value */}
227
+ {showValue && !isIndeterminate && (
228
+ <Text className="text-xs font-semibold text-text-primary">
229
+ {formatValue(value, max)}
230
+ </Text>
231
+ )}
232
+ </View>
233
+ )
234
+ }
235
+
236
+ export interface ProgressStepsProps extends ViewProps {
237
+ /** Current step (0-indexed) */
238
+ currentStep: number
239
+ /** Total number of steps */
240
+ totalSteps: number
241
+ /** Color of completed steps */
242
+ color?: ProgressColor
243
+ /** Size of step indicators */
244
+ size?: ProgressSize
245
+ /** Labels for each step */
246
+ labels?: string[]
247
+ /** Additional className */
248
+ className?: string
249
+ }
250
+
251
+ const stepSizeStyles: Record<ProgressSize, { dot: string; connector: string }> = {
252
+ sm: { dot: 'w-2 h-2', connector: 'h-0.5' },
253
+ md: { dot: 'w-3 h-3', connector: 'h-1' },
254
+ lg: { dot: 'w-4 h-4', connector: 'h-1.5' },
255
+ }
256
+
257
+ /**
258
+ * Step-based progress indicator.
259
+ *
260
+ * @example
261
+ * <ProgressSteps currentStep={2} totalSteps={4} labels={['Cart', 'Shipping', 'Payment', 'Done']} />
262
+ */
263
+ export function ProgressSteps({
264
+ currentStep,
265
+ totalSteps,
266
+ color = 'primary',
267
+ size = 'md',
268
+ labels,
269
+ className,
270
+ ...props
271
+ }: ProgressStepsProps) {
272
+ const { dot, connector } = stepSizeStyles[size]
273
+
274
+ return (
275
+ <View className={cn('w-full', className)} {...props}>
276
+ <View className="flex-row items-center">
277
+ {Array.from({ length: totalSteps }).map((_, index) => (
278
+ <React.Fragment key={index}>
279
+ {/* Step dot */}
280
+ <View
281
+ className={cn(
282
+ 'rounded-full',
283
+ dot,
284
+ index <= currentStep ? colorStyles[color] : 'bg-border-strong'
285
+ )}
286
+ />
287
+
288
+ {/* Connector line */}
289
+ {index < totalSteps - 1 && (
290
+ <View
291
+ className={cn(
292
+ 'flex-1 mx-1',
293
+ connector,
294
+ index < currentStep ? colorStyles[color] : 'bg-border-strong'
295
+ )}
296
+ />
297
+ )}
298
+ </React.Fragment>
299
+ ))}
300
+ </View>
301
+
302
+ {/* Labels */}
303
+ {labels && labels.length > 0 && (
304
+ <View className="flex-row mt-2">
305
+ {labels.map((label, index) => (
306
+ <Text
307
+ key={index}
308
+ className={cn(
309
+ 'flex-1 text-xs text-center',
310
+ index <= currentStep ? 'text-text-primary font-medium' : 'text-text-tertiary'
311
+ )}
312
+ >
313
+ {label}
314
+ </Text>
315
+ ))}
316
+ </View>
317
+ )}
318
+ </View>
319
+ )
320
+ }
@@ -0,0 +1,9 @@
1
+ export { Progress, CircularProgress, ProgressSteps } from './Progress'
2
+ export type {
3
+ ProgressProps,
4
+ CircularProgressProps,
5
+ ProgressStepsProps,
6
+ ProgressSize,
7
+ ProgressColor,
8
+ ProgressVariant,
9
+ } from './Progress'