@titan-design/react-ui 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +10 -5
- 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/index.ts +8 -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 +135 -0
- package/src/components/ui/avatar/Avatar.tsx +168 -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 +86 -0
- package/src/components/ui/badge/Badge.tsx +105 -0
- package/src/components/ui/badge/index.ts +2 -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 +242 -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 +265 -0
- package/src/components/ui/card/Card.tsx +380 -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 +36 -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/popover/Popover.stories.tsx +190 -0
- package/src/components/ui/popover/Popover.test.tsx +277 -0
- package/src/components/ui/popover/Popover.tsx +162 -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 +181 -0
- package/src/components/ui/progress/Progress.tsx +320 -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 +180 -0
- package/src/components/ui/select/Select.tsx +236 -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 +8 -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 +179 -0
- package/src/components/ui/tooltip/Tooltip.tsx +143 -0
- package/src/components/ui/tooltip/index.ts +2 -0
- package/src/index.ts +10 -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/index.ts +39 -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/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
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
3
|
+
import { View, Text } from 'react-native'
|
|
4
|
+
import { ToolbarButton, ToolbarButtonGroup } from './ToolbarButton'
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof ToolbarButton> = {
|
|
7
|
+
title: 'Components/ToolbarButton',
|
|
8
|
+
component: ToolbarButton,
|
|
9
|
+
tags: ['autodocs'],
|
|
10
|
+
argTypes: {
|
|
11
|
+
variant: {
|
|
12
|
+
control: 'select',
|
|
13
|
+
options: ['default', 'neumorphic'],
|
|
14
|
+
},
|
|
15
|
+
isActive: {
|
|
16
|
+
control: 'select',
|
|
17
|
+
options: [undefined, true, false],
|
|
18
|
+
description: 'undefined/true = pressed look, false = raised look',
|
|
19
|
+
},
|
|
20
|
+
isDisabled: {
|
|
21
|
+
control: 'boolean',
|
|
22
|
+
},
|
|
23
|
+
showLabel: {
|
|
24
|
+
control: 'boolean',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
decorators: [
|
|
28
|
+
(Story) => (
|
|
29
|
+
<View className="bg-surface-elevated p-6 rounded-lg">
|
|
30
|
+
<Story />
|
|
31
|
+
</View>
|
|
32
|
+
),
|
|
33
|
+
],
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default meta
|
|
37
|
+
type Story = StoryObj<typeof ToolbarButton>
|
|
38
|
+
|
|
39
|
+
// Icon components matching MUI icon sizing (20px for fontSize="small")
|
|
40
|
+
// These simulate MUI SvgIcon components at 20x20
|
|
41
|
+
const SettingsIcon = ({ color }: { color?: string }) => (
|
|
42
|
+
<View style={{ width: 20, height: 20, alignItems: 'center', justifyContent: 'center' }}>
|
|
43
|
+
<Text className={color ? undefined : 'text-text-secondary'} style={{ fontSize: 16, ...(color ? { color } : {}) }}>⚙</Text>
|
|
44
|
+
</View>
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
const FilterIcon = ({ color }: { color?: string }) => (
|
|
48
|
+
<View style={{ width: 20, height: 20, alignItems: 'center', justifyContent: 'center' }}>
|
|
49
|
+
<Text className={color ? undefined : 'text-text-secondary'} style={{ fontSize: 16, ...(color ? { color } : {}) }}>⏣</Text>
|
|
50
|
+
</View>
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
const ViewIcon = ({ color }: { color?: string }) => (
|
|
54
|
+
<View style={{ width: 20, height: 20, alignItems: 'center', justifyContent: 'center' }}>
|
|
55
|
+
<Text className={color ? undefined : 'text-text-secondary'} style={{ fontSize: 16, ...(color ? { color } : {}) }}>◉</Text>
|
|
56
|
+
</View>
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
export const Default: Story = {
|
|
60
|
+
args: {
|
|
61
|
+
label: 'Settings',
|
|
62
|
+
icon: <SettingsIcon />,
|
|
63
|
+
variant: 'neumorphic',
|
|
64
|
+
showLabel: true,
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const ActiveVsInactive: Story = {
|
|
69
|
+
render: () => (
|
|
70
|
+
<View style={{ gap: 16 }}>
|
|
71
|
+
<View>
|
|
72
|
+
<Text className="text-text-secondary text-xs mb-1">
|
|
73
|
+
isActive=true - sunken/dark look, ORANGE icon, white text
|
|
74
|
+
</Text>
|
|
75
|
+
<ToolbarButton
|
|
76
|
+
label="Active (orange icon)"
|
|
77
|
+
icon={<FilterIcon />}
|
|
78
|
+
isActive={true}
|
|
79
|
+
/>
|
|
80
|
+
</View>
|
|
81
|
+
<View>
|
|
82
|
+
<Text className="text-text-secondary text-xs mb-1">
|
|
83
|
+
isActive=undefined - sunken/dark look, white icon (default state)
|
|
84
|
+
</Text>
|
|
85
|
+
<ToolbarButton
|
|
86
|
+
label="Default (white icon)"
|
|
87
|
+
icon={<FilterIcon />}
|
|
88
|
+
/>
|
|
89
|
+
</View>
|
|
90
|
+
<View>
|
|
91
|
+
<Text className="text-text-secondary text-xs mb-1">
|
|
92
|
+
isActive=false - elevated/light look, gray icon
|
|
93
|
+
</Text>
|
|
94
|
+
<ToolbarButton
|
|
95
|
+
label="Inactive (gray icon)"
|
|
96
|
+
icon={<FilterIcon />}
|
|
97
|
+
isActive={false}
|
|
98
|
+
/>
|
|
99
|
+
</View>
|
|
100
|
+
</View>
|
|
101
|
+
),
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const Interactive: Story = {
|
|
105
|
+
render: function Render() {
|
|
106
|
+
const [active, setActive] = useState(false)
|
|
107
|
+
return (
|
|
108
|
+
<View style={{ gap: 16 }}>
|
|
109
|
+
<Text className="text-text-secondary text-xs">
|
|
110
|
+
Click to toggle. When active=true, icon turns orange:
|
|
111
|
+
</Text>
|
|
112
|
+
<ToolbarButton
|
|
113
|
+
label="Toggle Me"
|
|
114
|
+
icon={<SettingsIcon />}
|
|
115
|
+
isActive={active}
|
|
116
|
+
onPress={() => setActive(!active)}
|
|
117
|
+
/>
|
|
118
|
+
<Text className="text-text-secondary text-xs">
|
|
119
|
+
isActive: {active ? 'true (sunken, orange icon)' : 'false (elevated, gray icon)'}
|
|
120
|
+
</Text>
|
|
121
|
+
</View>
|
|
122
|
+
)
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export const IconOnly: Story = {
|
|
127
|
+
render: () => (
|
|
128
|
+
<View style={{ gap: 16 }}>
|
|
129
|
+
<Text className="text-text-secondary text-xs">
|
|
130
|
+
Icon-only buttons. Active button has orange icon:
|
|
131
|
+
</Text>
|
|
132
|
+
<View style={{ flexDirection: 'row', gap: 8 }}>
|
|
133
|
+
<ToolbarButton
|
|
134
|
+
label="Settings"
|
|
135
|
+
icon={<SettingsIcon />}
|
|
136
|
+
showLabel={false}
|
|
137
|
+
isActive={false}
|
|
138
|
+
/>
|
|
139
|
+
<ToolbarButton
|
|
140
|
+
label="Filter (active)"
|
|
141
|
+
icon={<FilterIcon />}
|
|
142
|
+
showLabel={false}
|
|
143
|
+
isActive={true}
|
|
144
|
+
/>
|
|
145
|
+
<ToolbarButton
|
|
146
|
+
label="View"
|
|
147
|
+
icon={<ViewIcon />}
|
|
148
|
+
showLabel={false}
|
|
149
|
+
isActive={false}
|
|
150
|
+
/>
|
|
151
|
+
</View>
|
|
152
|
+
</View>
|
|
153
|
+
),
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export const Disabled: Story = {
|
|
157
|
+
render: () => (
|
|
158
|
+
<View style={{ gap: 16 }}>
|
|
159
|
+
<Text className="text-text-secondary text-xs">
|
|
160
|
+
Disabled buttons use MUI's default dark theme styling.
|
|
161
|
+
</Text>
|
|
162
|
+
<Text className="text-text-secondary text-[11px]">
|
|
163
|
+
Background: rgba(255,255,255,0.12) | No shadows | Colors preserved with opacity
|
|
164
|
+
</Text>
|
|
165
|
+
<Text className="text-text-secondary text-[11px]">
|
|
166
|
+
Note: Orange icon IS shown when isActive=true (matching original XP behavior)
|
|
167
|
+
</Text>
|
|
168
|
+
<View style={{ flexDirection: 'row', gap: 8 }}>
|
|
169
|
+
<ToolbarButton
|
|
170
|
+
label="Disabled Active"
|
|
171
|
+
icon={<SettingsIcon />}
|
|
172
|
+
isActive={true}
|
|
173
|
+
isDisabled={true}
|
|
174
|
+
/>
|
|
175
|
+
<ToolbarButton
|
|
176
|
+
label="Disabled Inactive"
|
|
177
|
+
icon={<SettingsIcon />}
|
|
178
|
+
isActive={false}
|
|
179
|
+
isDisabled={true}
|
|
180
|
+
/>
|
|
181
|
+
</View>
|
|
182
|
+
</View>
|
|
183
|
+
),
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export const HoverStates: Story = {
|
|
187
|
+
render: () => (
|
|
188
|
+
<View style={{ gap: 16 }}>
|
|
189
|
+
<Text className="text-text-secondary text-xs">
|
|
190
|
+
Hover over buttons to see the hover effect (background lightens, shadows removed)
|
|
191
|
+
</Text>
|
|
192
|
+
<Text className="text-text-secondary text-[11px]">
|
|
193
|
+
Active: #3C3C3C → #4C4C4C | Inactive: #6E6E6E → #5D5D5D | Shadows: removed
|
|
194
|
+
</Text>
|
|
195
|
+
<View style={{ flexDirection: 'row', gap: 8 }}>
|
|
196
|
+
<ToolbarButton
|
|
197
|
+
label="Hover Me (Active)"
|
|
198
|
+
icon={<SettingsIcon />}
|
|
199
|
+
isActive={true}
|
|
200
|
+
/>
|
|
201
|
+
<ToolbarButton
|
|
202
|
+
label="Hover Me (Inactive)"
|
|
203
|
+
icon={<SettingsIcon />}
|
|
204
|
+
isActive={false}
|
|
205
|
+
/>
|
|
206
|
+
</View>
|
|
207
|
+
</View>
|
|
208
|
+
),
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export const WithMenu: Story = {
|
|
212
|
+
render: function Render() {
|
|
213
|
+
return (
|
|
214
|
+
<ToolbarButton
|
|
215
|
+
label="View Options"
|
|
216
|
+
icon={<ViewIcon />}
|
|
217
|
+
isActive={false}
|
|
218
|
+
menuContent={
|
|
219
|
+
<View style={{ paddingVertical: 8 }}>
|
|
220
|
+
<Text className="text-text-primary" style={{ paddingHorizontal: 16, paddingVertical: 8 }}>Grid View</Text>
|
|
221
|
+
<Text className="text-text-primary" style={{ paddingHorizontal: 16, paddingVertical: 8 }}>List View</Text>
|
|
222
|
+
<Text className="text-text-primary" style={{ paddingHorizontal: 16, paddingVertical: 8 }}>Compact View</Text>
|
|
223
|
+
</View>
|
|
224
|
+
}
|
|
225
|
+
/>
|
|
226
|
+
)
|
|
227
|
+
},
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export const ButtonGroup: Story = {
|
|
231
|
+
render: function Render() {
|
|
232
|
+
const [activeIndex, setActiveIndex] = useState<number | null>(1)
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<View style={{ gap: 16 }}>
|
|
236
|
+
<Text className="text-text-secondary text-xs mb-1">
|
|
237
|
+
Click to select (one active at a time):
|
|
238
|
+
</Text>
|
|
239
|
+
<ToolbarButtonGroup gap="sm">
|
|
240
|
+
<ToolbarButton
|
|
241
|
+
label="Filter"
|
|
242
|
+
icon={<FilterIcon />}
|
|
243
|
+
isActive={activeIndex === 0}
|
|
244
|
+
onPress={() => setActiveIndex(activeIndex === 0 ? null : 0)}
|
|
245
|
+
/>
|
|
246
|
+
<ToolbarButton
|
|
247
|
+
label="Settings"
|
|
248
|
+
icon={<SettingsIcon />}
|
|
249
|
+
isActive={activeIndex === 1}
|
|
250
|
+
onPress={() => setActiveIndex(activeIndex === 1 ? null : 1)}
|
|
251
|
+
/>
|
|
252
|
+
<ToolbarButton
|
|
253
|
+
label="View"
|
|
254
|
+
icon={<ViewIcon />}
|
|
255
|
+
isActive={activeIndex === 2}
|
|
256
|
+
onPress={() => setActiveIndex(activeIndex === 2 ? null : 2)}
|
|
257
|
+
/>
|
|
258
|
+
</ToolbarButtonGroup>
|
|
259
|
+
</View>
|
|
260
|
+
)
|
|
261
|
+
},
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export const ToolbarExample: Story = {
|
|
265
|
+
render: function Render() {
|
|
266
|
+
const [filterActive, setFilterActive] = useState(true)
|
|
267
|
+
const [settingsActive, setSettingsActive] = useState(false)
|
|
268
|
+
|
|
269
|
+
return (
|
|
270
|
+
<View>
|
|
271
|
+
<Text className="text-text-secondary text-xs mb-3">
|
|
272
|
+
Typical toolbar usage - filters active, settings inactive:
|
|
273
|
+
</Text>
|
|
274
|
+
<ToolbarButtonGroup gap="sm">
|
|
275
|
+
<ToolbarButton
|
|
276
|
+
label="Filters"
|
|
277
|
+
icon={<FilterIcon />}
|
|
278
|
+
isActive={filterActive}
|
|
279
|
+
onPress={() => setFilterActive(!filterActive)}
|
|
280
|
+
/>
|
|
281
|
+
<ToolbarButton
|
|
282
|
+
label="Settings"
|
|
283
|
+
icon={<SettingsIcon />}
|
|
284
|
+
isActive={settingsActive}
|
|
285
|
+
onPress={() => setSettingsActive(!settingsActive)}
|
|
286
|
+
/>
|
|
287
|
+
<ToolbarButton
|
|
288
|
+
label="View"
|
|
289
|
+
icon={<ViewIcon />}
|
|
290
|
+
isActive={false}
|
|
291
|
+
menuContent={
|
|
292
|
+
<View style={{ paddingVertical: 8 }}>
|
|
293
|
+
<Text className="text-text-primary" style={{ paddingHorizontal: 16, paddingVertical: 8 }}>Grid</Text>
|
|
294
|
+
<Text className="text-text-primary" style={{ paddingHorizontal: 16, paddingVertical: 8 }}>List</Text>
|
|
295
|
+
</View>
|
|
296
|
+
}
|
|
297
|
+
/>
|
|
298
|
+
</ToolbarButtonGroup>
|
|
299
|
+
</View>
|
|
300
|
+
)
|
|
301
|
+
},
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export const CompactMode: Story = {
|
|
305
|
+
render: function Render() {
|
|
306
|
+
const [isCompact, setIsCompact] = useState(false)
|
|
307
|
+
|
|
308
|
+
return (
|
|
309
|
+
<View style={{ gap: 16 }}>
|
|
310
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
|
311
|
+
<Text className="text-text-secondary text-xs">
|
|
312
|
+
Mode: {isCompact ? 'Compact (icon only)' : 'Full (icon + label)'}
|
|
313
|
+
</Text>
|
|
314
|
+
<ToolbarButton
|
|
315
|
+
label="Toggle"
|
|
316
|
+
icon={<ViewIcon />}
|
|
317
|
+
showLabel={false}
|
|
318
|
+
isActive={isCompact}
|
|
319
|
+
onPress={() => setIsCompact(!isCompact)}
|
|
320
|
+
/>
|
|
321
|
+
</View>
|
|
322
|
+
<View className="h-px bg-border-default" />
|
|
323
|
+
<ToolbarButtonGroup gap="sm">
|
|
324
|
+
<ToolbarButton
|
|
325
|
+
label="Filter"
|
|
326
|
+
icon={<FilterIcon />}
|
|
327
|
+
showLabel={!isCompact}
|
|
328
|
+
isActive={true}
|
|
329
|
+
/>
|
|
330
|
+
<ToolbarButton
|
|
331
|
+
label="Settings"
|
|
332
|
+
icon={<SettingsIcon />}
|
|
333
|
+
showLabel={!isCompact}
|
|
334
|
+
isActive={false}
|
|
335
|
+
/>
|
|
336
|
+
<ToolbarButton
|
|
337
|
+
label="View"
|
|
338
|
+
icon={<ViewIcon />}
|
|
339
|
+
showLabel={!isCompact}
|
|
340
|
+
isActive={false}
|
|
341
|
+
/>
|
|
342
|
+
</ToolbarButtonGroup>
|
|
343
|
+
</View>
|
|
344
|
+
)
|
|
345
|
+
},
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export const Variants: Story = {
|
|
349
|
+
render: () => (
|
|
350
|
+
<View style={{ gap: 24 }}>
|
|
351
|
+
<View>
|
|
352
|
+
<Text className="text-text-secondary text-xs mb-2">Neumorphic (with 3D inset shadows)</Text>
|
|
353
|
+
<View style={{ flexDirection: 'row', gap: 8 }}>
|
|
354
|
+
<ToolbarButton label="Inactive" icon={<SettingsIcon />} variant="neumorphic" isActive={false} />
|
|
355
|
+
<ToolbarButton label="Active" icon={<SettingsIcon />} variant="neumorphic" isActive={true} />
|
|
356
|
+
</View>
|
|
357
|
+
</View>
|
|
358
|
+
<View>
|
|
359
|
+
<Text className="text-text-secondary text-xs mb-2">Flat (no shadows)</Text>
|
|
360
|
+
<View style={{ flexDirection: 'row', gap: 8 }}>
|
|
361
|
+
<ToolbarButton label="Inactive" icon={<SettingsIcon />} variant="default" isActive={false} />
|
|
362
|
+
<ToolbarButton label="Active" icon={<SettingsIcon />} variant="default" isActive={true} />
|
|
363
|
+
</View>
|
|
364
|
+
</View>
|
|
365
|
+
</View>
|
|
366
|
+
),
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export const CSSReference: Story = {
|
|
370
|
+
render: () => (
|
|
371
|
+
<View style={{ gap: 16 }}>
|
|
372
|
+
<Text className="text-text-secondary text-xs">
|
|
373
|
+
Computed CSS properties (derived from original SettingsButton):
|
|
374
|
+
</Text>
|
|
375
|
+
<View className="bg-surface-base p-4 rounded-lg">
|
|
376
|
+
<Text className="text-text-secondary text-[11px] font-mono">
|
|
377
|
+
{`/* Button base */
|
|
378
|
+
padding: 6px 16px;
|
|
379
|
+
border-radius: 4px;
|
|
380
|
+
display: flex;
|
|
381
|
+
align-items: center;
|
|
382
|
+
|
|
383
|
+
/* Active (isActive !== false) - sunken look */
|
|
384
|
+
background-color: #3C3C3C;
|
|
385
|
+
box-shadow: inset -1px -1px 6px #3C3C3C,
|
|
386
|
+
inset 1px 2px 1px #6E6E6E,
|
|
387
|
+
1px 1px 1px #6E6E6E;
|
|
388
|
+
color: #FFFFFF;
|
|
389
|
+
|
|
390
|
+
/* Inactive (isActive === false) - elevated look */
|
|
391
|
+
background-color: #6E6E6E;
|
|
392
|
+
box-shadow: inset 2px 2px 2px #1F1F1F,
|
|
393
|
+
inset -1px -1px 2px #6E6E6E;
|
|
394
|
+
color: #D1D1D1;
|
|
395
|
+
|
|
396
|
+
/* Text (Typography subtitle2 + bold) */
|
|
397
|
+
font-size: 14px;
|
|
398
|
+
font-weight: 700;
|
|
399
|
+
font-family: Nunito Sans;
|
|
400
|
+
line-height: 1.57;
|
|
401
|
+
|
|
402
|
+
/* Icon */
|
|
403
|
+
width: 20px;
|
|
404
|
+
height: 20px;
|
|
405
|
+
margin-right: 8px; /* when label shown */
|
|
406
|
+
color: #FF7900; /* when isActive=true */`}
|
|
407
|
+
</Text>
|
|
408
|
+
</View>
|
|
409
|
+
<View style={{ flexDirection: 'row', gap: 8 }}>
|
|
410
|
+
<ToolbarButton label="Inactive" icon={<SettingsIcon />} isActive={false} />
|
|
411
|
+
<ToolbarButton label="Active" icon={<SettingsIcon />} isActive={true} />
|
|
412
|
+
</View>
|
|
413
|
+
</View>
|
|
414
|
+
),
|
|
415
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
3
|
+
import { axe } from 'jest-axe'
|
|
4
|
+
import { ToolbarButton, ToolbarButtonGroup } from './ToolbarButton'
|
|
5
|
+
|
|
6
|
+
describe('ToolbarButton', () => {
|
|
7
|
+
it('renders with label', () => {
|
|
8
|
+
render(<ToolbarButton label="Settings" />)
|
|
9
|
+
expect(screen.getByRole('button', { name: 'Settings' })).toBeInTheDocument()
|
|
10
|
+
expect(screen.getByText('Settings')).toBeInTheDocument()
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('renders icon when provided', () => {
|
|
14
|
+
render(
|
|
15
|
+
<ToolbarButton
|
|
16
|
+
label="Settings"
|
|
17
|
+
icon={<span data-testid="icon">gear</span>}
|
|
18
|
+
/>
|
|
19
|
+
)
|
|
20
|
+
expect(screen.getByTestId('icon')).toBeInTheDocument()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('hides label when showLabel is false', () => {
|
|
24
|
+
render(<ToolbarButton label="Settings" showLabel={false} />)
|
|
25
|
+
expect(screen.getByRole('button', { name: 'Settings' })).toBeInTheDocument()
|
|
26
|
+
expect(screen.queryByText('Settings')).not.toBeInTheDocument()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
describe('press handling', () => {
|
|
30
|
+
it('calls onPress when clicked', () => {
|
|
31
|
+
const onPress = vi.fn()
|
|
32
|
+
render(<ToolbarButton label="Action" onPress={onPress} />)
|
|
33
|
+
|
|
34
|
+
fireEvent.click(screen.getByRole('button', { name: 'Action' }))
|
|
35
|
+
expect(onPress).toHaveBeenCalledTimes(1)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('does not call onPress when disabled', () => {
|
|
39
|
+
const onPress = vi.fn()
|
|
40
|
+
render(<ToolbarButton label="Disabled" isDisabled onPress={onPress} />)
|
|
41
|
+
|
|
42
|
+
fireEvent.click(screen.getByRole('button', { name: 'Disabled' }))
|
|
43
|
+
expect(onPress).not.toHaveBeenCalled()
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe('active state', () => {
|
|
48
|
+
it('communicates selected state when isActive is true', () => {
|
|
49
|
+
render(<ToolbarButton label="Active" isActive />)
|
|
50
|
+
const button = screen.getByRole('button', { name: 'Active' })
|
|
51
|
+
expect(button).toBeInTheDocument()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('communicates unselected state when isActive is false', () => {
|
|
55
|
+
render(<ToolbarButton label="Inactive" isActive={false} />)
|
|
56
|
+
const button = screen.getByRole('button', { name: 'Inactive' })
|
|
57
|
+
expect(button).toBeInTheDocument()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('renders without explicit active state', () => {
|
|
61
|
+
render(<ToolbarButton label="Default" />)
|
|
62
|
+
expect(screen.getByRole('button', { name: 'Default' })).toBeInTheDocument()
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
describe('disabled state', () => {
|
|
67
|
+
it('renders as disabled when isDisabled is true', () => {
|
|
68
|
+
render(<ToolbarButton label="Disabled" isDisabled />)
|
|
69
|
+
const button = screen.getByRole('button', { name: 'Disabled' })
|
|
70
|
+
expect(button).toBeDisabled()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('communicates disabled state to assistive technology', () => {
|
|
74
|
+
render(<ToolbarButton label="Disabled" isDisabled />)
|
|
75
|
+
const button = screen.getByRole('button', { name: 'Disabled' })
|
|
76
|
+
expect(button).toHaveAttribute('aria-disabled', 'true')
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('sizes', () => {
|
|
81
|
+
it('renders with sm size', () => {
|
|
82
|
+
render(<ToolbarButton label="Small" size="sm" />)
|
|
83
|
+
expect(screen.getByRole('button', { name: 'Small' })).toBeInTheDocument()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('renders with md size (default)', () => {
|
|
87
|
+
render(<ToolbarButton label="Medium" size="md" />)
|
|
88
|
+
expect(screen.getByRole('button', { name: 'Medium' })).toBeInTheDocument()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('renders with lg size', () => {
|
|
92
|
+
render(<ToolbarButton label="Large" size="lg" />)
|
|
93
|
+
expect(screen.getByRole('button', { name: 'Large' })).toBeInTheDocument()
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
describe('variants', () => {
|
|
98
|
+
it('renders with neumorphic variant (default)', () => {
|
|
99
|
+
render(<ToolbarButton label="Neumorphic" variant="neumorphic" />)
|
|
100
|
+
expect(screen.getByRole('button', { name: 'Neumorphic' })).toBeInTheDocument()
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('renders with default variant', () => {
|
|
104
|
+
render(<ToolbarButton label="Default" variant="default" />)
|
|
105
|
+
expect(screen.getByRole('button', { name: 'Default' })).toBeInTheDocument()
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
describe('menu content', () => {
|
|
110
|
+
it('shows menu when pressed with menuContent', () => {
|
|
111
|
+
render(
|
|
112
|
+
<ToolbarButton
|
|
113
|
+
label="Options"
|
|
114
|
+
menuContent={<span>Menu item</span>}
|
|
115
|
+
/>
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
fireEvent.click(screen.getByRole('button', { name: 'Options' }))
|
|
119
|
+
expect(screen.getByText('Menu item')).toBeInTheDocument()
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('hides menu when pressed again', () => {
|
|
123
|
+
render(
|
|
124
|
+
<ToolbarButton
|
|
125
|
+
label="Options"
|
|
126
|
+
menuContent={<span>Menu item</span>}
|
|
127
|
+
/>
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
const button = screen.getByRole('button', { name: 'Options' })
|
|
131
|
+
fireEvent.click(button)
|
|
132
|
+
expect(screen.getByText('Menu item')).toBeInTheDocument()
|
|
133
|
+
|
|
134
|
+
fireEvent.click(button)
|
|
135
|
+
expect(screen.queryByText('Menu item')).not.toBeInTheDocument()
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
describe('accessibility', () => {
|
|
140
|
+
it('has no accessibility violations', async () => {
|
|
141
|
+
const { container } = render(<ToolbarButton label="Accessible Button" />)
|
|
142
|
+
const results = await axe(container)
|
|
143
|
+
expect(results).toHaveNoViolations()
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('has correct accessibility label', () => {
|
|
147
|
+
render(<ToolbarButton label="Settings" />)
|
|
148
|
+
expect(screen.getByLabelText('Settings')).toBeInTheDocument()
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('applies tooltip as accessibility hint', () => {
|
|
152
|
+
render(<ToolbarButton label="Help" tooltip="Show help panel" />)
|
|
153
|
+
const button = screen.getByRole('button', { name: 'Help' })
|
|
154
|
+
expect(button).toBeInTheDocument()
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
describe('ToolbarButtonGroup', () => {
|
|
160
|
+
it('renders children', () => {
|
|
161
|
+
render(
|
|
162
|
+
<ToolbarButtonGroup>
|
|
163
|
+
<ToolbarButton label="One" />
|
|
164
|
+
<ToolbarButton label="Two" />
|
|
165
|
+
</ToolbarButtonGroup>
|
|
166
|
+
)
|
|
167
|
+
expect(screen.getByText('One')).toBeInTheDocument()
|
|
168
|
+
expect(screen.getByText('Two')).toBeInTheDocument()
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('has toolbar accessibility role', () => {
|
|
172
|
+
render(
|
|
173
|
+
<ToolbarButtonGroup>
|
|
174
|
+
<ToolbarButton label="Action" />
|
|
175
|
+
</ToolbarButtonGroup>
|
|
176
|
+
)
|
|
177
|
+
expect(screen.getByRole('toolbar')).toBeInTheDocument()
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('renders with different orientations', () => {
|
|
181
|
+
const { rerender } = render(
|
|
182
|
+
<ToolbarButtonGroup orientation="horizontal">
|
|
183
|
+
<ToolbarButton label="H" />
|
|
184
|
+
</ToolbarButtonGroup>
|
|
185
|
+
)
|
|
186
|
+
expect(screen.getByRole('toolbar')).toBeInTheDocument()
|
|
187
|
+
|
|
188
|
+
rerender(
|
|
189
|
+
<ToolbarButtonGroup orientation="vertical">
|
|
190
|
+
<ToolbarButton label="V" />
|
|
191
|
+
</ToolbarButtonGroup>
|
|
192
|
+
)
|
|
193
|
+
expect(screen.getByRole('toolbar')).toBeInTheDocument()
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('renders with different gap sizes', () => {
|
|
197
|
+
const { rerender } = render(
|
|
198
|
+
<ToolbarButtonGroup gap="none">
|
|
199
|
+
<ToolbarButton label="A" />
|
|
200
|
+
</ToolbarButtonGroup>
|
|
201
|
+
)
|
|
202
|
+
expect(screen.getByRole('toolbar')).toBeInTheDocument()
|
|
203
|
+
|
|
204
|
+
rerender(
|
|
205
|
+
<ToolbarButtonGroup gap="md">
|
|
206
|
+
<ToolbarButton label="A" />
|
|
207
|
+
</ToolbarButtonGroup>
|
|
208
|
+
)
|
|
209
|
+
expect(screen.getByRole('toolbar')).toBeInTheDocument()
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
describe('accessibility', () => {
|
|
213
|
+
it('has no accessibility violations', async () => {
|
|
214
|
+
const { container } = render(
|
|
215
|
+
<ToolbarButtonGroup>
|
|
216
|
+
<ToolbarButton label="Bold" />
|
|
217
|
+
<ToolbarButton label="Italic" />
|
|
218
|
+
</ToolbarButtonGroup>
|
|
219
|
+
)
|
|
220
|
+
const results = await axe(container)
|
|
221
|
+
expect(results).toHaveNoViolations()
|
|
222
|
+
})
|
|
223
|
+
})
|
|
224
|
+
})
|