@titan-design/react-ui 0.1.0 → 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 (192) hide show
  1. package/package.json +10 -5
  2. package/src/components/custom/DateTime/DateTime.stories.tsx +180 -0
  3. package/src/components/custom/DateTime/DateTime.test.tsx +152 -0
  4. package/src/components/custom/DateTime/DateTime.tsx +235 -0
  5. package/src/components/custom/DateTime/index.ts +2 -0
  6. package/src/components/custom/EmptyState/EmptyState.stories.tsx +99 -0
  7. package/src/components/custom/EmptyState/EmptyState.test.tsx +106 -0
  8. package/src/components/custom/EmptyState/EmptyState.tsx +68 -0
  9. package/src/components/custom/EmptyState/index.ts +2 -0
  10. package/src/components/custom/Metric/Metric.stories.tsx +133 -0
  11. package/src/components/custom/Metric/Metric.test.tsx +120 -0
  12. package/src/components/custom/Metric/Metric.tsx +92 -0
  13. package/src/components/custom/Metric/index.ts +2 -0
  14. package/src/components/custom/Sidebar/Sidebar.stories.tsx +227 -0
  15. package/src/components/custom/Sidebar/Sidebar.test.tsx +185 -0
  16. package/src/components/custom/Sidebar/Sidebar.tsx +257 -0
  17. package/src/components/custom/Sidebar/index.ts +18 -0
  18. package/src/components/custom/Table/Table.stories.tsx +481 -0
  19. package/src/components/custom/Table/Table.test.tsx +531 -0
  20. package/src/components/custom/Table/Table.tsx +696 -0
  21. package/src/components/custom/Table/index.ts +22 -0
  22. package/src/components/custom/Typography/Typography.stories.tsx +153 -0
  23. package/src/components/custom/Typography/Typography.test.tsx +107 -0
  24. package/src/components/custom/Typography/Typography.tsx +218 -0
  25. package/src/components/custom/Typography/index.ts +19 -0
  26. package/src/components/custom/index.ts +8 -0
  27. package/src/components/custom/stepper/Stepper.stories.tsx +245 -0
  28. package/src/components/custom/stepper/Stepper.test.tsx +230 -0
  29. package/src/components/custom/stepper/Stepper.tsx +250 -0
  30. package/src/components/custom/stepper/index.ts +10 -0
  31. package/src/components/index.ts +5 -0
  32. package/src/components/ui/alert/Alert.stories.tsx +154 -0
  33. package/src/components/ui/alert/Alert.test.tsx +202 -0
  34. package/src/components/ui/alert/Alert.tsx +170 -0
  35. package/src/components/ui/alert/index.ts +8 -0
  36. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +299 -0
  37. package/src/components/ui/autocomplete/Autocomplete.test.tsx +247 -0
  38. package/src/components/ui/autocomplete/Autocomplete.tsx +332 -0
  39. package/src/components/ui/autocomplete/index.ts +2 -0
  40. package/src/components/ui/avatar/Avatar.stories.tsx +105 -0
  41. package/src/components/ui/avatar/Avatar.test.tsx +135 -0
  42. package/src/components/ui/avatar/Avatar.tsx +168 -0
  43. package/src/components/ui/avatar/index.ts +2 -0
  44. package/src/components/ui/badge/Badge.stories.tsx +100 -0
  45. package/src/components/ui/badge/Badge.test.tsx +86 -0
  46. package/src/components/ui/badge/Badge.tsx +105 -0
  47. package/src/components/ui/badge/index.ts +2 -0
  48. package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +70 -0
  49. package/src/components/ui/breadcrumbs/Breadcrumbs.test.tsx +156 -0
  50. package/src/components/ui/breadcrumbs/Breadcrumbs.tsx +127 -0
  51. package/src/components/ui/breadcrumbs/index.ts +2 -0
  52. package/src/components/ui/button/Button.stories.tsx +216 -0
  53. package/src/components/ui/button/Button.test.tsx +132 -0
  54. package/src/components/ui/button/Button.tsx +242 -0
  55. package/src/components/ui/button/index.ts +10 -0
  56. package/src/components/ui/card/Card.stories.tsx +443 -0
  57. package/src/components/ui/card/Card.test.tsx +265 -0
  58. package/src/components/ui/card/Card.tsx +380 -0
  59. package/src/components/ui/card/index.ts +20 -0
  60. package/src/components/ui/checkbox/Checkbox.stories.tsx +126 -0
  61. package/src/components/ui/checkbox/Checkbox.test.tsx +152 -0
  62. package/src/components/ui/checkbox/Checkbox.tsx +163 -0
  63. package/src/components/ui/checkbox/index.ts +2 -0
  64. package/src/components/ui/chip/Chip.stories.tsx +112 -0
  65. package/src/components/ui/chip/Chip.test.tsx +119 -0
  66. package/src/components/ui/chip/Chip.tsx +151 -0
  67. package/src/components/ui/chip/index.ts +2 -0
  68. package/src/components/ui/collapse/Collapse.stories.tsx +313 -0
  69. package/src/components/ui/collapse/Collapse.test.tsx +298 -0
  70. package/src/components/ui/collapse/Collapse.tsx +275 -0
  71. package/src/components/ui/collapse/index.ts +18 -0
  72. package/src/components/ui/data-row/DataRow.stories.tsx +78 -0
  73. package/src/components/ui/data-row/DataRow.test.tsx +78 -0
  74. package/src/components/ui/data-row/DataRow.tsx +29 -0
  75. package/src/components/ui/data-row/index.ts +2 -0
  76. package/src/components/ui/divider/Divider.stories.tsx +71 -0
  77. package/src/components/ui/divider/Divider.test.tsx +55 -0
  78. package/src/components/ui/divider/Divider.tsx +37 -0
  79. package/src/components/ui/divider/index.ts +2 -0
  80. package/src/components/ui/drawer/Drawer.stories.tsx +228 -0
  81. package/src/components/ui/drawer/Drawer.test.tsx +142 -0
  82. package/src/components/ui/drawer/Drawer.tsx +228 -0
  83. package/src/components/ui/drawer/index.ts +9 -0
  84. package/src/components/ui/form-field/FormField.stories.tsx +260 -0
  85. package/src/components/ui/form-field/FormField.test.tsx +280 -0
  86. package/src/components/ui/form-field/FormField.tsx +308 -0
  87. package/src/components/ui/form-field/index.ts +2 -0
  88. package/src/components/ui/help-tip/HelpTip.stories.tsx +250 -0
  89. package/src/components/ui/help-tip/HelpTip.test.tsx +198 -0
  90. package/src/components/ui/help-tip/HelpTip.tsx +229 -0
  91. package/src/components/ui/help-tip/index.ts +2 -0
  92. package/src/components/ui/icon-box/IconBox.stories.tsx +139 -0
  93. package/src/components/ui/icon-box/IconBox.test.tsx +120 -0
  94. package/src/components/ui/icon-box/IconBox.tsx +57 -0
  95. package/src/components/ui/icon-box/index.ts +1 -0
  96. package/src/components/ui/index.ts +36 -0
  97. package/src/components/ui/input/Input.stories.tsx +192 -0
  98. package/src/components/ui/input/Input.test.tsx +86 -0
  99. package/src/components/ui/input/Input.tsx +273 -0
  100. package/src/components/ui/input/index.ts +2 -0
  101. package/src/components/ui/link/Link.stories.tsx +78 -0
  102. package/src/components/ui/link/Link.test.tsx +94 -0
  103. package/src/components/ui/link/Link.tsx +86 -0
  104. package/src/components/ui/link/index.ts +2 -0
  105. package/src/components/ui/list-item/ListItem.stories.tsx +146 -0
  106. package/src/components/ui/list-item/ListItem.test.tsx +128 -0
  107. package/src/components/ui/list-item/ListItem.tsx +89 -0
  108. package/src/components/ui/list-item/index.ts +14 -0
  109. package/src/components/ui/menu/Menu.stories.tsx +180 -0
  110. package/src/components/ui/menu/Menu.test.tsx +353 -0
  111. package/src/components/ui/menu/Menu.tsx +222 -0
  112. package/src/components/ui/menu/index.ts +2 -0
  113. package/src/components/ui/modal/Modal.stories.tsx +350 -0
  114. package/src/components/ui/modal/Modal.test.tsx +159 -0
  115. package/src/components/ui/modal/Modal.tsx +254 -0
  116. package/src/components/ui/modal/index.ts +19 -0
  117. package/src/components/ui/popover/Popover.stories.tsx +190 -0
  118. package/src/components/ui/popover/Popover.test.tsx +277 -0
  119. package/src/components/ui/popover/Popover.tsx +162 -0
  120. package/src/components/ui/popover/index.ts +2 -0
  121. package/src/components/ui/progress/Progress.stories.tsx +190 -0
  122. package/src/components/ui/progress/Progress.test.tsx +181 -0
  123. package/src/components/ui/progress/Progress.tsx +320 -0
  124. package/src/components/ui/progress/index.ts +9 -0
  125. package/src/components/ui/radio/Radio.stories.tsx +199 -0
  126. package/src/components/ui/radio/Radio.test.tsx +166 -0
  127. package/src/components/ui/radio/Radio.tsx +200 -0
  128. package/src/components/ui/radio/index.ts +2 -0
  129. package/src/components/ui/section/Section.stories.tsx +111 -0
  130. package/src/components/ui/section/Section.test.tsx +121 -0
  131. package/src/components/ui/section/Section.tsx +52 -0
  132. package/src/components/ui/section/index.ts +6 -0
  133. package/src/components/ui/select/Select.stories.tsx +286 -0
  134. package/src/components/ui/select/Select.test.tsx +180 -0
  135. package/src/components/ui/select/Select.tsx +236 -0
  136. package/src/components/ui/select/index.ts +2 -0
  137. package/src/components/ui/skeleton/Skeleton.stories.tsx +153 -0
  138. package/src/components/ui/skeleton/Skeleton.test.tsx +162 -0
  139. package/src/components/ui/skeleton/Skeleton.tsx +229 -0
  140. package/src/components/ui/skeleton/index.ts +16 -0
  141. package/src/components/ui/spinner/Spinner.stories.tsx +67 -0
  142. package/src/components/ui/spinner/Spinner.test.tsx +67 -0
  143. package/src/components/ui/spinner/Spinner.tsx +72 -0
  144. package/src/components/ui/spinner/index.ts +2 -0
  145. package/src/components/ui/stack/Stack.stories.tsx +127 -0
  146. package/src/components/ui/stack/Stack.test.tsx +184 -0
  147. package/src/components/ui/stack/Stack.tsx +76 -0
  148. package/src/components/ui/stack/index.ts +2 -0
  149. package/src/components/ui/surface/Surface.stories.tsx +91 -0
  150. package/src/components/ui/surface/Surface.test.tsx +63 -0
  151. package/src/components/ui/surface/Surface.tsx +51 -0
  152. package/src/components/ui/surface/index.ts +1 -0
  153. package/src/components/ui/switch/Switch.stories.tsx +111 -0
  154. package/src/components/ui/switch/Switch.test.tsx +112 -0
  155. package/src/components/ui/switch/Switch.tsx +103 -0
  156. package/src/components/ui/switch/index.ts +2 -0
  157. package/src/components/ui/tabs/Tabs.stories.tsx +213 -0
  158. package/src/components/ui/tabs/Tabs.test.tsx +192 -0
  159. package/src/components/ui/tabs/Tabs.tsx +253 -0
  160. package/src/components/ui/tabs/index.ts +16 -0
  161. package/src/components/ui/toast/Toast.stories.tsx +255 -0
  162. package/src/components/ui/toast/Toast.test.tsx +207 -0
  163. package/src/components/ui/toast/Toast.tsx +327 -0
  164. package/src/components/ui/toast/index.ts +8 -0
  165. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +415 -0
  166. package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +224 -0
  167. package/src/components/ui/toolbar-button/ToolbarButton.tsx +307 -0
  168. package/src/components/ui/toolbar-button/index.ts +7 -0
  169. package/src/components/ui/tooltip/Tooltip.stories.tsx +126 -0
  170. package/src/components/ui/tooltip/Tooltip.test.tsx +179 -0
  171. package/src/components/ui/tooltip/Tooltip.tsx +143 -0
  172. package/src/components/ui/tooltip/index.ts +2 -0
  173. package/src/index.ts +10 -0
  174. package/src/test/setup.ts +19 -0
  175. package/src/theme/Colors.stories.tsx +258 -0
  176. package/src/theme/config.ts +193 -0
  177. package/src/theme/elevation.stories.tsx +346 -0
  178. package/src/theme/elevation.test.ts +27 -0
  179. package/src/theme/elevation.ts +567 -0
  180. package/src/theme/index.ts +39 -0
  181. package/src/theme/shadows.stories.tsx +523 -0
  182. package/src/theme/shadows.ts +439 -0
  183. package/src/theme/tokens/index.ts +2 -0
  184. package/src/theme/tokens/primitives.ts +354 -0
  185. package/src/theme/tokens/semantic.ts +381 -0
  186. package/src/types/jest-axe.d.ts +16 -0
  187. package/src/types/nativewind.d.ts +54 -0
  188. package/src/utils/cn.ts +14 -0
  189. package/src/utils/colors.ts +140 -0
  190. package/src/utils/form.ts +188 -0
  191. package/src/utils/index.ts +29 -0
  192. package/src/utils/useTheme.ts +72 -0
