@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,237 @@
|
|
|
1
|
+
import React, { useState, useRef, useCallback, createContext, useContext } from 'react'
|
|
2
|
+
import { View, Pressable, Modal, type ViewProps } from 'react-native'
|
|
3
|
+
import { cn } from '../../../utils/cn'
|
|
4
|
+
|
|
5
|
+
export type PopoverPlacement = 'top' | 'bottom' | 'left' | 'right'
|
|
6
|
+
|
|
7
|
+
interface PopoverContextType {
|
|
8
|
+
isOpen: boolean
|
|
9
|
+
setIsOpen: (open: boolean) => void
|
|
10
|
+
placement: PopoverPlacement
|
|
11
|
+
triggerMode: 'click' | 'hover'
|
|
12
|
+
handleHoverIn: () => void
|
|
13
|
+
handleHoverOut: () => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const PopoverContext = createContext<PopoverContextType>({
|
|
17
|
+
isOpen: false,
|
|
18
|
+
setIsOpen: () => {},
|
|
19
|
+
placement: 'bottom',
|
|
20
|
+
triggerMode: 'click',
|
|
21
|
+
handleHoverIn: () => {},
|
|
22
|
+
handleHoverOut: () => {},
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
export interface PopoverProps extends ViewProps {
|
|
26
|
+
/** Placement of the popover */
|
|
27
|
+
placement?: PopoverPlacement
|
|
28
|
+
/** Controlled open state */
|
|
29
|
+
isOpen?: boolean
|
|
30
|
+
/** Callback when open state changes */
|
|
31
|
+
onOpenChange?: (isOpen: boolean) => void
|
|
32
|
+
/** Whether clicking outside closes the popover */
|
|
33
|
+
closeOnClickOutside?: boolean
|
|
34
|
+
/** Trigger mode: 'click' opens on click, 'hover' opens on hover */
|
|
35
|
+
triggerMode?: 'click' | 'hover'
|
|
36
|
+
/** Delay in ms before closing in hover mode (default 150) */
|
|
37
|
+
closeDelay?: number
|
|
38
|
+
/** Additional className */
|
|
39
|
+
className?: string
|
|
40
|
+
children?: React.ReactNode
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Popover component for displaying floating content.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* <Popover>
|
|
48
|
+
* <PopoverTrigger>
|
|
49
|
+
* <Button><ButtonText>Open</ButtonText></Button>
|
|
50
|
+
* </PopoverTrigger>
|
|
51
|
+
* <PopoverContent>
|
|
52
|
+
* <Text>Popover content</Text>
|
|
53
|
+
* </PopoverContent>
|
|
54
|
+
* </Popover>
|
|
55
|
+
*/
|
|
56
|
+
export function Popover({
|
|
57
|
+
placement = 'bottom',
|
|
58
|
+
isOpen: controlledIsOpen,
|
|
59
|
+
onOpenChange,
|
|
60
|
+
closeOnClickOutside = true,
|
|
61
|
+
triggerMode = 'click',
|
|
62
|
+
closeDelay = 150,
|
|
63
|
+
className,
|
|
64
|
+
children,
|
|
65
|
+
...props
|
|
66
|
+
}: PopoverProps) {
|
|
67
|
+
const [internalIsOpen, setInternalIsOpen] = useState(false)
|
|
68
|
+
const isOpen = controlledIsOpen ?? internalIsOpen
|
|
69
|
+
const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
70
|
+
|
|
71
|
+
const setIsOpen = useCallback((open: boolean) => {
|
|
72
|
+
if (controlledIsOpen === undefined) {
|
|
73
|
+
setInternalIsOpen(open)
|
|
74
|
+
}
|
|
75
|
+
onOpenChange?.(open)
|
|
76
|
+
}, [controlledIsOpen, onOpenChange])
|
|
77
|
+
|
|
78
|
+
const handleHoverIn = useCallback(() => {
|
|
79
|
+
if (triggerMode !== 'hover') return
|
|
80
|
+
if (closeTimerRef.current) {
|
|
81
|
+
clearTimeout(closeTimerRef.current)
|
|
82
|
+
closeTimerRef.current = null
|
|
83
|
+
}
|
|
84
|
+
setIsOpen(true)
|
|
85
|
+
}, [triggerMode, setIsOpen])
|
|
86
|
+
|
|
87
|
+
const handleHoverOut = useCallback(() => {
|
|
88
|
+
if (triggerMode !== 'hover') return
|
|
89
|
+
closeTimerRef.current = setTimeout(() => {
|
|
90
|
+
setIsOpen(false)
|
|
91
|
+
closeTimerRef.current = null
|
|
92
|
+
}, closeDelay)
|
|
93
|
+
}, [triggerMode, closeDelay, setIsOpen])
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<PopoverContext.Provider
|
|
97
|
+
value={{ isOpen, setIsOpen, placement, triggerMode, handleHoverIn, handleHoverOut }}
|
|
98
|
+
>
|
|
99
|
+
<View className={cn('relative', className)} {...props}>
|
|
100
|
+
{children}
|
|
101
|
+
</View>
|
|
102
|
+
</PopoverContext.Provider>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface PopoverTriggerProps {
|
|
107
|
+
children: React.ReactNode
|
|
108
|
+
className?: string
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Trigger element for the popover.
|
|
113
|
+
*/
|
|
114
|
+
export function PopoverTrigger({ children, className }: PopoverTriggerProps) {
|
|
115
|
+
const { isOpen, setIsOpen, triggerMode, handleHoverIn, handleHoverOut } =
|
|
116
|
+
useContext(PopoverContext)
|
|
117
|
+
|
|
118
|
+
const hoverProps =
|
|
119
|
+
triggerMode === 'hover'
|
|
120
|
+
? {
|
|
121
|
+
onHoverIn: handleHoverIn,
|
|
122
|
+
onHoverOut: handleHoverOut,
|
|
123
|
+
onMouseEnter: handleHoverIn,
|
|
124
|
+
onMouseLeave: handleHoverOut,
|
|
125
|
+
}
|
|
126
|
+
: {}
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<Pressable
|
|
130
|
+
onPress={() => setIsOpen(!isOpen)}
|
|
131
|
+
accessibilityRole="button"
|
|
132
|
+
accessibilityState={{ expanded: isOpen }}
|
|
133
|
+
className={className}
|
|
134
|
+
{...hoverProps}
|
|
135
|
+
>
|
|
136
|
+
{children}
|
|
137
|
+
</Pressable>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface PopoverContentProps {
|
|
142
|
+
children?: React.ReactNode
|
|
143
|
+
className?: string
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const SPACER_STYLES: Record<PopoverPlacement, React.CSSProperties> = {
|
|
147
|
+
top: { left: 0, right: 0, bottom: 0, height: 8, transform: 'translateY(100%)' },
|
|
148
|
+
bottom: { left: 0, right: 0, top: 0, height: 8, transform: 'translateY(-100%)' },
|
|
149
|
+
left: { top: 0, bottom: 0, right: 0, width: 8, transform: 'translateX(100%)' },
|
|
150
|
+
right: { top: 0, bottom: 0, left: 0, width: 8, transform: 'translateX(-100%)' },
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Content container for the popover.
|
|
155
|
+
*/
|
|
156
|
+
export function PopoverContent({ children, className }: PopoverContentProps) {
|
|
157
|
+
const { isOpen, setIsOpen, placement, triggerMode, handleHoverIn, handleHoverOut } =
|
|
158
|
+
useContext(PopoverContext)
|
|
159
|
+
|
|
160
|
+
if (!isOpen) return null
|
|
161
|
+
|
|
162
|
+
const placementStyles = {
|
|
163
|
+
top: 'bottom-full left-0 mb-2',
|
|
164
|
+
bottom: 'top-full left-0 mt-2',
|
|
165
|
+
left: 'right-full top-0 mr-2',
|
|
166
|
+
right: 'left-full top-0 ml-2',
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const hoverProps =
|
|
170
|
+
triggerMode === 'hover'
|
|
171
|
+
? {
|
|
172
|
+
onHoverIn: handleHoverIn,
|
|
173
|
+
onHoverOut: handleHoverOut,
|
|
174
|
+
onMouseEnter: handleHoverIn,
|
|
175
|
+
onMouseLeave: handleHoverOut,
|
|
176
|
+
}
|
|
177
|
+
: {}
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<>
|
|
181
|
+
{/* Backdrop */}
|
|
182
|
+
<Pressable
|
|
183
|
+
onPress={() => setIsOpen(false)}
|
|
184
|
+
className="fixed inset-0 z-40"
|
|
185
|
+
style={{ position: 'absolute' }}
|
|
186
|
+
/>
|
|
187
|
+
{/* Content */}
|
|
188
|
+
<View
|
|
189
|
+
className={cn(
|
|
190
|
+
'absolute z-50 min-w-[200px]',
|
|
191
|
+
'bg-surface-elevated rounded-lg shadow-lg border border-border-default',
|
|
192
|
+
'p-4',
|
|
193
|
+
placementStyles[placement],
|
|
194
|
+
className
|
|
195
|
+
)}
|
|
196
|
+
{...hoverProps}
|
|
197
|
+
>
|
|
198
|
+
{triggerMode === 'hover' && (
|
|
199
|
+
<div
|
|
200
|
+
style={{
|
|
201
|
+
position: 'absolute',
|
|
202
|
+
pointerEvents: 'auto',
|
|
203
|
+
background: 'transparent',
|
|
204
|
+
...SPACER_STYLES[placement],
|
|
205
|
+
}}
|
|
206
|
+
onMouseEnter={handleHoverIn}
|
|
207
|
+
onMouseLeave={handleHoverOut}
|
|
208
|
+
/>
|
|
209
|
+
)}
|
|
210
|
+
{children}
|
|
211
|
+
</View>
|
|
212
|
+
</>
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export interface PopoverCloseButtonProps {
|
|
217
|
+
children?: React.ReactNode
|
|
218
|
+
className?: string
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Button to close the popover.
|
|
223
|
+
*/
|
|
224
|
+
export function PopoverCloseButton({ children, className }: PopoverCloseButtonProps) {
|
|
225
|
+
const { setIsOpen } = useContext(PopoverContext)
|
|
226
|
+
|
|
227
|
+
return (
|
|
228
|
+
<Pressable
|
|
229
|
+
onPress={() => setIsOpen(false)}
|
|
230
|
+
accessibilityRole="button"
|
|
231
|
+
accessibilityLabel="Close popover"
|
|
232
|
+
className={cn('p-1', className)}
|
|
233
|
+
>
|
|
234
|
+
{children}
|
|
235
|
+
</Pressable>
|
|
236
|
+
)
|
|
237
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { View, Text } from 'react-native'
|
|
3
|
+
import { Progress, CircularProgress, ProgressSteps } from './Progress'
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Progress> = {
|
|
6
|
+
title: 'Components/Progress',
|
|
7
|
+
component: Progress,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
argTypes: {
|
|
10
|
+
value: {
|
|
11
|
+
control: { type: 'range', min: 0, max: 100, step: 1 },
|
|
12
|
+
},
|
|
13
|
+
size: {
|
|
14
|
+
control: 'select',
|
|
15
|
+
options: ['sm', 'md', 'lg'],
|
|
16
|
+
},
|
|
17
|
+
color: {
|
|
18
|
+
control: 'select',
|
|
19
|
+
options: ['primary', 'secondary', 'success', 'error', 'warning', 'info'],
|
|
20
|
+
},
|
|
21
|
+
isIndeterminate: {
|
|
22
|
+
control: 'boolean',
|
|
23
|
+
},
|
|
24
|
+
showValue: {
|
|
25
|
+
control: 'boolean',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default meta
|
|
31
|
+
type Story = StoryObj<typeof Progress>
|
|
32
|
+
|
|
33
|
+
export const Default: Story = {
|
|
34
|
+
args: {
|
|
35
|
+
value: 60,
|
|
36
|
+
size: 'md',
|
|
37
|
+
color: 'primary',
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const WithLabel: Story = {
|
|
42
|
+
args: {
|
|
43
|
+
value: 45,
|
|
44
|
+
label: 'Uploading files...',
|
|
45
|
+
showValue: true,
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const AllSizes: Story = {
|
|
50
|
+
render: () => (
|
|
51
|
+
<View className="gap-6 w-64">
|
|
52
|
+
<View>
|
|
53
|
+
<Text className="text-text-secondary text-sm mb-2">Small</Text>
|
|
54
|
+
<Progress value={60} size="sm" />
|
|
55
|
+
</View>
|
|
56
|
+
<View>
|
|
57
|
+
<Text className="text-text-secondary text-sm mb-2">Medium</Text>
|
|
58
|
+
<Progress value={60} size="md" />
|
|
59
|
+
</View>
|
|
60
|
+
<View>
|
|
61
|
+
<Text className="text-text-secondary text-sm mb-2">Large</Text>
|
|
62
|
+
<Progress value={60} size="lg" />
|
|
63
|
+
</View>
|
|
64
|
+
</View>
|
|
65
|
+
),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const AllColors: Story = {
|
|
69
|
+
render: () => (
|
|
70
|
+
<View className="gap-4 w-64">
|
|
71
|
+
<Progress value={75} color="primary" label="Primary" showValue />
|
|
72
|
+
<Progress value={75} color="secondary" label="Secondary" showValue />
|
|
73
|
+
<Progress value={75} color="success" label="Success" showValue />
|
|
74
|
+
<Progress value={75} color="error" label="Error" showValue />
|
|
75
|
+
<Progress value={75} color="warning" label="Warning" showValue />
|
|
76
|
+
<Progress value={75} color="info" label="Info" showValue />
|
|
77
|
+
</View>
|
|
78
|
+
),
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const Indeterminate: Story = {
|
|
82
|
+
args: {
|
|
83
|
+
isIndeterminate: true,
|
|
84
|
+
label: 'Loading...',
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const CustomFormat: Story = {
|
|
89
|
+
args: {
|
|
90
|
+
value: 750,
|
|
91
|
+
max: 1000,
|
|
92
|
+
showValue: true,
|
|
93
|
+
label: 'Storage used',
|
|
94
|
+
formatValue: (value, max) => `${value} MB / ${max} MB`,
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const Circular: Story = {
|
|
99
|
+
render: () => (
|
|
100
|
+
<View className="flex-row gap-6 items-center">
|
|
101
|
+
<CircularProgress value={25} showValue />
|
|
102
|
+
<CircularProgress value={50} showValue />
|
|
103
|
+
<CircularProgress value={75} showValue />
|
|
104
|
+
<CircularProgress value={100} showValue />
|
|
105
|
+
</View>
|
|
106
|
+
),
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const CircularSizes: Story = {
|
|
110
|
+
render: () => (
|
|
111
|
+
<View className="flex-row gap-6 items-center">
|
|
112
|
+
<CircularProgress value={60} size={32} strokeWidth={3} showValue />
|
|
113
|
+
<CircularProgress value={60} size={48} strokeWidth={4} showValue />
|
|
114
|
+
<CircularProgress value={60} size={64} strokeWidth={5} showValue />
|
|
115
|
+
<CircularProgress value={60} size={80} strokeWidth={6} showValue />
|
|
116
|
+
</View>
|
|
117
|
+
),
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const Steps: Story = {
|
|
121
|
+
render: () => (
|
|
122
|
+
<View className="gap-8">
|
|
123
|
+
<ProgressSteps
|
|
124
|
+
currentStep={1}
|
|
125
|
+
totalSteps={4}
|
|
126
|
+
labels={['Cart', 'Shipping', 'Payment', 'Done']}
|
|
127
|
+
/>
|
|
128
|
+
<ProgressSteps
|
|
129
|
+
currentStep={2}
|
|
130
|
+
totalSteps={4}
|
|
131
|
+
color="success"
|
|
132
|
+
labels={['Step 1', 'Step 2', 'Step 3', 'Step 4']}
|
|
133
|
+
/>
|
|
134
|
+
<ProgressSteps
|
|
135
|
+
currentStep={0}
|
|
136
|
+
totalSteps={5}
|
|
137
|
+
size="lg"
|
|
138
|
+
/>
|
|
139
|
+
</View>
|
|
140
|
+
),
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export const UsageExamples: Story = {
|
|
144
|
+
render: () => (
|
|
145
|
+
<View className="gap-6">
|
|
146
|
+
{/* File upload */}
|
|
147
|
+
<View className="p-4 bg-surface-elevated rounded-lg">
|
|
148
|
+
<Text className="text-text-primary font-medium mb-3">File Upload</Text>
|
|
149
|
+
<Progress value={67} label="document.pdf" showValue />
|
|
150
|
+
</View>
|
|
151
|
+
|
|
152
|
+
{/* Storage quota */}
|
|
153
|
+
<View className="p-4 bg-surface-elevated rounded-lg">
|
|
154
|
+
<Text className="text-text-primary font-medium mb-3">Storage</Text>
|
|
155
|
+
<Progress
|
|
156
|
+
value={8.5}
|
|
157
|
+
max={10}
|
|
158
|
+
color="warning"
|
|
159
|
+
label="8.5 GB of 10 GB used"
|
|
160
|
+
/>
|
|
161
|
+
</View>
|
|
162
|
+
|
|
163
|
+
{/* Checkout steps */}
|
|
164
|
+
<View className="p-4 bg-surface-elevated rounded-lg">
|
|
165
|
+
<Text className="text-text-primary font-medium mb-3">Checkout Progress</Text>
|
|
166
|
+
<ProgressSteps
|
|
167
|
+
currentStep={2}
|
|
168
|
+
totalSteps={4}
|
|
169
|
+
labels={['Cart', 'Shipping', 'Payment', 'Confirm']}
|
|
170
|
+
/>
|
|
171
|
+
</View>
|
|
172
|
+
|
|
173
|
+
{/* Loading stats */}
|
|
174
|
+
<View className="p-4 bg-surface-elevated rounded-lg flex-row gap-4">
|
|
175
|
+
<View className="items-center">
|
|
176
|
+
<CircularProgress value={87} size={56} showValue color="success" />
|
|
177
|
+
<Text className="text-xs text-text-secondary mt-1">Success Rate</Text>
|
|
178
|
+
</View>
|
|
179
|
+
<View className="items-center">
|
|
180
|
+
<CircularProgress value={45} size={56} showValue color="warning" />
|
|
181
|
+
<Text className="text-xs text-text-secondary mt-1">Performance</Text>
|
|
182
|
+
</View>
|
|
183
|
+
<View className="items-center">
|
|
184
|
+
<CircularProgress value={12} size={56} showValue color="error" />
|
|
185
|
+
<Text className="text-xs text-text-secondary mt-1">Error Rate</Text>
|
|
186
|
+
</View>
|
|
187
|
+
</View>
|
|
188
|
+
</View>
|
|
189
|
+
),
|
|
190
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
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('trackWidth and customColor', () => {
|
|
83
|
+
it('supports fixed trackWidth', () => {
|
|
84
|
+
const { container } = render(<Progress value={50} trackWidth={60} />)
|
|
85
|
+
expect(container.firstChild).toBeInTheDocument()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('supports customColor', () => {
|
|
89
|
+
const { container } = render(<Progress value={50} customColor="#FF6B6B" />)
|
|
90
|
+
expect(container.firstChild).toBeInTheDocument()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('renders correctly without trackWidth or customColor', () => {
|
|
94
|
+
render(<Progress value={75} />)
|
|
95
|
+
expect(screen.getByRole('progressbar')).toBeInTheDocument()
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe('CircularProgress', () => {
|
|
100
|
+
it('renders correctly', () => {
|
|
101
|
+
render(<CircularProgress value={50} />)
|
|
102
|
+
expect(screen.getByRole('progressbar')).toBeInTheDocument()
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('sets accessibility value', () => {
|
|
106
|
+
render(<CircularProgress value={75} max={100} />)
|
|
107
|
+
const progressbar = screen.getByRole('progressbar')
|
|
108
|
+
// react-native-web maps accessibilityValue but not to standard aria-value* attributes
|
|
109
|
+
expect(progressbar).toBeInTheDocument()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('shows value when showValue is true', () => {
|
|
113
|
+
render(<CircularProgress value={50} showValue />)
|
|
114
|
+
expect(screen.getByText('50%')).toBeInTheDocument()
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('does not show value when indeterminate', () => {
|
|
118
|
+
render(<CircularProgress isIndeterminate showValue />)
|
|
119
|
+
expect(screen.queryByText(/%/)).not.toBeInTheDocument()
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('renders with custom size and strokeWidth', () => {
|
|
123
|
+
render(<CircularProgress size={64} strokeWidth={6} value={50} />)
|
|
124
|
+
expect(screen.getByRole('progressbar')).toBeInTheDocument()
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('renders with all color options', () => {
|
|
128
|
+
const colors = ['primary', 'secondary', 'success', 'error', 'warning', 'info'] as const
|
|
129
|
+
colors.forEach((color) => {
|
|
130
|
+
const { unmount } = render(<CircularProgress color={color} value={50} />)
|
|
131
|
+
expect(screen.getByRole('progressbar')).toBeInTheDocument()
|
|
132
|
+
unmount()
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('renders SVG circles', () => {
|
|
137
|
+
const { container } = render(<CircularProgress value={50} />)
|
|
138
|
+
const circles = container.querySelectorAll('circle')
|
|
139
|
+
expect(circles.length).toBe(2) // track + progress
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('applies custom color to SVG progress circle', () => {
|
|
143
|
+
const { container } = render(<CircularProgress value={50} color="success" />)
|
|
144
|
+
const progressCircle = container.querySelectorAll('circle')[1]
|
|
145
|
+
expect(progressCircle?.getAttribute('stroke')).toBe('var(--color-status-success)')
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
describe('ProgressSteps', () => {
|
|
150
|
+
it('renders correct number of steps', () => {
|
|
151
|
+
const { container } = render(<ProgressSteps currentStep={1} totalSteps={4} />)
|
|
152
|
+
expect(container.firstChild).toBeInTheDocument()
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('renders labels', () => {
|
|
156
|
+
render(
|
|
157
|
+
<ProgressSteps
|
|
158
|
+
currentStep={1}
|
|
159
|
+
totalSteps={3}
|
|
160
|
+
labels={['Step 1', 'Step 2', 'Step 3']}
|
|
161
|
+
/>
|
|
162
|
+
)
|
|
163
|
+
expect(screen.getByText('Step 1')).toBeInTheDocument()
|
|
164
|
+
expect(screen.getByText('Step 2')).toBeInTheDocument()
|
|
165
|
+
expect(screen.getByText('Step 3')).toBeInTheDocument()
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('renders with all size options', () => {
|
|
169
|
+
const sizes = ['sm', 'md', 'lg'] as const
|
|
170
|
+
sizes.forEach((size) => {
|
|
171
|
+
const { unmount } = render(<ProgressSteps currentStep={0} totalSteps={3} size={size} />)
|
|
172
|
+
unmount()
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('renders with all color options', () => {
|
|
177
|
+
const colors = ['primary', 'secondary', 'success', 'error', 'warning', 'info'] as const
|
|
178
|
+
colors.forEach((color) => {
|
|
179
|
+
const { unmount } = render(<ProgressSteps currentStep={0} totalSteps={3} color={color} />)
|
|
180
|
+
unmount()
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
describe('accessibility', () => {
|
|
186
|
+
it('has no accessibility violations for Progress', async () => {
|
|
187
|
+
const { container } = render(<Progress value={50} label="Upload progress" />)
|
|
188
|
+
const results = await axe(container, {
|
|
189
|
+
rules: { 'aria-progressbar-name': { enabled: false } },
|
|
190
|
+
})
|
|
191
|
+
expect(results).toHaveNoViolations()
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('has no accessibility violations for CircularProgress', async () => {
|
|
195
|
+
const { container } = render(<CircularProgress value={50} />)
|
|
196
|
+
const results = await axe(container, {
|
|
197
|
+
rules: { 'aria-progressbar-name': { enabled: false } },
|
|
198
|
+
})
|
|
199
|
+
expect(results).toHaveNoViolations()
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('has no accessibility violations for ProgressSteps', async () => {
|
|
203
|
+
const { container } = render(
|
|
204
|
+
<ProgressSteps currentStep={1} totalSteps={3} labels={['A', 'B', 'C']} />
|
|
205
|
+
)
|
|
206
|
+
const results = await axe(container)
|
|
207
|
+
expect(results).toHaveNoViolations()
|
|
208
|
+
})
|
|
209
|
+
})
|
|
210
|
+
})
|