@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.
- package/README.md +47 -1
- package/dist/{chunk-MGW37MPO.mjs → chunk-UXVRPOME.mjs} +173 -3
- package/dist/chunk-UXVRPOME.mjs.map +1 -0
- package/dist/index.d.mts +101 -12
- package/dist/index.d.ts +101 -12
- package/dist/index.js +1267 -468
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +734 -112
- package/dist/index.mjs.map +1 -1
- package/dist/theme/index.d.mts +86 -1
- package/dist/theme/index.d.ts +86 -1
- package/dist/theme/index.js +166 -0
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +1 -1
- package/package.json +22 -6
- package/src/components/custom/DateTime/DateTime.stories.tsx +180 -0
- package/src/components/custom/DateTime/DateTime.test.tsx +152 -0
- package/src/components/custom/DateTime/DateTime.tsx +235 -0
- package/src/components/custom/DateTime/index.ts +2 -0
- package/src/components/custom/EmptyState/EmptyState.stories.tsx +99 -0
- package/src/components/custom/EmptyState/EmptyState.test.tsx +106 -0
- package/src/components/custom/EmptyState/EmptyState.tsx +68 -0
- package/src/components/custom/EmptyState/index.ts +2 -0
- package/src/components/custom/Metric/Metric.stories.tsx +133 -0
- package/src/components/custom/Metric/Metric.test.tsx +120 -0
- package/src/components/custom/Metric/Metric.tsx +92 -0
- package/src/components/custom/Metric/index.ts +2 -0
- package/src/components/custom/Sidebar/Sidebar.stories.tsx +227 -0
- package/src/components/custom/Sidebar/Sidebar.test.tsx +185 -0
- package/src/components/custom/Sidebar/Sidebar.tsx +257 -0
- package/src/components/custom/Sidebar/index.ts +18 -0
- package/src/components/custom/Table/Table.stories.tsx +481 -0
- package/src/components/custom/Table/Table.test.tsx +531 -0
- package/src/components/custom/Table/Table.tsx +696 -0
- package/src/components/custom/Table/index.ts +22 -0
- package/src/components/custom/Typography/Typography.stories.tsx +153 -0
- package/src/components/custom/Typography/Typography.test.tsx +107 -0
- package/src/components/custom/Typography/Typography.tsx +218 -0
- package/src/components/custom/Typography/index.ts +19 -0
- package/src/components/custom/Workout/RestTimer.stories.tsx +71 -0
- package/src/components/custom/Workout/RestTimer.test.tsx +145 -0
- package/src/components/custom/Workout/RestTimer.tsx +172 -0
- package/src/components/custom/Workout/SupersetWrapper.stories.tsx +144 -0
- package/src/components/custom/Workout/SupersetWrapper.test.tsx +130 -0
- package/src/components/custom/Workout/SupersetWrapper.tsx +63 -0
- package/src/components/custom/Workout/index.ts +2 -0
- package/src/components/custom/index.ts +9 -0
- package/src/components/custom/stepper/Stepper.stories.tsx +245 -0
- package/src/components/custom/stepper/Stepper.test.tsx +230 -0
- package/src/components/custom/stepper/Stepper.tsx +250 -0
- package/src/components/custom/stepper/index.ts +10 -0
- package/src/components/index.ts +5 -0
- package/src/components/ui/alert/Alert.stories.tsx +154 -0
- package/src/components/ui/alert/Alert.test.tsx +202 -0
- package/src/components/ui/alert/Alert.tsx +170 -0
- package/src/components/ui/alert/index.ts +8 -0
- package/src/components/ui/autocomplete/Autocomplete.stories.tsx +299 -0
- package/src/components/ui/autocomplete/Autocomplete.test.tsx +247 -0
- package/src/components/ui/autocomplete/Autocomplete.tsx +332 -0
- package/src/components/ui/autocomplete/index.ts +2 -0
- package/src/components/ui/avatar/Avatar.stories.tsx +105 -0
- package/src/components/ui/avatar/Avatar.test.tsx +152 -0
- package/src/components/ui/avatar/Avatar.tsx +176 -0
- package/src/components/ui/avatar/index.ts +2 -0
- package/src/components/ui/badge/Badge.stories.tsx +100 -0
- package/src/components/ui/badge/Badge.test.tsx +103 -0
- package/src/components/ui/badge/Badge.tsx +119 -0
- package/src/components/ui/badge/index.ts +3 -0
- package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +70 -0
- package/src/components/ui/breadcrumbs/Breadcrumbs.test.tsx +156 -0
- package/src/components/ui/breadcrumbs/Breadcrumbs.tsx +127 -0
- package/src/components/ui/breadcrumbs/index.ts +2 -0
- package/src/components/ui/button/Button.stories.tsx +216 -0
- package/src/components/ui/button/Button.test.tsx +132 -0
- package/src/components/ui/button/Button.tsx +297 -0
- package/src/components/ui/button/index.ts +10 -0
- package/src/components/ui/card/Card.stories.tsx +443 -0
- package/src/components/ui/card/Card.test.tsx +294 -0
- package/src/components/ui/card/Card.tsx +401 -0
- package/src/components/ui/card/index.ts +20 -0
- package/src/components/ui/checkbox/Checkbox.stories.tsx +126 -0
- package/src/components/ui/checkbox/Checkbox.test.tsx +152 -0
- package/src/components/ui/checkbox/Checkbox.tsx +163 -0
- package/src/components/ui/checkbox/index.ts +2 -0
- package/src/components/ui/chip/Chip.stories.tsx +112 -0
- package/src/components/ui/chip/Chip.test.tsx +119 -0
- package/src/components/ui/chip/Chip.tsx +151 -0
- package/src/components/ui/chip/index.ts +2 -0
- package/src/components/ui/collapse/Collapse.stories.tsx +313 -0
- package/src/components/ui/collapse/Collapse.test.tsx +298 -0
- package/src/components/ui/collapse/Collapse.tsx +275 -0
- package/src/components/ui/collapse/index.ts +18 -0
- package/src/components/ui/data-row/DataRow.stories.tsx +78 -0
- package/src/components/ui/data-row/DataRow.test.tsx +78 -0
- package/src/components/ui/data-row/DataRow.tsx +29 -0
- package/src/components/ui/data-row/index.ts +2 -0
- package/src/components/ui/divider/Divider.stories.tsx +71 -0
- package/src/components/ui/divider/Divider.test.tsx +55 -0
- package/src/components/ui/divider/Divider.tsx +37 -0
- package/src/components/ui/divider/index.ts +2 -0
- package/src/components/ui/drawer/Drawer.stories.tsx +228 -0
- package/src/components/ui/drawer/Drawer.test.tsx +142 -0
- package/src/components/ui/drawer/Drawer.tsx +228 -0
- package/src/components/ui/drawer/index.ts +9 -0
- package/src/components/ui/form-field/FormField.stories.tsx +260 -0
- package/src/components/ui/form-field/FormField.test.tsx +280 -0
- package/src/components/ui/form-field/FormField.tsx +308 -0
- package/src/components/ui/form-field/index.ts +2 -0
- package/src/components/ui/help-tip/HelpTip.stories.tsx +250 -0
- package/src/components/ui/help-tip/HelpTip.test.tsx +198 -0
- package/src/components/ui/help-tip/HelpTip.tsx +229 -0
- package/src/components/ui/help-tip/index.ts +2 -0
- package/src/components/ui/icon-box/IconBox.stories.tsx +139 -0
- package/src/components/ui/icon-box/IconBox.test.tsx +120 -0
- package/src/components/ui/icon-box/IconBox.tsx +57 -0
- package/src/components/ui/icon-box/index.ts +1 -0
- package/src/components/ui/index.ts +38 -0
- package/src/components/ui/indicator/Indicator.stories.tsx +74 -0
- package/src/components/ui/indicator/Indicator.test.tsx +55 -0
- package/src/components/ui/indicator/Indicator.tsx +73 -0
- package/src/components/ui/indicator/index.ts +2 -0
- package/src/components/ui/input/Input.stories.tsx +192 -0
- package/src/components/ui/input/Input.test.tsx +86 -0
- package/src/components/ui/input/Input.tsx +273 -0
- package/src/components/ui/input/index.ts +2 -0
- package/src/components/ui/link/Link.stories.tsx +78 -0
- package/src/components/ui/link/Link.test.tsx +94 -0
- package/src/components/ui/link/Link.tsx +86 -0
- package/src/components/ui/link/index.ts +2 -0
- package/src/components/ui/list-item/ListItem.stories.tsx +146 -0
- package/src/components/ui/list-item/ListItem.test.tsx +128 -0
- package/src/components/ui/list-item/ListItem.tsx +89 -0
- package/src/components/ui/list-item/index.ts +14 -0
- package/src/components/ui/menu/Menu.stories.tsx +180 -0
- package/src/components/ui/menu/Menu.test.tsx +353 -0
- package/src/components/ui/menu/Menu.tsx +222 -0
- package/src/components/ui/menu/index.ts +2 -0
- package/src/components/ui/modal/Modal.stories.tsx +350 -0
- package/src/components/ui/modal/Modal.test.tsx +159 -0
- package/src/components/ui/modal/Modal.tsx +254 -0
- package/src/components/ui/modal/index.ts +19 -0
- package/src/components/ui/pill/Pill.stories.tsx +71 -0
- package/src/components/ui/pill/Pill.test.tsx +69 -0
- package/src/components/ui/pill/Pill.tsx +104 -0
- package/src/components/ui/pill/index.ts +2 -0
- package/src/components/ui/popover/Popover.stories.tsx +190 -0
- package/src/components/ui/popover/Popover.test.tsx +419 -0
- package/src/components/ui/popover/Popover.tsx +237 -0
- package/src/components/ui/popover/index.ts +2 -0
- package/src/components/ui/progress/Progress.stories.tsx +190 -0
- package/src/components/ui/progress/Progress.test.tsx +210 -0
- package/src/components/ui/progress/Progress.tsx +339 -0
- package/src/components/ui/progress/index.ts +9 -0
- package/src/components/ui/radio/Radio.stories.tsx +199 -0
- package/src/components/ui/radio/Radio.test.tsx +166 -0
- package/src/components/ui/radio/Radio.tsx +200 -0
- package/src/components/ui/radio/index.ts +2 -0
- package/src/components/ui/section/Section.stories.tsx +111 -0
- package/src/components/ui/section/Section.test.tsx +121 -0
- package/src/components/ui/section/Section.tsx +52 -0
- package/src/components/ui/section/index.ts +6 -0
- package/src/components/ui/select/Select.stories.tsx +286 -0
- package/src/components/ui/select/Select.test.tsx +200 -0
- package/src/components/ui/select/Select.tsx +243 -0
- package/src/components/ui/select/index.ts +2 -0
- package/src/components/ui/skeleton/Skeleton.stories.tsx +153 -0
- package/src/components/ui/skeleton/Skeleton.test.tsx +162 -0
- package/src/components/ui/skeleton/Skeleton.tsx +229 -0
- package/src/components/ui/skeleton/index.ts +16 -0
- package/src/components/ui/spinner/Spinner.stories.tsx +67 -0
- package/src/components/ui/spinner/Spinner.test.tsx +67 -0
- package/src/components/ui/spinner/Spinner.tsx +72 -0
- package/src/components/ui/spinner/index.ts +2 -0
- package/src/components/ui/stack/Stack.stories.tsx +127 -0
- package/src/components/ui/stack/Stack.test.tsx +184 -0
- package/src/components/ui/stack/Stack.tsx +76 -0
- package/src/components/ui/stack/index.ts +2 -0
- package/src/components/ui/surface/Surface.stories.tsx +91 -0
- package/src/components/ui/surface/Surface.test.tsx +63 -0
- package/src/components/ui/surface/Surface.tsx +51 -0
- package/src/components/ui/surface/index.ts +1 -0
- package/src/components/ui/switch/Switch.stories.tsx +111 -0
- package/src/components/ui/switch/Switch.test.tsx +112 -0
- package/src/components/ui/switch/Switch.tsx +103 -0
- package/src/components/ui/switch/index.ts +2 -0
- package/src/components/ui/tabs/Tabs.stories.tsx +213 -0
- package/src/components/ui/tabs/Tabs.test.tsx +192 -0
- package/src/components/ui/tabs/Tabs.tsx +253 -0
- package/src/components/ui/tabs/index.ts +16 -0
- package/src/components/ui/toast/Toast.stories.tsx +255 -0
- package/src/components/ui/toast/Toast.test.tsx +207 -0
- package/src/components/ui/toast/Toast.tsx +327 -0
- package/src/components/ui/toast/index.ts +9 -0
- package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +415 -0
- package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +224 -0
- package/src/components/ui/toolbar-button/ToolbarButton.tsx +307 -0
- package/src/components/ui/toolbar-button/index.ts +7 -0
- package/src/components/ui/tooltip/Tooltip.stories.tsx +126 -0
- package/src/components/ui/tooltip/Tooltip.test.tsx +262 -0
- package/src/components/ui/tooltip/Tooltip.tsx +234 -0
- package/src/components/ui/tooltip/index.ts +2 -0
- package/src/index.ts +10 -0
- package/src/stories/PresetShowcase.stories.tsx +462 -0
- package/src/stories/ThemePresets.stories.tsx +85 -0
- package/src/test/setup.ts +19 -0
- package/src/theme/Colors.stories.tsx +258 -0
- package/src/theme/config.ts +193 -0
- package/src/theme/elevation.stories.tsx +346 -0
- package/src/theme/elevation.test.ts +27 -0
- package/src/theme/elevation.ts +567 -0
- package/src/theme/global.css +129 -1
- package/src/theme/index.ts +40 -0
- package/src/theme/presets/apply.ts +97 -0
- package/src/theme/presets/audiobook.ts +93 -0
- package/src/theme/presets/default.ts +6 -0
- package/src/theme/presets/index.ts +4 -0
- package/src/theme/presets/presets.test.ts +51 -0
- package/src/theme/presets/types.ts +76 -0
- package/src/theme/shadows.stories.tsx +523 -0
- package/src/theme/shadows.ts +439 -0
- package/src/theme/tokens/index.ts +2 -0
- package/src/theme/tokens/primitives.ts +354 -0
- package/src/theme/tokens/semantic.ts +381 -0
- package/src/types/jest-axe.d.ts +16 -0
- package/src/types/nativewind.d.ts +54 -0
- package/src/utils/avatar-color.test.ts +30 -0
- package/src/utils/avatar-color.ts +20 -0
- package/src/utils/cn.ts +14 -0
- package/src/utils/colors.ts +140 -0
- package/src/utils/form.ts +188 -0
- package/src/utils/index.ts +29 -0
- package/src/utils/useTheme.ts +72 -0
- package/src/web-jsx/jsx-dev-runtime.ts +28 -0
- package/src/web-jsx/jsx-runtime.ts +41 -0
- package/tailwind.config.js +53 -3
- 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,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="{size}". 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="slide" 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
|
+
}
|