@titan-design/react-ui 0.1.0 → 0.2.5

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 (236) hide show
  1. package/README.md +47 -1
  2. package/dist/{chunk-MGW37MPO.mjs → chunk-UXVRPOME.mjs} +173 -3
  3. package/dist/chunk-UXVRPOME.mjs.map +1 -0
  4. package/dist/index.d.mts +101 -12
  5. package/dist/index.d.ts +101 -12
  6. package/dist/index.js +1267 -468
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +734 -112
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/theme/index.d.mts +86 -1
  11. package/dist/theme/index.d.ts +86 -1
  12. package/dist/theme/index.js +166 -0
  13. package/dist/theme/index.js.map +1 -1
  14. package/dist/theme/index.mjs +1 -1
  15. package/package.json +22 -6
  16. package/src/components/custom/DateTime/DateTime.stories.tsx +180 -0
  17. package/src/components/custom/DateTime/DateTime.test.tsx +152 -0
  18. package/src/components/custom/DateTime/DateTime.tsx +235 -0
  19. package/src/components/custom/DateTime/index.ts +2 -0
  20. package/src/components/custom/EmptyState/EmptyState.stories.tsx +99 -0
  21. package/src/components/custom/EmptyState/EmptyState.test.tsx +106 -0
  22. package/src/components/custom/EmptyState/EmptyState.tsx +68 -0
  23. package/src/components/custom/EmptyState/index.ts +2 -0
  24. package/src/components/custom/Metric/Metric.stories.tsx +133 -0
  25. package/src/components/custom/Metric/Metric.test.tsx +120 -0
  26. package/src/components/custom/Metric/Metric.tsx +92 -0
  27. package/src/components/custom/Metric/index.ts +2 -0
  28. package/src/components/custom/Sidebar/Sidebar.stories.tsx +227 -0
  29. package/src/components/custom/Sidebar/Sidebar.test.tsx +185 -0
  30. package/src/components/custom/Sidebar/Sidebar.tsx +257 -0
  31. package/src/components/custom/Sidebar/index.ts +18 -0
  32. package/src/components/custom/Table/Table.stories.tsx +481 -0
  33. package/src/components/custom/Table/Table.test.tsx +531 -0
  34. package/src/components/custom/Table/Table.tsx +696 -0
  35. package/src/components/custom/Table/index.ts +22 -0
  36. package/src/components/custom/Typography/Typography.stories.tsx +153 -0
  37. package/src/components/custom/Typography/Typography.test.tsx +107 -0
  38. package/src/components/custom/Typography/Typography.tsx +218 -0
  39. package/src/components/custom/Typography/index.ts +19 -0
  40. package/src/components/custom/Workout/RestTimer.stories.tsx +71 -0
  41. package/src/components/custom/Workout/RestTimer.test.tsx +145 -0
  42. package/src/components/custom/Workout/RestTimer.tsx +172 -0
  43. package/src/components/custom/Workout/SupersetWrapper.stories.tsx +144 -0
  44. package/src/components/custom/Workout/SupersetWrapper.test.tsx +130 -0
  45. package/src/components/custom/Workout/SupersetWrapper.tsx +63 -0
  46. package/src/components/custom/Workout/index.ts +2 -0
  47. package/src/components/custom/index.ts +9 -0
  48. package/src/components/custom/stepper/Stepper.stories.tsx +245 -0
  49. package/src/components/custom/stepper/Stepper.test.tsx +230 -0
  50. package/src/components/custom/stepper/Stepper.tsx +250 -0
  51. package/src/components/custom/stepper/index.ts +10 -0
  52. package/src/components/index.ts +5 -0
  53. package/src/components/ui/alert/Alert.stories.tsx +154 -0
  54. package/src/components/ui/alert/Alert.test.tsx +202 -0
  55. package/src/components/ui/alert/Alert.tsx +170 -0
  56. package/src/components/ui/alert/index.ts +8 -0
  57. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +299 -0
  58. package/src/components/ui/autocomplete/Autocomplete.test.tsx +247 -0
  59. package/src/components/ui/autocomplete/Autocomplete.tsx +332 -0
  60. package/src/components/ui/autocomplete/index.ts +2 -0
  61. package/src/components/ui/avatar/Avatar.stories.tsx +105 -0
  62. package/src/components/ui/avatar/Avatar.test.tsx +152 -0
  63. package/src/components/ui/avatar/Avatar.tsx +176 -0
  64. package/src/components/ui/avatar/index.ts +2 -0
  65. package/src/components/ui/badge/Badge.stories.tsx +100 -0
  66. package/src/components/ui/badge/Badge.test.tsx +103 -0
  67. package/src/components/ui/badge/Badge.tsx +119 -0
  68. package/src/components/ui/badge/index.ts +3 -0
  69. package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +70 -0
  70. package/src/components/ui/breadcrumbs/Breadcrumbs.test.tsx +156 -0
  71. package/src/components/ui/breadcrumbs/Breadcrumbs.tsx +127 -0
  72. package/src/components/ui/breadcrumbs/index.ts +2 -0
  73. package/src/components/ui/button/Button.stories.tsx +216 -0
  74. package/src/components/ui/button/Button.test.tsx +132 -0
  75. package/src/components/ui/button/Button.tsx +297 -0
  76. package/src/components/ui/button/index.ts +10 -0
  77. package/src/components/ui/card/Card.stories.tsx +443 -0
  78. package/src/components/ui/card/Card.test.tsx +294 -0
  79. package/src/components/ui/card/Card.tsx +401 -0
  80. package/src/components/ui/card/index.ts +20 -0
  81. package/src/components/ui/checkbox/Checkbox.stories.tsx +126 -0
  82. package/src/components/ui/checkbox/Checkbox.test.tsx +152 -0
  83. package/src/components/ui/checkbox/Checkbox.tsx +163 -0
  84. package/src/components/ui/checkbox/index.ts +2 -0
  85. package/src/components/ui/chip/Chip.stories.tsx +112 -0
  86. package/src/components/ui/chip/Chip.test.tsx +119 -0
  87. package/src/components/ui/chip/Chip.tsx +151 -0
  88. package/src/components/ui/chip/index.ts +2 -0
  89. package/src/components/ui/collapse/Collapse.stories.tsx +313 -0
  90. package/src/components/ui/collapse/Collapse.test.tsx +298 -0
  91. package/src/components/ui/collapse/Collapse.tsx +275 -0
  92. package/src/components/ui/collapse/index.ts +18 -0
  93. package/src/components/ui/data-row/DataRow.stories.tsx +78 -0
  94. package/src/components/ui/data-row/DataRow.test.tsx +78 -0
  95. package/src/components/ui/data-row/DataRow.tsx +29 -0
  96. package/src/components/ui/data-row/index.ts +2 -0
  97. package/src/components/ui/divider/Divider.stories.tsx +71 -0
  98. package/src/components/ui/divider/Divider.test.tsx +55 -0
  99. package/src/components/ui/divider/Divider.tsx +37 -0
  100. package/src/components/ui/divider/index.ts +2 -0
  101. package/src/components/ui/drawer/Drawer.stories.tsx +228 -0
  102. package/src/components/ui/drawer/Drawer.test.tsx +142 -0
  103. package/src/components/ui/drawer/Drawer.tsx +228 -0
  104. package/src/components/ui/drawer/index.ts +9 -0
  105. package/src/components/ui/form-field/FormField.stories.tsx +260 -0
  106. package/src/components/ui/form-field/FormField.test.tsx +280 -0
  107. package/src/components/ui/form-field/FormField.tsx +308 -0
  108. package/src/components/ui/form-field/index.ts +2 -0
  109. package/src/components/ui/help-tip/HelpTip.stories.tsx +250 -0
  110. package/src/components/ui/help-tip/HelpTip.test.tsx +198 -0
  111. package/src/components/ui/help-tip/HelpTip.tsx +229 -0
  112. package/src/components/ui/help-tip/index.ts +2 -0
  113. package/src/components/ui/icon-box/IconBox.stories.tsx +139 -0
  114. package/src/components/ui/icon-box/IconBox.test.tsx +120 -0
  115. package/src/components/ui/icon-box/IconBox.tsx +57 -0
  116. package/src/components/ui/icon-box/index.ts +1 -0
  117. package/src/components/ui/index.ts +38 -0
  118. package/src/components/ui/indicator/Indicator.stories.tsx +74 -0
  119. package/src/components/ui/indicator/Indicator.test.tsx +55 -0
  120. package/src/components/ui/indicator/Indicator.tsx +73 -0
  121. package/src/components/ui/indicator/index.ts +2 -0
  122. package/src/components/ui/input/Input.stories.tsx +192 -0
  123. package/src/components/ui/input/Input.test.tsx +86 -0
  124. package/src/components/ui/input/Input.tsx +273 -0
  125. package/src/components/ui/input/index.ts +2 -0
  126. package/src/components/ui/link/Link.stories.tsx +78 -0
  127. package/src/components/ui/link/Link.test.tsx +94 -0
  128. package/src/components/ui/link/Link.tsx +86 -0
  129. package/src/components/ui/link/index.ts +2 -0
  130. package/src/components/ui/list-item/ListItem.stories.tsx +146 -0
  131. package/src/components/ui/list-item/ListItem.test.tsx +128 -0
  132. package/src/components/ui/list-item/ListItem.tsx +89 -0
  133. package/src/components/ui/list-item/index.ts +14 -0
  134. package/src/components/ui/menu/Menu.stories.tsx +180 -0
  135. package/src/components/ui/menu/Menu.test.tsx +353 -0
  136. package/src/components/ui/menu/Menu.tsx +222 -0
  137. package/src/components/ui/menu/index.ts +2 -0
  138. package/src/components/ui/modal/Modal.stories.tsx +350 -0
  139. package/src/components/ui/modal/Modal.test.tsx +159 -0
  140. package/src/components/ui/modal/Modal.tsx +254 -0
  141. package/src/components/ui/modal/index.ts +19 -0
  142. package/src/components/ui/pill/Pill.stories.tsx +71 -0
  143. package/src/components/ui/pill/Pill.test.tsx +69 -0
  144. package/src/components/ui/pill/Pill.tsx +104 -0
  145. package/src/components/ui/pill/index.ts +2 -0
  146. package/src/components/ui/popover/Popover.stories.tsx +190 -0
  147. package/src/components/ui/popover/Popover.test.tsx +419 -0
  148. package/src/components/ui/popover/Popover.tsx +237 -0
  149. package/src/components/ui/popover/index.ts +2 -0
  150. package/src/components/ui/progress/Progress.stories.tsx +190 -0
  151. package/src/components/ui/progress/Progress.test.tsx +210 -0
  152. package/src/components/ui/progress/Progress.tsx +339 -0
  153. package/src/components/ui/progress/index.ts +9 -0
  154. package/src/components/ui/radio/Radio.stories.tsx +199 -0
  155. package/src/components/ui/radio/Radio.test.tsx +166 -0
  156. package/src/components/ui/radio/Radio.tsx +200 -0
  157. package/src/components/ui/radio/index.ts +2 -0
  158. package/src/components/ui/section/Section.stories.tsx +111 -0
  159. package/src/components/ui/section/Section.test.tsx +121 -0
  160. package/src/components/ui/section/Section.tsx +52 -0
  161. package/src/components/ui/section/index.ts +6 -0
  162. package/src/components/ui/select/Select.stories.tsx +286 -0
  163. package/src/components/ui/select/Select.test.tsx +200 -0
  164. package/src/components/ui/select/Select.tsx +243 -0
  165. package/src/components/ui/select/index.ts +2 -0
  166. package/src/components/ui/skeleton/Skeleton.stories.tsx +153 -0
  167. package/src/components/ui/skeleton/Skeleton.test.tsx +162 -0
  168. package/src/components/ui/skeleton/Skeleton.tsx +229 -0
  169. package/src/components/ui/skeleton/index.ts +16 -0
  170. package/src/components/ui/spinner/Spinner.stories.tsx +67 -0
  171. package/src/components/ui/spinner/Spinner.test.tsx +67 -0
  172. package/src/components/ui/spinner/Spinner.tsx +72 -0
  173. package/src/components/ui/spinner/index.ts +2 -0
  174. package/src/components/ui/stack/Stack.stories.tsx +127 -0
  175. package/src/components/ui/stack/Stack.test.tsx +184 -0
  176. package/src/components/ui/stack/Stack.tsx +76 -0
  177. package/src/components/ui/stack/index.ts +2 -0
  178. package/src/components/ui/surface/Surface.stories.tsx +91 -0
  179. package/src/components/ui/surface/Surface.test.tsx +63 -0
  180. package/src/components/ui/surface/Surface.tsx +51 -0
  181. package/src/components/ui/surface/index.ts +1 -0
  182. package/src/components/ui/switch/Switch.stories.tsx +111 -0
  183. package/src/components/ui/switch/Switch.test.tsx +112 -0
  184. package/src/components/ui/switch/Switch.tsx +103 -0
  185. package/src/components/ui/switch/index.ts +2 -0
  186. package/src/components/ui/tabs/Tabs.stories.tsx +213 -0
  187. package/src/components/ui/tabs/Tabs.test.tsx +192 -0
  188. package/src/components/ui/tabs/Tabs.tsx +253 -0
  189. package/src/components/ui/tabs/index.ts +16 -0
  190. package/src/components/ui/toast/Toast.stories.tsx +255 -0
  191. package/src/components/ui/toast/Toast.test.tsx +207 -0
  192. package/src/components/ui/toast/Toast.tsx +327 -0
  193. package/src/components/ui/toast/index.ts +9 -0
  194. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +415 -0
  195. package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +224 -0
  196. package/src/components/ui/toolbar-button/ToolbarButton.tsx +307 -0
  197. package/src/components/ui/toolbar-button/index.ts +7 -0
  198. package/src/components/ui/tooltip/Tooltip.stories.tsx +126 -0
  199. package/src/components/ui/tooltip/Tooltip.test.tsx +262 -0
  200. package/src/components/ui/tooltip/Tooltip.tsx +234 -0
  201. package/src/components/ui/tooltip/index.ts +2 -0
  202. package/src/index.ts +10 -0
  203. package/src/stories/PresetShowcase.stories.tsx +462 -0
  204. package/src/stories/ThemePresets.stories.tsx +85 -0
  205. package/src/test/setup.ts +19 -0
  206. package/src/theme/Colors.stories.tsx +258 -0
  207. package/src/theme/config.ts +193 -0
  208. package/src/theme/elevation.stories.tsx +346 -0
  209. package/src/theme/elevation.test.ts +27 -0
  210. package/src/theme/elevation.ts +567 -0
  211. package/src/theme/global.css +129 -1
  212. package/src/theme/index.ts +40 -0
  213. package/src/theme/presets/apply.ts +97 -0
  214. package/src/theme/presets/audiobook.ts +93 -0
  215. package/src/theme/presets/default.ts +6 -0
  216. package/src/theme/presets/index.ts +4 -0
  217. package/src/theme/presets/presets.test.ts +51 -0
  218. package/src/theme/presets/types.ts +76 -0
  219. package/src/theme/shadows.stories.tsx +523 -0
  220. package/src/theme/shadows.ts +439 -0
  221. package/src/theme/tokens/index.ts +2 -0
  222. package/src/theme/tokens/primitives.ts +354 -0
  223. package/src/theme/tokens/semantic.ts +381 -0
  224. package/src/types/jest-axe.d.ts +16 -0
  225. package/src/types/nativewind.d.ts +54 -0
  226. package/src/utils/avatar-color.test.ts +30 -0
  227. package/src/utils/avatar-color.ts +20 -0
  228. package/src/utils/cn.ts +14 -0
  229. package/src/utils/colors.ts +140 -0
  230. package/src/utils/form.ts +188 -0
  231. package/src/utils/index.ts +29 -0
  232. package/src/utils/useTheme.ts +72 -0
  233. package/src/web-jsx/jsx-dev-runtime.ts +28 -0
  234. package/src/web-jsx/jsx-runtime.ts +41 -0
  235. package/tailwind.config.js +53 -3
  236. package/dist/chunk-MGW37MPO.mjs.map +0 -1