@@ -0,0 +1,207 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { render, screen, fireEvent, act } from '@testing-library/react'
3
+ import { axe } from 'jest-axe'
4
+ import { Toast, ToastProvider, useToast } from './Toast'
5
+
6
+ describe('Toast (standalone)', () => {
7
+ it('renders with title', () => {
8
+ render(<Toast title="Success!" />)
9
+ expect(screen.getByText('Success!')).toBeInTheDocument()
10
+ })
11
+
12
+ it('renders with description', () => {
13
+ render(<Toast title="Saved" description="Your changes have been saved." />)
14
+ expect(screen.getByText('Your changes have been saved.')).toBeInTheDocument()
15
+ })
16
+
17
+ it('renders without description', () => {
18
+ render(<Toast title="Done" />)
19
+ expect(screen.getByText('Done')).toBeInTheDocument()
20
+ expect(screen.queryByText('Your changes')).not.toBeInTheDocument()
21
+ })
22
+
23
+ describe('statuses', () => {
24
+ const statuses = ['success', 'error', 'warning', 'info'] as const
25
+ statuses.forEach((status) => {
26
+ it(`renders with status ${status}`, () => {
27
+ render(<Toast title={`${status} toast`} status={status} />)
28
+ expect(screen.getByText(`${status} toast`)).toBeInTheDocument()
29
+ })
30
+ })
31
+ })
32
+
33
+ it('renders with default info status', () => {
34
+ render(<Toast title="Info toast" />)
35
+ expect(screen.getByRole('alert')).toBeInTheDocument()
36
+ })
37
+
38
+ describe('icon visibility', () => {
39
+ it('shows icon by default', () => {
40
+ render(<Toast title="Test" status="success" />)
41
+ // The checkmark icon is rendered
42
+ expect(screen.getByRole('alert')).toBeInTheDocument()
43
+ })
44
+
45
+ it('hides icon when showIcon is false', () => {
46
+ render(<Toast title="Test" showIcon={false} />)
47
+ expect(screen.getByText('Test')).toBeInTheDocument()
48
+ })
49
+ })
50
+
51
+ describe('close button', () => {
52
+ it('does not show close button by default', () => {
53
+ render(<Toast title="Test" />)
54
+ expect(screen.queryByLabelText('Close toast')).not.toBeInTheDocument()
55
+ })
56
+
57
+ it('shows close button when isClosable and onClose are provided', () => {
58
+ const onClose = vi.fn()
59
+ render(<Toast title="Test" isClosable onClose={onClose} />)
60
+ expect(screen.getByLabelText('Close toast')).toBeInTheDocument()
61
+ })
62
+
63
+ it('calls onClose when close button is pressed', () => {
64
+ const onClose = vi.fn()
65
+ render(<Toast title="Test" isClosable onClose={onClose} />)
66
+
67
+ fireEvent.click(screen.getByLabelText('Close toast'))
68
+ expect(onClose).toHaveBeenCalledTimes(1)
69
+ })
70
+ })
71
+
72
+ describe('accessibility', () => {
73
+ it('has no accessibility violations', async () => {
74
+ const { container } = render(
75
+ <Toast title="Accessible toast" description="Description text" status="success" />
76
+ )
77
+ const results = await axe(container)
78
+ expect(results).toHaveNoViolations()
79
+ })
80
+
81
+ it('has alert role', () => {
82
+ render(<Toast title="Alert" />)
83
+ expect(screen.getByRole('alert')).toBeInTheDocument()
84
+ })
85
+ })
86
+ })
87
+
88
+ describe('ToastProvider', () => {
89
+ function TestConsumer() {
90
+ const { addToast, removeAllToasts } = useToast()
91
+ return (
92
+ <div>
93
+ <button
94
+ onClick={() =>
95
+ addToast({ title: 'New Toast', status: 'success' })
96
+ }
97
+ >
98
+ Add Toast
99
+ </button>
100
+ <button onClick={removeAllToasts}>Clear All</button>
101
+ </div>
102
+ )
103
+ }
104
+
105
+ it('renders children', () => {
106
+ render(
107
+ <ToastProvider>
108
+ <div>App Content</div>
109
+ </ToastProvider>
110
+ )
111
+ expect(screen.getByText('App Content')).toBeInTheDocument()
112
+ })
113
+
114
+ it('adds a toast via useToast hook', () => {
115
+ render(
116
+ <ToastProvider>
117
+ <TestConsumer />
118
+ </ToastProvider>
119
+ )
120
+
121
+ fireEvent.click(screen.getByText('Add Toast'))
122
+ expect(screen.getByText('New Toast')).toBeInTheDocument()
123
+ })
124
+
125
+ it('removes all toasts', () => {
126
+ render(
127
+ <ToastProvider>
128
+ <TestConsumer />
129
+ </ToastProvider>
130
+ )
131
+
132
+ fireEvent.click(screen.getByText('Add Toast'))
133
+ expect(screen.getByText('New Toast')).toBeInTheDocument()
134
+
135
+ fireEvent.click(screen.getByText('Clear All'))
136
+ expect(screen.queryByText('New Toast')).not.toBeInTheDocument()
137
+ })
138
+
139
+ it('respects maxToasts limit', () => {
140
+ render(
141
+ <ToastProvider maxToasts={2}>
142
+ <TestConsumer />
143
+ </ToastProvider>
144
+ )
145
+
146
+ fireEvent.click(screen.getByText('Add Toast'))
147
+ fireEvent.click(screen.getByText('Add Toast'))
148
+ fireEvent.click(screen.getByText('Add Toast'))
149
+
150
+ // Only 2 should be visible
151
+ const toasts = screen.getAllByText('New Toast')
152
+ expect(toasts.length).toBeLessThanOrEqual(2)
153
+ })
154
+
155
+ it('auto-dismisses toasts after duration', () => {
156
+ vi.useFakeTimers()
157
+
158
+ render(
159
+ <ToastProvider defaultDuration={3000}>
160
+ <TestConsumer />
161
+ </ToastProvider>
162
+ )
163
+
164
+ fireEvent.click(screen.getByText('Add Toast'))
165
+ expect(screen.getByText('New Toast')).toBeInTheDocument()
166
+
167
+ act(() => {
168
+ vi.advanceTimersByTime(3000)
169
+ })
170
+
171
+ expect(screen.queryByText('New Toast')).not.toBeInTheDocument()
172
+
173
+ vi.useRealTimers()
174
+ })
175
+
176
+ it('throws when useToast is used outside ToastProvider', () => {
177
+ function BadConsumer() {
178
+ useToast()
179
+ return null
180
+ }
181
+
182
+ expect(() => render(<BadConsumer />)).toThrow(
183
+ 'useToast must be used within a ToastProvider'
184
+ )
185
+ })
186
+
187
+ describe('positions', () => {
188
+ const positions = [
189
+ 'top',
190
+ 'top-right',
191
+ 'top-left',
192
+ 'bottom',
193
+ 'bottom-right',
194
+ 'bottom-left',
195
+ ] as const
196
+ positions.forEach((position) => {
197
+ it(`renders with position ${position}`, () => {
198
+ render(
199
+ <ToastProvider position={position}>
200
+ <div>Content</div>
201
+ </ToastProvider>
202
+ )
203
+ expect(screen.getByText('Content')).toBeInTheDocument()
204
+ })
205
+ })
206
+ })
207
+ })
@@ -0,0 +1,327 @@
1
+ import React, { createContext, useContext, useState, useCallback, useEffect } from 'react'
2
+ import { View, Text, Pressable, type ViewProps } from 'react-native'
3
+ import { cn } from '../../../utils/cn'
4
+
5
+ export type ToastStatus = 'success' | 'error' | 'warning' | 'info'
6
+ export type ToastPosition = 'top' | 'top-right' | 'top-left' | 'bottom' | 'bottom-right' | 'bottom-left'
7
+
8
+ export interface ToastConfig {
9
+ /** Unique ID for the toast */
10
+ id: string
11
+ /** Title text */
12
+ title: string
13
+ /** Description text */
14
+ description?: string
15
+ /** Status/type of the toast */
16
+ status?: ToastStatus
17
+ /** Duration in milliseconds (0 = no auto-dismiss) */
18
+ duration?: number
19
+ /** Whether the toast can be dismissed */
20
+ isClosable?: boolean
21
+ /** Callback when toast is closed */
22
+ onClose?: () => void
23
+ }
24
+
25
+ interface ToastContextType {
26
+ toasts: ToastConfig[]
27
+ addToast: (config: Omit<ToastConfig, 'id'>) => string
28
+ removeToast: (id: string) => void
29
+ removeAllToasts: () => void
30
+ }
31
+
32
+ const ToastContext = createContext<ToastContextType | null>(null)
33
+
34
+ let toastIdCounter = 0
35
+
36
+ export interface ToastProviderProps {
37
+ /** Position of toasts on screen */
38
+ position?: ToastPosition
39
+ /** Default duration for toasts */
40
+ defaultDuration?: number
41
+ /** Maximum number of visible toasts */
42
+ maxToasts?: number
43
+ children?: React.ReactNode
44
+ }
45
+
46
+ const positionStyles: Record<ToastPosition, string> = {
47
+ 'top': 'top-4 left-1/2 -translate-x-1/2',
48
+ 'top-right': 'top-4 right-4',
49
+ 'top-left': 'top-4 left-4',
50
+ 'bottom': 'bottom-4 left-1/2 -translate-x-1/2',
51
+ 'bottom-right': 'bottom-4 right-4',
52
+ 'bottom-left': 'bottom-4 left-4',
53
+ }
54
+
55
+ /**
56
+ * Provider component for the toast notification system.
57
+ *
58
+ * @example
59
+ * // Wrap your app with ToastProvider
60
+ * <ToastProvider position="top-right">
61
+ * <App />
62
+ * </ToastProvider>
63
+ *
64
+ * // Then use the useToast hook in any component
65
+ * const { addToast } = useToast()
66
+ * addToast({ title: 'Success!', status: 'success' })
67
+ */
68
+ export function ToastProvider({
69
+ position = 'top-right',
70
+ defaultDuration = 5000,
71
+ maxToasts = 5,
72
+ children,
73
+ }: ToastProviderProps) {
74
+ const [toasts, setToasts] = useState<ToastConfig[]>([])
75
+
76
+ const addToast = useCallback((config: Omit<ToastConfig, 'id'>) => {
77
+ const id = `toast-${++toastIdCounter}`
78
+ const newToast: ToastConfig = {
79
+ id,
80
+ duration: defaultDuration,
81
+ isClosable: true,
82
+ status: 'info',
83
+ ...config,
84
+ }
85
+
86
+ setToasts((prev) => {
87
+ const updated = [newToast, ...prev]
88
+ return updated.slice(0, maxToasts)
89
+ })
90
+
91
+ return id
92
+ }, [defaultDuration, maxToasts])
93
+
94
+ const removeToast = useCallback((id: string) => {
95
+ setToasts((prev) => prev.filter((t) => t.id !== id))
96
+ }, [])
97
+
98
+ const removeAllToasts = useCallback(() => {
99
+ setToasts([])
100
+ }, [])
101
+
102
+ return (
103
+ <ToastContext.Provider value={{ toasts, addToast, removeToast, removeAllToasts }}>
104
+ {children}
105
+ {/* Toast Container */}
106
+ <View
107
+ className={cn(
108
+ 'fixed z-[9999] flex flex-col gap-2',
109
+ positionStyles[position]
110
+ )}
111
+ pointerEvents="box-none"
112
+ >
113
+ {toasts.map((toast) => (
114
+ <ToastItem
115
+ key={toast.id}
116
+ {...toast}
117
+ onClose={() => {
118
+ removeToast(toast.id)
119
+ toast.onClose?.()
120
+ }}
121
+ />
122
+ ))}
123
+ </View>
124
+ </ToastContext.Provider>
125
+ )
126
+ }
127
+
128
+ /**
129
+ * Hook to access toast functions.
130
+ *
131
+ * @example
132
+ * const { addToast, removeAllToasts } = useToast()
133
+ *
134
+ * // Show a success toast
135
+ * addToast({
136
+ * title: 'Saved!',
137
+ * description: 'Your changes have been saved.',
138
+ * status: 'success'
139
+ * })
140
+ */
141
+ export function useToast() {
142
+ const context = useContext(ToastContext)
143
+ if (!context) {
144
+ throw new Error('useToast must be used within a ToastProvider')
145
+ }
146
+ return context
147
+ }
148
+
149
+ interface ToastItemProps extends ToastConfig {
150
+ onClose: () => void
151
+ }
152
+
153
+ const statusStyles: Record<ToastStatus, { bg: string; border: string; icon: string }> = {
154
+ success: {
155
+ bg: 'bg-status-success/10',
156
+ border: 'border-status-success',
157
+ icon: '✓',
158
+ },
159
+ error: {
160
+ bg: 'bg-status-error/10',
161
+ border: 'border-status-error',
162
+ icon: '✕',
163
+ },
164
+ warning: {
165
+ bg: 'bg-status-warning/10',
166
+ border: 'border-status-warning',
167
+ icon: '⚠',
168
+ },
169
+ info: {
170
+ bg: 'bg-status-info/10',
171
+ border: 'border-status-info',
172
+ icon: 'ℹ',
173
+ },
174
+ }
175
+
176
+ const iconColors: Record<ToastStatus, string> = {
177
+ success: 'text-status-success',
178
+ error: 'text-status-error',
179
+ warning: 'text-status-warning',
180
+ info: 'text-status-info',
181
+ }
182
+
183
+ function ToastItem({
184
+ id,
185
+ title,
186
+ description,
187
+ status = 'info',
188
+ duration = 5000,
189
+ isClosable = true,
190
+ onClose,
191
+ }: ToastItemProps) {
192
+ const { bg, border, icon } = statusStyles[status]
193
+
194
+ // Auto-dismiss
195
+ useEffect(() => {
196
+ if (duration > 0) {
197
+ const timer = setTimeout(onClose, duration)
198
+ return () => clearTimeout(timer)
199
+ }
200
+ }, [duration, onClose])
201
+
202
+ return (
203
+ <View
204
+ className={cn(
205
+ 'min-w-[280px] max-w-[400px] rounded-lg border-l-4 shadow-lg',
206
+ 'bg-surface-elevated',
207
+ border
208
+ )}
209
+ accessibilityRole="alert"
210
+ >
211
+ <View className={cn('flex-row items-start p-3', bg)}>
212
+ {/* Icon */}
213
+ <View className={cn('w-6 h-6 items-center justify-center mr-3')}>
214
+ <Text className={cn('text-lg', iconColors[status])}>{icon}</Text>
215
+ </View>
216
+
217
+ {/* Content */}
218
+ <View className="flex-1">
219
+ <Text className="text-sm font-semibold text-text-primary">
220
+ {title}
221
+ </Text>
222
+ {description && (
223
+ <Text className="text-sm text-text-secondary mt-0.5">
224
+ {description}
225
+ </Text>
226
+ )}
227
+ </View>
228
+
229
+ {/* Close button */}
230
+ {isClosable && (
231
+ <Pressable
232
+ onPress={onClose}
233
+ className="p-1 -m-1 rounded web:hover:bg-interactive-hover"
234
+ accessibilityLabel="Close toast"
235
+ >
236
+ <Text className="text-text-tertiary text-lg">×</Text>
237
+ </Pressable>
238
+ )}
239
+ </View>
240
+ </View>
241
+ )
242
+ }
243
+
244
+ export interface ToastProps extends ViewProps {
245
+ /** Title text */
246
+ title: string
247
+ /** Description text */
248
+ description?: string
249
+ /** Status/type of the toast */
250
+ status?: ToastStatus
251
+ /** Whether to show the icon */
252
+ showIcon?: boolean
253
+ /** Whether the toast can be dismissed */
254
+ isClosable?: boolean
255
+ /** Callback when close button is clicked */
256
+ onClose?: () => void
257
+ /** Additional className */
258
+ className?: string
259
+ }
260
+
261
+ /**
262
+ * Standalone Toast component (for static rendering without provider).
263
+ *
264
+ * @example
265
+ * <Toast
266
+ * title="Success!"
267
+ * description="Your changes have been saved."
268
+ * status="success"
269
+ * />
270
+ */
271
+ export function Toast({
272
+ title,
273
+ description,
274
+ status = 'info',
275
+ showIcon = true,
276
+ isClosable = false,
277
+ onClose,
278
+ className,
279
+ ...props
280
+ }: ToastProps) {
281
+ const { bg, border, icon } = statusStyles[status]
282
+
283
+ return (
284
+ <View
285
+ className={cn(
286
+ 'min-w-[280px] max-w-[400px] rounded-lg border-l-4 shadow-lg',
287
+ 'bg-surface-elevated',
288
+ border,
289
+ className
290
+ )}
291
+ accessibilityRole="alert"
292
+ {...props}
293
+ >
294
+ <View className={cn('flex-row items-start p-3', bg)}>
295
+ {/* Icon */}
296
+ {showIcon && (
297
+ <View className="w-6 h-6 items-center justify-center mr-3">
298
+ <Text className={cn('text-lg', iconColors[status])}>{icon}</Text>
299
+ </View>
300
+ )}
301
+
302
+ {/* Content */}
303
+ <View className="flex-1">
304
+ <Text className="text-sm font-semibold text-text-primary">
305
+ {title}
306
+ </Text>
307
+ {description && (
308
+ <Text className="text-sm text-text-secondary mt-0.5">
309
+ {description}
310
+ </Text>
311
+ )}
312
+ </View>
313
+
314
+ {/* Close button */}
315
+ {isClosable && onClose && (
316
+ <Pressable
317
+ onPress={onClose}
318
+ className="p-1 -m-1 rounded web:hover:bg-interactive-hover"
319
+ accessibilityLabel="Close toast"
320
+ >
321
+ <Text className="text-text-tertiary text-lg">×</Text>
322
+ </Pressable>
323
+ )}
324
+ </View>
325
+ </View>
326
+ )
327
+ }
@@ -0,0 +1,8 @@
1
+ export { Toast, ToastProvider, useToast } from './Toast'
2
+ export type {
3
+ ToastProps,
4
+ ToastProviderProps,
5
+ ToastConfig,
6
+ ToastStatus,
7
+ ToastPosition,
8
+ } from './Toast'