@@ -0,0 +1,222 @@
1
+ import React, { useState, createContext, useContext } from 'react'
2
+ import { View, Text, Pressable, type ViewProps } from 'react-native'
3
+ import { cn } from '../../../utils/cn'
4
+
5
+ interface MenuContextType {
6
+ isOpen: boolean
7
+ setIsOpen: (open: boolean) => void
8
+ closeMenu: () => void
9
+ }
10
+
11
+ const MenuContext = createContext<MenuContextType>({
12
+ isOpen: false,
13
+ setIsOpen: () => {},
14
+ closeMenu: () => {},
15
+ })
16
+
17
+ export interface MenuProps extends ViewProps {
18
+ /** Controlled open state */
19
+ isOpen?: boolean
20
+ /** Callback when open state changes */
21
+ onOpenChange?: (isOpen: boolean) => void
22
+ /** Additional className */
23
+ className?: string
24
+ children?: React.ReactNode
25
+ }
26
+
27
+ /**
28
+ * Menu component for dropdown menus.
29
+ *
30
+ * @example
31
+ * <Menu>
32
+ * <MenuTrigger>
33
+ * <Button><ButtonText>Options</ButtonText></Button>
34
+ * </MenuTrigger>
35
+ * <MenuList>
36
+ * <MenuItem onPress={() => {}}>Edit</MenuItem>
37
+ * <MenuItem onPress={() => {}}>Delete</MenuItem>
38
+ * <MenuDivider />
39
+ * <MenuItem onPress={() => {}} isDestructive>Remove</MenuItem>
40
+ * </MenuList>
41
+ * </Menu>
42
+ */
43
+ export function Menu({
44
+ isOpen: controlledIsOpen,
45
+ onOpenChange,
46
+ className,
47
+ children,
48
+ ...props
49
+ }: MenuProps) {
50
+ const [internalIsOpen, setInternalIsOpen] = useState(false)
51
+ const isOpen = controlledIsOpen ?? internalIsOpen
52
+
53
+ const setIsOpen = (open: boolean) => {
54
+ if (controlledIsOpen === undefined) {
55
+ setInternalIsOpen(open)
56
+ }
57
+ onOpenChange?.(open)
58
+ }
59
+
60
+ const closeMenu = () => setIsOpen(false)
61
+
62
+ return (
63
+ <MenuContext.Provider value={{ isOpen, setIsOpen, closeMenu }}>
64
+ <View className={cn('relative', className)} {...props}>
65
+ {children}
66
+ </View>
67
+ </MenuContext.Provider>
68
+ )
69
+ }
70
+
71
+ export interface MenuTriggerProps {
72
+ children: React.ReactNode
73
+ className?: string
74
+ }
75
+
76
+ /**
77
+ * Trigger element for the menu.
78
+ */
79
+ export function MenuTrigger({ children, className }: MenuTriggerProps) {
80
+ const { isOpen, setIsOpen } = useContext(MenuContext)
81
+
82
+ return (
83
+ <Pressable
84
+ onPress={() => setIsOpen(!isOpen)}
85
+ accessibilityRole="button"
86
+ accessibilityState={{ expanded: isOpen }}
87
+ className={className}
88
+ >
89
+ {children}
90
+ </Pressable>
91
+ )
92
+ }
93
+
94
+ export interface MenuListProps {
95
+ children?: React.ReactNode
96
+ className?: string
97
+ }
98
+
99
+ /**
100
+ * Container for menu items.
101
+ */
102
+ export function MenuList({ children, className }: MenuListProps) {
103
+ const { isOpen, setIsOpen } = useContext(MenuContext)
104
+
105
+ if (!isOpen) return null
106
+
107
+ return (
108
+ <>
109
+ {/* Backdrop */}
110
+ <Pressable
111
+ onPress={() => setIsOpen(false)}
112
+ className="fixed inset-0 z-40"
113
+ style={{ position: 'absolute' }}
114
+ />
115
+ {/* Menu */}
116
+ <View
117
+ className={cn(
118
+ 'absolute z-50 top-full left-0 mt-1 min-w-[160px]',
119
+ 'bg-surface-elevated rounded-lg shadow-lg border border-border-default',
120
+ 'py-1 overflow-hidden',
121
+ className
122
+ )}
123
+ accessibilityRole="menu"
124
+ >
125
+ {children}
126
+ </View>
127
+ </>
128
+ )
129
+ }
130
+
131
+ export interface MenuItemProps {
132
+ /** Press handler */
133
+ onPress?: () => void
134
+ /** Whether the item is disabled */
135
+ isDisabled?: boolean
136
+ /** Whether the item is destructive (shows in red) */
137
+ isDestructive?: boolean
138
+ /** Icon element */
139
+ icon?: React.ReactNode
140
+ /** Additional className */
141
+ className?: string
142
+ children?: React.ReactNode
143
+ }
144
+
145
+ /**
146
+ * Individual menu item.
147
+ */
148
+ export function MenuItem({
149
+ onPress,
150
+ isDisabled = false,
151
+ isDestructive = false,
152
+ icon,
153
+ className,
154
+ children,
155
+ }: MenuItemProps) {
156
+ const { closeMenu } = useContext(MenuContext)
157
+
158
+ const handlePress = () => {
159
+ if (isDisabled) return
160
+ onPress?.()
161
+ closeMenu()
162
+ }
163
+
164
+ return (
165
+ <Pressable
166
+ onPress={handlePress}
167
+ disabled={isDisabled}
168
+ accessibilityRole="menuitem"
169
+ accessibilityState={{ disabled: isDisabled }}
170
+ className={cn(
171
+ 'flex-row items-center px-4 py-2',
172
+ 'web:hover:bg-interactive-hover active:bg-interactive-active',
173
+ isDisabled && 'opacity-50 cursor-not-allowed',
174
+ className
175
+ )}
176
+ >
177
+ {icon && <View className="mr-3">{icon}</View>}
178
+ <Text
179
+ className={cn(
180
+ 'text-sm',
181
+ isDestructive ? 'text-status-error' : 'text-text-primary'
182
+ )}
183
+ >
184
+ {children}
185
+ </Text>
186
+ </Pressable>
187
+ )
188
+ }
189
+
190
+ export interface MenuDividerProps {
191
+ className?: string
192
+ }
193
+
194
+ /**
195
+ * Divider between menu items.
196
+ */
197
+ export function MenuDivider({ className }: MenuDividerProps) {
198
+ return <View className={cn('h-px bg-border-default my-1', className)} />
199
+ }
200
+
201
+ export interface MenuGroupProps {
202
+ /** Group label */
203
+ label?: string
204
+ children?: React.ReactNode
205
+ className?: string
206
+ }
207
+
208
+ /**
209
+ * Group of menu items with optional label.
210
+ */
211
+ export function MenuGroup({ label, children, className }: MenuGroupProps) {
212
+ return (
213
+ <View className={className}>
214
+ {label && (
215
+ <Text className="px-4 py-2 text-xs font-semibold text-text-secondary uppercase">
216
+ {label}
217
+ </Text>
218
+ )}
219
+ {children}
220
+ </View>
221
+ )
222
+ }
@@ -0,0 +1,2 @@
1
+ export { Menu, MenuTrigger, MenuList, MenuItem, MenuDivider, MenuGroup } from './Menu'
2
+ export type { MenuProps, MenuTriggerProps, MenuListProps, MenuItemProps, MenuDividerProps, MenuGroupProps } from './Menu'
@@ -0,0 +1,350 @@
1
+ import React, { useState } from 'react'
2
+ import type { Meta, StoryObj } from '@storybook/react-vite'
3
+ import { View, Text, TextInput } from 'react-native'
4
+ import {
5
+ Modal,
6
+ ModalContent,
7
+ ModalHeader,
8
+ ModalTitle,
9
+ ModalCloseButton,
10
+ ModalBody,
11
+ ModalFooter,
12
+ } from './Modal'
13
+ import { Button, ButtonText } from '../button/Button'
14
+
15
+ const meta: Meta<typeof Modal> = {
16
+ title: 'Components/Modal',
17
+ component: Modal,
18
+ tags: ['autodocs'],
19
+ argTypes: {
20
+ isOpen: {
21
+ control: 'boolean',
22
+ description: 'Whether the modal is open',
23
+ },
24
+ size: {
25
+ control: 'select',
26
+ options: ['xs', 'sm', 'md', 'lg', 'xl', 'full'],
27
+ description: 'Modal size',
28
+ },
29
+ scrollBehavior: {
30
+ control: 'select',
31
+ options: ['inside', 'outside'],
32
+ description: 'Where scrolling happens',
33
+ },
34
+ closeOnOverlayClick: {
35
+ control: 'boolean',
36
+ description: 'Whether clicking backdrop closes modal',
37
+ },
38
+ backdropBlur: {
39
+ control: 'boolean',
40
+ description: 'Whether to blur the backdrop',
41
+ },
42
+ animationType: {
43
+ control: 'select',
44
+ options: ['none', 'fade', 'slide'],
45
+ description: 'Animation type',
46
+ },
47
+ },
48
+ }
49
+
50
+ export default meta
51
+ type Story = StoryObj<typeof Modal>
52
+
53
+ /**
54
+ * Interactive wrapper that manages open state for modal stories.
55
+ */
56
+ function ModalDemo({
57
+ children,
58
+ buttonLabel = 'Open Modal',
59
+ ...modalProps
60
+ }: Omit<React.ComponentProps<typeof Modal>, 'isOpen'> & { buttonLabel?: string }) {
61
+ const [isOpen, setIsOpen] = useState(false)
62
+ return (
63
+ <View>
64
+ <Button variant="solid" color="primary" onPress={() => setIsOpen(true)}>
65
+ <ButtonText>{buttonLabel}</ButtonText>
66
+ </Button>
67
+ <Modal isOpen={isOpen} onClose={() => setIsOpen(false)} {...modalProps}>
68
+ {typeof children === 'function'
69
+ ? (children as (close: () => void) => React.ReactNode)(() => setIsOpen(false))
70
+ : children}
71
+ </Modal>
72
+ </View>
73
+ )
74
+ }
75
+
76
+ export const Default: Story = {
77
+ render: () => (
78
+ <ModalDemo>
79
+ <ModalContent>
80
+ <ModalHeader>
81
+ <ModalTitle>Modal Title</ModalTitle>
82
+ <ModalCloseButton />
83
+ </ModalHeader>
84
+ <ModalBody>
85
+ <Text className="text-text-primary">
86
+ This is the default modal with medium size. It includes a header with title and close
87
+ button, a body with content, and a footer with actions.
88
+ </Text>
89
+ </ModalBody>
90
+ <ModalFooter>
91
+ <Button variant="ghost" color="secondary">
92
+ <ButtonText>Cancel</ButtonText>
93
+ </Button>
94
+ <Button variant="solid" color="primary">
95
+ <ButtonText>Confirm</ButtonText>
96
+ </Button>
97
+ </ModalFooter>
98
+ </ModalContent>
99
+ </ModalDemo>
100
+ ),
101
+ }
102
+
103
+ export const Sizes: Story = {
104
+ render: () => (
105
+ <View style={{ flexDirection: 'row', gap: 8, flexWrap: 'wrap' }}>
106
+ {(['xs', 'sm', 'md', 'lg', 'xl', 'full'] as const).map((size) => (
107
+ <ModalDemo key={size} size={size} buttonLabel={`Size: ${size}`}>
108
+ <ModalContent>
109
+ <ModalHeader>
110
+ <ModalTitle>{size.toUpperCase()} Modal</ModalTitle>
111
+ <ModalCloseButton />
112
+ </ModalHeader>
113
+ <ModalBody>
114
+ <Text className="text-text-primary">
115
+ This modal uses size=&quot;{size}&quot;. The content width is constrained by the
116
+ max-width of the selected size variant.
117
+ </Text>
118
+ </ModalBody>
119
+ <ModalFooter>
120
+ <Button variant="solid" color="primary">
121
+ <ButtonText>Close</ButtonText>
122
+ </Button>
123
+ </ModalFooter>
124
+ </ModalContent>
125
+ </ModalDemo>
126
+ ))}
127
+ </View>
128
+ ),
129
+ }
130
+
131
+ export const WithFormContent: Story = {
132
+ render: () => (
133
+ <ModalDemo size="md" buttonLabel="Open Form Modal">
134
+ {(close: () => void) => (
135
+ <ModalContent>
136
+ <ModalHeader>
137
+ <ModalTitle>Create New Item</ModalTitle>
138
+ <ModalCloseButton />
139
+ </ModalHeader>
140
+ <ModalBody>
141
+ <View style={{ gap: 16 }}>
142
+ <View style={{ gap: 4 }}>
143
+ <Text className="text-sm font-medium text-text-secondary">Name</Text>
144
+ <TextInput
145
+ placeholder="Enter item name"
146
+ className="px-3 py-2 rounded-md border border-border-input bg-surface-base text-text-primary"
147
+ />
148
+ </View>
149
+ <View style={{ gap: 4 }}>
150
+ <Text className="text-sm font-medium text-text-secondary">Description</Text>
151
+ <TextInput
152
+ placeholder="Enter description"
153
+ multiline
154
+ numberOfLines={3}
155
+ className="px-3 py-2 rounded-md border border-border-input bg-surface-base text-text-primary"
156
+ style={{ minHeight: 80, textAlignVertical: 'top' }}
157
+ />
158
+ </View>
159
+ <View style={{ gap: 4 }}>
160
+ <Text className="text-sm font-medium text-text-secondary">Category</Text>
161
+ <TextInput
162
+ placeholder="Enter category"
163
+ className="px-3 py-2 rounded-md border border-border-input bg-surface-base text-text-primary"
164
+ />
165
+ </View>
166
+ </View>
167
+ </ModalBody>
168
+ <ModalFooter>
169
+ <Button variant="ghost" color="secondary" onPress={close}>
170
+ <ButtonText>Cancel</ButtonText>
171
+ </Button>
172
+ <Button variant="solid" color="primary" onPress={close}>
173
+ <ButtonText>Create</ButtonText>
174
+ </Button>
175
+ </ModalFooter>
176
+ </ModalContent>
177
+ )}
178
+ </ModalDemo>
179
+ ),
180
+ }
181
+
182
+ export const ScrollableContent: Story = {
183
+ render: () => (
184
+ <ModalDemo size="md" scrollBehavior="inside" buttonLabel="Scrollable Modal">
185
+ <ModalContent>
186
+ <ModalHeader>
187
+ <ModalTitle>Terms of Service</ModalTitle>
188
+ <ModalCloseButton />
189
+ </ModalHeader>
190
+ <ModalBody maxHeight={300}>
191
+ <View style={{ gap: 12 }}>
192
+ {Array.from({ length: 15 }).map((_, i) => (
193
+ <View key={i}>
194
+ <Text className="text-text-primary font-semibold mb-1">Section {i + 1}</Text>
195
+ <Text className="text-text-secondary text-sm">
196
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor
197
+ incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
198
+ nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
199
+ </Text>
200
+ </View>
201
+ ))}
202
+ </View>
203
+ </ModalBody>
204
+ <ModalFooter>
205
+ <Button variant="ghost" color="secondary">
206
+ <ButtonText>Decline</ButtonText>
207
+ </Button>
208
+ <Button variant="solid" color="primary">
209
+ <ButtonText>Accept</ButtonText>
210
+ </Button>
211
+ </ModalFooter>
212
+ </ModalContent>
213
+ </ModalDemo>
214
+ ),
215
+ }
216
+
217
+ export const BackdropBlur: Story = {
218
+ render: () => (
219
+ <View style={{ gap: 16 }}>
220
+ <Text className="text-text-secondary text-sm">
221
+ The backdrop blur variant uses a lighter overlay with a blur effect behind the modal.
222
+ </Text>
223
+ <ModalDemo backdropBlur buttonLabel="Blurred Backdrop">
224
+ <ModalContent>
225
+ <ModalHeader>
226
+ <ModalTitle>Blurred Backdrop</ModalTitle>
227
+ <ModalCloseButton />
228
+ </ModalHeader>
229
+ <ModalBody>
230
+ <Text className="text-text-primary">
231
+ This modal uses backdropBlur which applies a CSS backdrop-filter blur on web and a
232
+ lighter overlay opacity.
233
+ </Text>
234
+ </ModalBody>
235
+ <ModalFooter>
236
+ <Button variant="solid" color="primary">
237
+ <ButtonText>Got it</ButtonText>
238
+ </Button>
239
+ </ModalFooter>
240
+ </ModalContent>
241
+ </ModalDemo>
242
+ </View>
243
+ ),
244
+ }
245
+
246
+ export const NoOverlayClose: Story = {
247
+ render: () => (
248
+ <ModalDemo closeOnOverlayClick={false} buttonLabel="Persistent Modal">
249
+ <ModalContent>
250
+ <ModalHeader>
251
+ <ModalTitle>Persistent Modal</ModalTitle>
252
+ <ModalCloseButton />
253
+ </ModalHeader>
254
+ <ModalBody>
255
+ <Text className="text-text-primary">
256
+ This modal does not close when clicking the overlay. You must use the close button or
257
+ footer actions to dismiss it. This is useful for important confirmations.
258
+ </Text>
259
+ </ModalBody>
260
+ <ModalFooter>
261
+ <Button variant="solid" color="primary">
262
+ <ButtonText>I Understand</ButtonText>
263
+ </Button>
264
+ </ModalFooter>
265
+ </ModalContent>
266
+ </ModalDemo>
267
+ ),
268
+ }
269
+
270
+ export const DeleteConfirmation: Story = {
271
+ render: () => (
272
+ <ModalDemo size="sm" buttonLabel="Delete Item">
273
+ {(close: () => void) => (
274
+ <ModalContent>
275
+ <ModalHeader>
276
+ <ModalTitle>Delete Item</ModalTitle>
277
+ <ModalCloseButton />
278
+ </ModalHeader>
279
+ <ModalBody>
280
+ <Text className="text-text-primary">
281
+ Are you sure you want to delete this item? This action cannot be undone.
282
+ </Text>
283
+ </ModalBody>
284
+ <ModalFooter>
285
+ <Button variant="ghost" color="secondary" onPress={close}>
286
+ <ButtonText>Cancel</ButtonText>
287
+ </Button>
288
+ <Button variant="solid" color="error" onPress={close}>
289
+ <ButtonText>Delete</ButtonText>
290
+ </Button>
291
+ </ModalFooter>
292
+ </ModalContent>
293
+ )}
294
+ </ModalDemo>
295
+ ),
296
+ }
297
+
298
+ export const SlideAnimation: Story = {
299
+ render: () => (
300
+ <ModalDemo animationType="slide" buttonLabel="Slide Modal">
301
+ <ModalContent>
302
+ <ModalHeader>
303
+ <ModalTitle>Slide Animation</ModalTitle>
304
+ <ModalCloseButton />
305
+ </ModalHeader>
306
+ <ModalBody>
307
+ <Text className="text-text-primary">
308
+ This modal uses animationType=&quot;slide&quot; instead of the default fade animation.
309
+ </Text>
310
+ </ModalBody>
311
+ <ModalFooter>
312
+ <Button variant="solid" color="primary">
313
+ <ButtonText>Close</ButtonText>
314
+ </Button>
315
+ </ModalFooter>
316
+ </ModalContent>
317
+ </ModalDemo>
318
+ ),
319
+ }
320
+
321
+ export const LargeWithScrollOutside: Story = {
322
+ render: () => (
323
+ <ModalDemo size="lg" scrollBehavior="outside" buttonLabel="Large Scrollable (Outside)">
324
+ <ModalContent>
325
+ <ModalHeader>
326
+ <ModalTitle>Large Modal - Outside Scroll</ModalTitle>
327
+ <ModalCloseButton />
328
+ </ModalHeader>
329
+ <ModalBody>
330
+ <View style={{ gap: 12 }}>
331
+ {Array.from({ length: 20 }).map((_, i) => (
332
+ <Text key={i} className="text-text-primary text-sm">
333
+ Paragraph {i + 1}: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do
334
+ eiusmod tempor incididunt ut labore et dolore magna aliqua.
335
+ </Text>
336
+ ))}
337
+ </View>
338
+ </ModalBody>
339
+ <ModalFooter>
340
+ <Button variant="ghost" color="secondary">
341
+ <ButtonText>Cancel</ButtonText>
342
+ </Button>
343
+ <Button variant="solid" color="primary">
344
+ <ButtonText>Save</ButtonText>
345
+ </Button>
346
+ </ModalFooter>
347
+ </ModalContent>
348
+ </ModalDemo>
349
+ ),
350
+ }