@olympusoss/canvas 4.0.0 → 5.0.0
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 +108 -0
- package/package.json +14 -3
- package/src/atoms/avatar/avatar.md +185 -0
- package/src/atoms/avatar/avatar.styles.ts +48 -0
- package/src/atoms/avatar/avatar.tsx +99 -0
- package/src/atoms/badge/badge.md +237 -0
- package/src/atoms/badge/badge.styles.ts +79 -0
- package/src/atoms/badge/badge.tsx +86 -0
- package/src/atoms/breadcrumb/breadcrumb.md +233 -0
- package/src/atoms/breadcrumb/breadcrumb.styles.ts +40 -0
- package/src/atoms/breadcrumb/breadcrumb.tsx +130 -0
- package/src/atoms/button/button.android.tsx +6 -0
- package/src/atoms/button/button.ios.tsx +6 -0
- package/src/atoms/button/button.md +184 -0
- package/src/atoms/button/button.shared.tsx +79 -0
- package/src/atoms/button/button.styles.ts +152 -0
- package/src/atoms/button/button.tsx +6 -0
- package/src/atoms/button-group/button-group.android.tsx +6 -0
- package/src/atoms/button-group/button-group.ios.tsx +6 -0
- package/src/atoms/button-group/button-group.md +120 -0
- package/src/atoms/button-group/button-group.shared.tsx +398 -0
- package/src/atoms/button-group/button-group.styles.ts +483 -0
- package/src/atoms/button-group/button-group.tsx +6 -0
- package/src/atoms/checkbox/checkbox.android.tsx +6 -0
- package/src/atoms/checkbox/checkbox.ios.tsx +6 -0
- package/src/atoms/checkbox/checkbox.md +150 -0
- package/src/atoms/checkbox/checkbox.shared.tsx +103 -0
- package/src/atoms/checkbox/checkbox.styles.ts +106 -0
- package/src/atoms/checkbox/checkbox.tsx +6 -0
- package/src/atoms/combobox/combobox.android.tsx +6 -0
- package/src/atoms/combobox/combobox.ios.tsx +6 -0
- package/src/atoms/combobox/combobox.md +213 -0
- package/src/atoms/combobox/combobox.shared.tsx +160 -0
- package/src/atoms/combobox/combobox.styles.ts +270 -0
- package/src/atoms/combobox/combobox.tsx +6 -0
- package/src/atoms/divider/divider.md +140 -0
- package/src/atoms/divider/divider.styles.ts +35 -0
- package/src/atoms/divider/divider.tsx +67 -0
- package/src/atoms/dropdown/dropdown.android.tsx +6 -0
- package/src/atoms/dropdown/dropdown.ios.tsx +6 -0
- package/src/atoms/dropdown/dropdown.md +221 -0
- package/src/atoms/dropdown/dropdown.shared.tsx +190 -0
- package/src/atoms/dropdown/dropdown.styles.ts +233 -0
- package/src/atoms/dropdown/dropdown.tsx +6 -0
- package/src/atoms/icon/icon.md +131 -0
- package/src/atoms/icon/icon.styles.ts +30 -0
- package/src/atoms/icon/icon.tsx +328 -0
- package/src/atoms/index.ts +24 -0
- package/src/atoms/input/input.android.tsx +6 -0
- package/src/atoms/input/input.ios.tsx +6 -0
- package/src/atoms/input/input.md +118 -0
- package/src/atoms/input/input.shared.tsx +203 -0
- package/src/atoms/input/input.styles.ts +286 -0
- package/src/atoms/input/input.tsx +6 -0
- package/src/atoms/kbd/kbd.md +91 -0
- package/src/atoms/kbd/kbd.styles.ts +33 -0
- package/src/atoms/kbd/kbd.tsx +27 -0
- package/src/atoms/listbox/listbox.md +177 -0
- package/src/atoms/listbox/listbox.styles.ts +60 -0
- package/src/atoms/listbox/listbox.tsx +113 -0
- package/src/atoms/pagination/pagination.android.tsx +6 -0
- package/src/atoms/pagination/pagination.ios.tsx +6 -0
- package/src/atoms/pagination/pagination.md +133 -0
- package/src/atoms/pagination/pagination.shared.tsx +289 -0
- package/src/atoms/pagination/pagination.styles.ts +245 -0
- package/src/atoms/pagination/pagination.tsx +6 -0
- package/src/atoms/popover/popover.android.tsx +8 -0
- package/src/atoms/popover/popover.ios.tsx +6 -0
- package/src/atoms/popover/popover.md +87 -0
- package/src/atoms/popover/popover.shared.tsx +124 -0
- package/src/atoms/popover/popover.styles.ts +144 -0
- package/src/atoms/popover/popover.tsx +6 -0
- package/src/atoms/radio/radio.android.tsx +6 -0
- package/src/atoms/radio/radio.ios.tsx +6 -0
- package/src/atoms/radio/radio.md +173 -0
- package/src/atoms/radio/radio.shared.tsx +98 -0
- package/src/atoms/radio/radio.styles.ts +109 -0
- package/src/atoms/radio/radio.tsx +6 -0
- package/src/atoms/select/select.android.tsx +6 -0
- package/src/atoms/select/select.ios.tsx +6 -0
- package/src/atoms/select/select.md +156 -0
- package/src/atoms/select/select.shared.tsx +143 -0
- package/src/atoms/select/select.styles.ts +310 -0
- package/src/atoms/select/select.tsx +6 -0
- package/src/atoms/skeleton/skeleton.md +135 -0
- package/src/atoms/skeleton/skeleton.styles.ts +117 -0
- package/src/atoms/skeleton/skeleton.tsx +145 -0
- package/src/atoms/spinner/spinner.android.tsx +7 -0
- package/src/atoms/spinner/spinner.ios.tsx +7 -0
- package/src/atoms/spinner/spinner.md +94 -0
- package/src/atoms/spinner/spinner.shared.tsx +92 -0
- package/src/atoms/spinner/spinner.styles.tsx +115 -0
- package/src/atoms/spinner/spinner.tsx +7 -0
- package/src/atoms/switch/switch.android.tsx +6 -0
- package/src/atoms/switch/switch.ios.tsx +6 -0
- package/src/atoms/switch/switch.md +91 -0
- package/src/atoms/switch/switch.shared.tsx +97 -0
- package/src/atoms/switch/switch.styles.ts +79 -0
- package/src/atoms/switch/switch.tsx +6 -0
- package/src/atoms/textarea/textarea.android.tsx +6 -0
- package/src/atoms/textarea/textarea.ios.tsx +6 -0
- package/src/atoms/textarea/textarea.md +140 -0
- package/src/atoms/textarea/textarea.shared.tsx +74 -0
- package/src/atoms/textarea/textarea.styles.ts +116 -0
- package/src/atoms/textarea/textarea.tsx +6 -0
- package/src/atoms/tooltip/tooltip.android.tsx +6 -0
- package/src/atoms/tooltip/tooltip.ios.tsx +7 -0
- package/src/atoms/tooltip/tooltip.md +122 -0
- package/src/atoms/tooltip/tooltip.shared.tsx +113 -0
- package/src/atoms/tooltip/tooltip.styles.ts +113 -0
- package/src/atoms/tooltip/tooltip.tsx +6 -0
- package/src/atoms/typography/typography.md +330 -0
- package/src/atoms/typography/typography.styles.ts +95 -0
- package/src/atoms/typography/typography.tsx +76 -0
- package/src/index.ts +12 -2
- package/src/molecules/action-panels/action-panels.md +133 -0
- package/src/molecules/action-panels/action-panels.styles.ts +39 -0
- package/src/molecules/action-panels/action-panels.tsx +113 -0
- package/src/molecules/alert/alert.md +119 -0
- package/src/molecules/alert/alert.styles.ts +88 -0
- package/src/molecules/alert/alert.tsx +74 -0
- package/src/molecules/alert-dialog/alert-dialog.android.tsx +6 -0
- package/src/molecules/alert-dialog/alert-dialog.ios.tsx +6 -0
- package/src/molecules/alert-dialog/alert-dialog.md +177 -0
- package/src/molecules/alert-dialog/alert-dialog.shared.tsx +187 -0
- package/src/molecules/alert-dialog/alert-dialog.styles.ts +248 -0
- package/src/molecules/alert-dialog/alert-dialog.tsx +6 -0
- package/src/molecules/card/card.md +190 -0
- package/src/molecules/card/card.styles.ts +67 -0
- package/src/molecules/card/card.tsx +176 -0
- package/src/molecules/code-block/code-block.md +159 -0
- package/src/molecules/code-block/code-block.styles.ts +167 -0
- package/src/molecules/code-block/code-block.tsx +176 -0
- package/src/molecules/description-lists/description-lists.md +129 -0
- package/src/molecules/description-lists/description-lists.styles.ts +102 -0
- package/src/molecules/description-lists/description-lists.tsx +133 -0
- package/src/molecules/empty-state/empty-state.md +218 -0
- package/src/molecules/empty-state/empty-state.styles.ts +63 -0
- package/src/molecules/empty-state/empty-state.tsx +77 -0
- package/src/molecules/feeds/feeds.md +102 -0
- package/src/molecules/feeds/feeds.styles.ts +120 -0
- package/src/molecules/feeds/feeds.tsx +167 -0
- package/src/molecules/field/field.md +117 -0
- package/src/molecules/field/field.styles.ts +85 -0
- package/src/molecules/field/field.tsx +175 -0
- package/src/molecules/fieldset/fieldset.md +141 -0
- package/src/molecules/fieldset/fieldset.styles.ts +79 -0
- package/src/molecules/fieldset/fieldset.tsx +182 -0
- package/src/molecules/form/form.md +137 -0
- package/src/molecules/form/form.styles.ts +39 -0
- package/src/molecules/form/form.tsx +246 -0
- package/src/molecules/grid-lists/grid-lists.md +114 -0
- package/src/molecules/grid-lists/grid-lists.styles.ts +79 -0
- package/src/molecules/grid-lists/grid-lists.tsx +157 -0
- package/src/molecules/index.ts +16 -0
- package/src/molecules/media-objects/media-objects.md +87 -0
- package/src/molecules/media-objects/media-objects.styles.ts +94 -0
- package/src/molecules/media-objects/media-objects.tsx +128 -0
- package/src/molecules/stacked-lists/stacked-lists.md +116 -0
- package/src/molecules/stacked-lists/stacked-lists.styles.ts +111 -0
- package/src/molecules/stacked-lists/stacked-lists.tsx +195 -0
- package/src/molecules/stats/stats.md +166 -0
- package/src/molecules/stats/stats.styles.ts +91 -0
- package/src/molecules/stats/stats.tsx +88 -0
- package/src/organisms/calendar/calendar.android.tsx +6 -0
- package/src/organisms/calendar/calendar.ios.tsx +6 -0
- package/src/organisms/calendar/calendar.md +114 -0
- package/src/organisms/calendar/calendar.shared.tsx +146 -0
- package/src/organisms/calendar/calendar.styles.ts +315 -0
- package/src/organisms/calendar/calendar.tsx +6 -0
- package/src/organisms/charts/charts.md +326 -0
- package/src/organisms/charts/charts.styles.ts +135 -0
- package/src/organisms/charts/charts.tsx +124 -0
- package/src/organisms/command/command.md +117 -0
- package/src/organisms/command/command.styles.ts +179 -0
- package/src/organisms/command/command.tsx +164 -0
- package/src/organisms/data-table/data-table.md +182 -0
- package/src/organisms/data-table/data-table.styles.ts +103 -0
- package/src/organisms/data-table/data-table.tsx +105 -0
- package/src/organisms/dialog/dialog.android.tsx +6 -0
- package/src/organisms/dialog/dialog.ios.tsx +6 -0
- package/src/organisms/dialog/dialog.md +271 -0
- package/src/organisms/dialog/dialog.shared.tsx +230 -0
- package/src/organisms/dialog/dialog.styles.ts +272 -0
- package/src/organisms/dialog/dialog.tsx +6 -0
- package/src/organisms/filter-panel/filter-panel.md +116 -0
- package/src/organisms/filter-panel/filter-panel.styles.ts +83 -0
- package/src/organisms/filter-panel/filter-panel.tsx +91 -0
- package/src/organisms/index.ts +13 -0
- package/src/organisms/navbars/navbars.android.tsx +6 -0
- package/src/organisms/navbars/navbars.ios.tsx +6 -0
- package/src/organisms/navbars/navbars.md +144 -0
- package/src/organisms/navbars/navbars.shared.tsx +137 -0
- package/src/organisms/navbars/navbars.styles.ts +251 -0
- package/src/organisms/navbars/navbars.tsx +6 -0
- package/src/organisms/overlays/overlays.android.tsx +6 -0
- package/src/organisms/overlays/overlays.ios.tsx +6 -0
- package/src/organisms/overlays/overlays.md +123 -0
- package/src/organisms/overlays/overlays.shared.tsx +175 -0
- package/src/organisms/overlays/overlays.styles.ts +309 -0
- package/src/organisms/overlays/overlays.tsx +6 -0
- package/src/organisms/row-menu/row-menu.android.tsx +6 -0
- package/src/organisms/row-menu/row-menu.ios.tsx +6 -0
- package/src/organisms/row-menu/row-menu.md +102 -0
- package/src/organisms/row-menu/row-menu.shared.tsx +105 -0
- package/src/organisms/row-menu/row-menu.styles.ts +262 -0
- package/src/organisms/row-menu/row-menu.tsx +6 -0
- package/src/organisms/sidebar/sidebar.android.tsx +6 -0
- package/src/organisms/sidebar/sidebar.ios.tsx +6 -0
- package/src/organisms/sidebar/sidebar.md +188 -0
- package/src/organisms/sidebar/sidebar.shared.tsx +167 -0
- package/src/organisms/sidebar/sidebar.styles.ts +262 -0
- package/src/organisms/sidebar/sidebar.tsx +6 -0
- package/src/organisms/stepper/stepper.android.tsx +6 -0
- package/src/organisms/stepper/stepper.ios.tsx +6 -0
- package/src/organisms/stepper/stepper.md +150 -0
- package/src/organisms/stepper/stepper.shared.tsx +158 -0
- package/src/organisms/stepper/stepper.styles.ts +280 -0
- package/src/organisms/stepper/stepper.tsx +6 -0
- package/src/organisms/tabs/tabs.android.tsx +6 -0
- package/src/organisms/tabs/tabs.ios.tsx +6 -0
- package/src/organisms/tabs/tabs.md +127 -0
- package/src/organisms/tabs/tabs.shared.tsx +281 -0
- package/src/organisms/tabs/tabs.styles.ts +398 -0
- package/src/organisms/tabs/tabs.tsx +6 -0
- package/src/style/color.ts +17 -0
- package/src/style/index.ts +14 -0
- package/src/style/primitives.ts +26 -0
- package/src/style/responsive.ts +45 -0
- package/src/style/shadow.ts +21 -0
- package/src/style/theme.tsx +56 -0
- package/src/style/tokens.ts +487 -0
- package/src/theme.ts +21 -0
- package/styles/canvas.css +128 -67
- package/tsconfig.json +4 -2
- package/src/cn.ts +0 -3
- package/styles/base.css +0 -17
- package/styles/components/alert.css +0 -66
- package/styles/components/app-shell.css +0 -46
- package/styles/components/avatar.css +0 -15
- package/styles/components/badge.css +0 -83
- package/styles/components/breadcrumb.css +0 -35
- package/styles/components/button-group.css +0 -23
- package/styles/components/button.css +0 -107
- package/styles/components/calendar.css +0 -73
- package/styles/components/card.css +0 -58
- package/styles/components/checkbox.css +0 -55
- package/styles/components/code-block.css +0 -18
- package/styles/components/combobox.css +0 -75
- package/styles/components/command.css +0 -94
- package/styles/components/data-table.css +0 -142
- package/styles/components/dialog.css +0 -72
- package/styles/components/dropdown.css +0 -54
- package/styles/components/empty-state.css +0 -17
- package/styles/components/field.css +0 -27
- package/styles/components/filter-panel.css +0 -58
- package/styles/components/form.css +0 -27
- package/styles/components/icon.css +0 -8
- package/styles/components/input-group.css +0 -45
- package/styles/components/input.css +0 -56
- package/styles/components/kbd.css +0 -15
- package/styles/components/page-header.css +0 -52
- package/styles/components/pagination.css +0 -48
- package/styles/components/popover.css +0 -14
- package/styles/components/radio.css +0 -28
- package/styles/components/row-menu.css +0 -69
- package/styles/components/section-card.css +0 -49
- package/styles/components/select.css +0 -57
- package/styles/components/separator.css +0 -32
- package/styles/components/sheet.css +0 -70
- package/styles/components/sidebar.css +0 -146
- package/styles/components/skeleton.css +0 -32
- package/styles/components/spinner.css +0 -26
- package/styles/components/stat-card.css +0 -71
- package/styles/components/stepper.css +0 -63
- package/styles/components/switch.css +0 -45
- package/styles/components/tabs.css +0 -40
- package/styles/components/textarea.css +0 -31
- package/styles/components/toast.css +0 -95
- package/styles/components/tooltip.css +0 -53
- package/styles/components/topbar.css +0 -24
- package/styles/components/typography.css +0 -105
- package/styles/patterns/backdrops.css +0 -35
- package/styles/patterns/density.css +0 -66
- package/styles/patterns/focus.css +0 -38
- package/styles/patterns/glass.css +0 -85
- package/styles/patterns/high-contrast.css +0 -70
- package/styles/patterns/reduced-motion.css +0 -12
- package/styles/patterns/scrollbar.css +0 -10
- package/styles/reset.css +0 -89
- package/styles/tokens/colors.css +0 -106
- package/styles/tokens/motion.css +0 -33
- package/styles/tokens/radius.css +0 -10
- package/styles/tokens/shadows.css +0 -35
- package/styles/tokens/spacing.css +0 -19
- package/styles/tokens/typography.css +0 -6
- package/styles/tokens/z-index.css +0 -12
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Button Groups
|
|
2
|
+
|
|
3
|
+
Segmented controls, split buttons, attached groups.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
<ButtonGroup segmented active={0} items={["Day", "Week", "Month"]} small />
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Variants
|
|
12
|
+
|
|
13
|
+
### Variant - attached
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
<ButtonGroup
|
|
17
|
+
stepper
|
|
18
|
+
items={[
|
|
19
|
+
"May 21",
|
|
20
|
+
"May 22",
|
|
21
|
+
"May 23",
|
|
22
|
+
"Today",
|
|
23
|
+
"May 25",
|
|
24
|
+
"May 26",
|
|
25
|
+
"May 27"
|
|
26
|
+
]}
|
|
27
|
+
active={3}
|
|
28
|
+
small
|
|
29
|
+
/>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Variant - split
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
<ButtonGroup
|
|
36
|
+
split
|
|
37
|
+
items={["Save"]}
|
|
38
|
+
menu={["Save as draft", "Save and close", "Save a copy"]}
|
|
39
|
+
small
|
|
40
|
+
/>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Size - default
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
<ButtonGroup segmented active={0} items={["Day", "Week", "Month"]} />
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Size - lg
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
<ButtonGroup segmented active={0} items={["Day", "Week", "Month"]} large />
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Disabled
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
<ButtonGroup segmented active={0} items={["Day", "Week", "Month"]} disabled small />
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Do & Don't
|
|
62
|
+
|
|
63
|
+
### Segmented
|
|
64
|
+
|
|
65
|
+
**Do** — Keep a segmented control to a few mutually-exclusive views.
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
<ButtonGroup segmented active={0} items={["Day", "Week", "Month"]} />
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Don't** — Past ~4 options a segmented control gets cramped and hard to scan; reach for a select.
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
<ButtonGroup segmented active={0} items={["Day", "Week", "Month", "Quarter", "Year", "5Y", "All"]} />
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Attached
|
|
78
|
+
|
|
79
|
+
**Do** — Reserve attached groups for closely-related actions like prev / today / next.
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
<ButtonGroup stepper active={1} items={["Yesterday", "Today", "Tomorrow"]} />
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Don't** — Attaching unrelated actions implies they belong to one control.
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
<ButtonGroup segmented active={-1} items={["Save", "Delete", "Export"]} />
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Split
|
|
92
|
+
|
|
93
|
+
**Do** — Separate the chevron with a hairline so the secondary menu reads as distinct.
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
<ButtonGroup split items={["Save"]} menu={["Save as draft", "Save and close", "Save a copy"]} />
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Don't** — With no divider the chevron looks like part of one button, hiding the menu.
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
<View style={{ flexDirection: "row", alignItems: "center", alignSelf: "flex-start" }}>
|
|
103
|
+
<Pressable
|
|
104
|
+
style={({ pressed }) => [
|
|
105
|
+
{ flexDirection: "row", alignItems: "center", justifyContent: "center", height: 36, paddingHorizontal: 16, borderTopLeftRadius: 6, borderBottomLeftRadius: 6, borderTopRightRadius: 0, borderBottomRightRadius: 0, backgroundColor: tokens.primary },
|
|
106
|
+
pressed ? { opacity: 0.9 } : null
|
|
107
|
+
]}
|
|
108
|
+
>
|
|
109
|
+
<Text style={{ fontWeight: "500", fontSize: 14, lineHeight: 20, color: tokens["primary-foreground"] }}>Save</Text>
|
|
110
|
+
</Pressable>
|
|
111
|
+
<Pressable
|
|
112
|
+
style={({ pressed }) => [
|
|
113
|
+
{ flexDirection: "row", alignItems: "center", justifyContent: "center", height: 36, paddingHorizontal: 8, borderTopRightRadius: 6, borderBottomRightRadius: 6, borderTopLeftRadius: 0, borderBottomLeftRadius: 0, backgroundColor: tokens.primary },
|
|
114
|
+
pressed ? { opacity: 0.9 } : null
|
|
115
|
+
]}
|
|
116
|
+
>
|
|
117
|
+
<Icon chevronDown primaryForeground size={16} />
|
|
118
|
+
</Pressable>
|
|
119
|
+
</View>
|
|
120
|
+
```
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { type GestureResponderEvent } from "react-native";
|
|
3
|
+
import { View, Pressable, Text, useTheme, type ColorTokens, type StyleProp, type ViewStyle, type TextStyle } from "../../style/index.js";
|
|
4
|
+
import { Icon } from "../icon/icon.js";
|
|
5
|
+
import * as s from "./button-group.styles.js";
|
|
6
|
+
|
|
7
|
+
// Shared ButtonGroup shell. The structure (the four kinds, their layout, the
|
|
8
|
+
// uncontrolled stepper position, the split dropdown), the accessibility, the
|
|
9
|
+
// kind/size precedence, and the handlers live here once; a platform file supplies
|
|
10
|
+
// only its skin (the native container shape, selected-segment treatment, label
|
|
11
|
+
// color, dividers, press feedback) and calls createButtonGroup.
|
|
12
|
+
//
|
|
13
|
+
// A button group is a horizontal row of buttons that read as one control.
|
|
14
|
+
//
|
|
15
|
+
// Four kinds, picked by boolean prop (first match wins):
|
|
16
|
+
// - `segmented` (default): attached segments sharing one control; one segment
|
|
17
|
+
// reads selected via `active`. Use for mutually exclusive views (Day / Week /
|
|
18
|
+
// Month). The native shape differs per OS: iOS draws a gray track with a
|
|
19
|
+
// raised white pill on the selected segment; Android a stadium-outlined group
|
|
20
|
+
// with a tonal selected fill; web the joined-buttons look with a solid fill.
|
|
21
|
+
// - `split`: a primary action attached to a chevron trigger, divided by a
|
|
22
|
+
// hairline; the chevron opens a dropdown of related actions (`menu`). Use
|
|
23
|
+
// for one primary action with a few related variants.
|
|
24
|
+
// - `stepper`: a prev / current / next control whose chevrons are built in;
|
|
25
|
+
// `items` is the list it cycles through (wrapping at the ends) and the middle
|
|
26
|
+
// label tracks the position. Use for stepping an ordered set (dates, pages).
|
|
27
|
+
// - `spaced`: a plain row of detached buttons separated by a gap. Use for
|
|
28
|
+
// a few peer actions that do not form a single control.
|
|
29
|
+
//
|
|
30
|
+
// Because there are no `first:` / `last:` style variants, the joined-corner and
|
|
31
|
+
// shared-border math is computed per segment in the skin rather than in markup.
|
|
32
|
+
|
|
33
|
+
export type Kind = "segmented" | "split" | "stepper" | "spaced";
|
|
34
|
+
export type Size = "small" | "default" | "large";
|
|
35
|
+
|
|
36
|
+
// Icon color axis (semantic boolean props), chosen by the skin per platform.
|
|
37
|
+
export type IconColor = "primary" | "primaryForeground" | "muted" | "foreground";
|
|
38
|
+
|
|
39
|
+
// Spread the chosen Icon color as its boolean prop (foreground is the default,
|
|
40
|
+
// so it needs none). Keeps the semantic prop API: no raw color strings.
|
|
41
|
+
function iconColorProps(c: IconColor) {
|
|
42
|
+
switch (c) {
|
|
43
|
+
case "primary": return { primary: true } as const;
|
|
44
|
+
case "primaryForeground": return { primaryForeground: true } as const;
|
|
45
|
+
case "muted": return { muted: true } as const;
|
|
46
|
+
case "foreground": return {} as const;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// The platform-varying surface. Everything color/shape-bearing the four kinds
|
|
51
|
+
// need lives here, built from the active tokens (so each follows light/dark/glass).
|
|
52
|
+
export interface ButtonGroupSkin {
|
|
53
|
+
// --- segmented / spaced ---
|
|
54
|
+
/** Optional gray track wrapping the segmented row (iOS); null = bare row. */
|
|
55
|
+
segmentedWrap: (t: ColorTokens) => ViewStyle | null;
|
|
56
|
+
/** Border width on each segment cell (0 on iOS/Android, 1 on web). */
|
|
57
|
+
segmentBorderWidth: number;
|
|
58
|
+
/** Corner radii for an attached segment given its position in the row. */
|
|
59
|
+
joinCorners: (index: number, count: number) => ViewStyle;
|
|
60
|
+
/** Corner radii for a detached (spaced) peer. */
|
|
61
|
+
spacedCorners: ViewStyle;
|
|
62
|
+
/** Shared-border overlap applied to every non-leading segment; null = none. */
|
|
63
|
+
overlap: ViewStyle | null;
|
|
64
|
+
/** Optional leading divider on every non-leading segment (Android stadium). */
|
|
65
|
+
segmentDivider?: (t: ColorTokens) => ViewStyle;
|
|
66
|
+
/** Selected vs. unselected segment fill/border. */
|
|
67
|
+
segmentSurface: (t: ColorTokens, selected: boolean) => ViewStyle;
|
|
68
|
+
/** Segment label color/weight for selected vs. unselected. */
|
|
69
|
+
segmentLabel: (t: ColorTokens, selected: boolean) => TextStyle;
|
|
70
|
+
/** Show a leading check glyph on the selected segment (Android M3). */
|
|
71
|
+
showSelectedCheck: boolean;
|
|
72
|
+
|
|
73
|
+
// --- split ---
|
|
74
|
+
splitPrimary: (t: ColorTokens) => ViewStyle;
|
|
75
|
+
splitPrimaryLabel: (t: ColorTokens) => TextStyle;
|
|
76
|
+
splitDivider: (t: ColorTokens, height: number) => ViewStyle;
|
|
77
|
+
splitTrigger: (t: ColorTokens, height: number) => ViewStyle;
|
|
78
|
+
splitChevronColor: IconColor;
|
|
79
|
+
splitMenu: (t: ColorTokens) => ViewStyle;
|
|
80
|
+
splitMenuItemPressed: (t: ColorTokens) => ViewStyle;
|
|
81
|
+
splitMenuText: (t: ColorTokens) => TextStyle;
|
|
82
|
+
|
|
83
|
+
// --- stepper ---
|
|
84
|
+
stepperArrow: (t: ColorTokens, height: number) => ViewStyle;
|
|
85
|
+
stepperArrowLeft: ViewStyle;
|
|
86
|
+
stepperArrowRight: ViewStyle;
|
|
87
|
+
stepperMiddle: (t: ColorTokens) => ViewStyle;
|
|
88
|
+
stepperLabel: (t: ColorTokens) => TextStyle;
|
|
89
|
+
stepperChevronColor: IconColor;
|
|
90
|
+
|
|
91
|
+
// --- feedback ---
|
|
92
|
+
/** iOS/web dim the cell on press; Android uses a ripple instead (null). */
|
|
93
|
+
pressedOpacity: number | null;
|
|
94
|
+
/** Android ripple over a pressed cell; null on iOS/web. */
|
|
95
|
+
ripple?: (t: ColorTokens) => { color: string; borderless: boolean };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface ButtonGroupProps {
|
|
99
|
+
/** Segment labels for segmented/spaced; the values the stepper cycles through. */
|
|
100
|
+
items?: string[];
|
|
101
|
+
/** Selected segment index (segmented), or the stepper's initial index. */
|
|
102
|
+
active?: number;
|
|
103
|
+
/** Called with the pressed/selected index and item (and, for the stepper, the new index). */
|
|
104
|
+
onSelect?: (index: number, item: string, event: GestureResponderEvent) => void;
|
|
105
|
+
|
|
106
|
+
// Kind (pick one; default is segmented).
|
|
107
|
+
segmented?: boolean;
|
|
108
|
+
split?: boolean;
|
|
109
|
+
stepper?: boolean;
|
|
110
|
+
spaced?: boolean;
|
|
111
|
+
|
|
112
|
+
/** Related actions shown in the split kind's chevron dropdown. */
|
|
113
|
+
menu?: string[];
|
|
114
|
+
|
|
115
|
+
// Size (pick one; default is the medium size).
|
|
116
|
+
small?: boolean;
|
|
117
|
+
large?: boolean;
|
|
118
|
+
|
|
119
|
+
disabled?: boolean;
|
|
120
|
+
/** Escape hatch for layout/positioning composition (margins, alignment). */
|
|
121
|
+
style?: StyleProp<ViewStyle>;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Kind precedence when more than one is passed: first match wins.
|
|
125
|
+
function kindOf(p: ButtonGroupProps): Kind {
|
|
126
|
+
if (p.segmented) return "segmented";
|
|
127
|
+
if (p.split) return "split";
|
|
128
|
+
if (p.stepper) return "stepper";
|
|
129
|
+
if (p.spaced) return "spaced";
|
|
130
|
+
return "segmented";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Size precedence when more than one is passed: first match wins.
|
|
134
|
+
function sizeOf(p: ButtonGroupProps): Size {
|
|
135
|
+
if (p.small) return "small";
|
|
136
|
+
if (p.large) return "large";
|
|
137
|
+
return "default";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const DEFAULT_ITEMS = ["Day", "Week", "Month"];
|
|
141
|
+
const DEFAULT_MENU = ["Save as draft", "Save and close", "Save a copy"];
|
|
142
|
+
|
|
143
|
+
/** Build a ButtonGroup component from a platform skin. */
|
|
144
|
+
export function createButtonGroup(skin: ButtonGroupSkin) {
|
|
145
|
+
const ripple = skin.ripple;
|
|
146
|
+
|
|
147
|
+
interface SegmentProps {
|
|
148
|
+
label: string;
|
|
149
|
+
selected: boolean;
|
|
150
|
+
/** Corner radii for this segment given its position in the row. */
|
|
151
|
+
corners: ViewStyle;
|
|
152
|
+
/** This segment overlaps the previous border / draws a leading divider. */
|
|
153
|
+
leading: boolean;
|
|
154
|
+
size: Size;
|
|
155
|
+
disabled?: boolean;
|
|
156
|
+
onPress?: (event: GestureResponderEvent) => void;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function Segment({ label, selected, corners, leading, size, disabled, onPress }: SegmentProps) {
|
|
160
|
+
const { tokens } = useTheme();
|
|
161
|
+
const container: StyleProp<ViewStyle> = [
|
|
162
|
+
s.segmentBase,
|
|
163
|
+
{ borderWidth: skin.segmentBorderWidth },
|
|
164
|
+
s.sizeContainer[size],
|
|
165
|
+
corners,
|
|
166
|
+
leading && skin.overlap ? skin.overlap : null,
|
|
167
|
+
leading && skin.segmentDivider ? skin.segmentDivider(tokens) : null,
|
|
168
|
+
skin.segmentSurface(tokens, selected),
|
|
169
|
+
disabled ? s.dim : null,
|
|
170
|
+
];
|
|
171
|
+
return (
|
|
172
|
+
<Pressable
|
|
173
|
+
style={({ pressed }) => [container, skin.pressedOpacity != null && pressed ? { opacity: skin.pressedOpacity } : null]}
|
|
174
|
+
onPress={onPress}
|
|
175
|
+
disabled={disabled}
|
|
176
|
+
android_ripple={ripple ? ripple(tokens) : undefined}
|
|
177
|
+
accessibilityRole="button"
|
|
178
|
+
accessibilityState={{ selected, disabled: !!disabled }}
|
|
179
|
+
>
|
|
180
|
+
{skin.showSelectedCheck && selected ? (
|
|
181
|
+
<Icon check primary size={s.chevronSize[size]} style={{ marginRight: 6 }} />
|
|
182
|
+
) : null}
|
|
183
|
+
<Text style={[s.sizeLabel[size], skin.segmentLabel(tokens, selected)]}>{label}</Text>
|
|
184
|
+
</Pressable>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// The split kind's secondary control: a chevron that toggles a floating dropdown
|
|
189
|
+
// of related actions, anchored to the right edge of the primary button. The
|
|
190
|
+
// menu floats (absolute) so it overflows the group rather than growing it.
|
|
191
|
+
function SplitButton({
|
|
192
|
+
primary,
|
|
193
|
+
menu,
|
|
194
|
+
size,
|
|
195
|
+
disabled,
|
|
196
|
+
onSelect,
|
|
197
|
+
style,
|
|
198
|
+
}: {
|
|
199
|
+
primary: string;
|
|
200
|
+
menu: string[];
|
|
201
|
+
size: Size;
|
|
202
|
+
disabled?: boolean;
|
|
203
|
+
onSelect?: (index: number, item: string, event: GestureResponderEvent) => void;
|
|
204
|
+
style?: StyleProp<ViewStyle>;
|
|
205
|
+
}) {
|
|
206
|
+
const { tokens } = useTheme();
|
|
207
|
+
const [open, setOpen] = useState(false);
|
|
208
|
+
const triggerHeight = s.sizeHeight[size];
|
|
209
|
+
return (
|
|
210
|
+
<View style={[s.splitContainer, open ? s.splitContainerLifted : null, disabled ? s.dim : null, style]}>
|
|
211
|
+
<Pressable
|
|
212
|
+
style={({ pressed }) => [skin.splitPrimary(tokens), s.sizeContainer[size], skin.pressedOpacity != null && pressed ? { opacity: skin.pressedOpacity } : null]}
|
|
213
|
+
onPress={(e) => onSelect?.(0, primary, e)}
|
|
214
|
+
disabled={disabled}
|
|
215
|
+
android_ripple={ripple ? ripple(tokens) : undefined}
|
|
216
|
+
accessibilityRole="button"
|
|
217
|
+
>
|
|
218
|
+
<Text style={[skin.splitPrimaryLabel(tokens), s.sizeLabel[size]]}>{primary}</Text>
|
|
219
|
+
</Pressable>
|
|
220
|
+
{/* Hairline divider so the chevron reads as a distinct trigger. */}
|
|
221
|
+
<View style={skin.splitDivider(tokens, triggerHeight)} />
|
|
222
|
+
<Pressable
|
|
223
|
+
style={({ pressed }) => [skin.splitTrigger(tokens, triggerHeight), skin.pressedOpacity != null && pressed ? { opacity: skin.pressedOpacity } : null]}
|
|
224
|
+
onPress={() => setOpen((o) => !o)}
|
|
225
|
+
disabled={disabled}
|
|
226
|
+
android_ripple={ripple ? ripple(tokens) : undefined}
|
|
227
|
+
accessibilityRole="button"
|
|
228
|
+
accessibilityState={{ expanded: open }}
|
|
229
|
+
accessibilityLabel="More actions"
|
|
230
|
+
>
|
|
231
|
+
<View style={{ transform: [{ rotate: open ? "180deg" : "0deg" }] }}>
|
|
232
|
+
<Icon chevronDown size={s.chevronSize[size]} {...iconColorProps(skin.splitChevronColor)} />
|
|
233
|
+
</View>
|
|
234
|
+
</Pressable>
|
|
235
|
+
{open ? (
|
|
236
|
+
<View style={skin.splitMenu(tokens)}>
|
|
237
|
+
{menu.map((item, i) => (
|
|
238
|
+
<Pressable
|
|
239
|
+
key={`${item}-${i}`}
|
|
240
|
+
style={({ pressed }) => [s.splitMenuItem, pressed ? skin.splitMenuItemPressed(tokens) : null]}
|
|
241
|
+
onPress={(e) => {
|
|
242
|
+
onSelect?.(i + 1, item, e);
|
|
243
|
+
setOpen(false);
|
|
244
|
+
}}
|
|
245
|
+
accessibilityRole="menuitem"
|
|
246
|
+
>
|
|
247
|
+
<Text style={skin.splitMenuText(tokens)}>{item}</Text>
|
|
248
|
+
</Pressable>
|
|
249
|
+
))}
|
|
250
|
+
</View>
|
|
251
|
+
) : null}
|
|
252
|
+
</View>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Stepper: a prev / current / next control. The chevrons are built in here; the
|
|
257
|
+
// `items` array is what it cycles through. Uncontrolled, it tracks its own
|
|
258
|
+
// position from the initial index, wraps at the ends, and reports each change
|
|
259
|
+
// through onSelect. The middle cell is a passive label showing the current item.
|
|
260
|
+
function Stepper({
|
|
261
|
+
items,
|
|
262
|
+
initial,
|
|
263
|
+
size,
|
|
264
|
+
disabled,
|
|
265
|
+
onSelect,
|
|
266
|
+
style,
|
|
267
|
+
}: {
|
|
268
|
+
items: string[];
|
|
269
|
+
initial: number;
|
|
270
|
+
size: Size;
|
|
271
|
+
disabled?: boolean;
|
|
272
|
+
onSelect?: (index: number, item: string, event: GestureResponderEvent) => void;
|
|
273
|
+
style?: StyleProp<ViewStyle>;
|
|
274
|
+
}) {
|
|
275
|
+
const { tokens } = useTheme();
|
|
276
|
+
const count = items.length;
|
|
277
|
+
const clamp = (n: number) => (count > 0 ? Math.min(Math.max(0, n), count - 1) : 0);
|
|
278
|
+
const [index, setIndex] = useState(() => clamp(initial));
|
|
279
|
+
const i = clamp(index);
|
|
280
|
+
const chevron = s.chevronSize[size];
|
|
281
|
+
const height = s.sizeHeight[size];
|
|
282
|
+
const step = (dir: number, e: GestureResponderEvent) => {
|
|
283
|
+
if (count === 0) return;
|
|
284
|
+
const next = (i + dir + count) % count;
|
|
285
|
+
setIndex(next);
|
|
286
|
+
onSelect?.(next, items[next], e);
|
|
287
|
+
};
|
|
288
|
+
return (
|
|
289
|
+
<View style={[s.stepperContainer, disabled ? s.dim : null, style]}>
|
|
290
|
+
<Pressable
|
|
291
|
+
style={({ pressed }) => [skin.stepperArrow(tokens, height), skin.stepperArrowLeft, skin.pressedOpacity != null && pressed ? { opacity: skin.pressedOpacity } : null]}
|
|
292
|
+
onPress={(e) => step(-1, e)}
|
|
293
|
+
disabled={disabled}
|
|
294
|
+
android_ripple={ripple ? ripple(tokens) : undefined}
|
|
295
|
+
accessibilityRole="button"
|
|
296
|
+
accessibilityLabel="Previous"
|
|
297
|
+
>
|
|
298
|
+
<Icon chevronLeft size={chevron} {...iconColorProps(skin.stepperChevronColor)} />
|
|
299
|
+
</Pressable>
|
|
300
|
+
<View style={[skin.stepperMiddle(tokens), s.sizeContainer[size]]}>
|
|
301
|
+
<Text style={[skin.stepperLabel(tokens), s.sizeLabel[size]]}>{items[i] ?? ""}</Text>
|
|
302
|
+
</View>
|
|
303
|
+
<Pressable
|
|
304
|
+
style={({ pressed }) => [skin.stepperArrow(tokens, height), skin.stepperArrowRight, skin.pressedOpacity != null && pressed ? { opacity: skin.pressedOpacity } : null]}
|
|
305
|
+
onPress={(e) => step(1, e)}
|
|
306
|
+
disabled={disabled}
|
|
307
|
+
android_ripple={ripple ? ripple(tokens) : undefined}
|
|
308
|
+
accessibilityRole="button"
|
|
309
|
+
accessibilityLabel="Next"
|
|
310
|
+
>
|
|
311
|
+
<Icon chevronRight size={chevron} {...iconColorProps(skin.stepperChevronColor)} />
|
|
312
|
+
</Pressable>
|
|
313
|
+
</View>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return function ButtonGroup(props: ButtonGroupProps) {
|
|
318
|
+
const { items = DEFAULT_ITEMS, active = 0, onSelect, disabled, style } = props;
|
|
319
|
+
const { tokens } = useTheme();
|
|
320
|
+
const kind = kindOf(props);
|
|
321
|
+
const size = sizeOf(props);
|
|
322
|
+
|
|
323
|
+
// Spaced: detached peers separated by a gap, each with full rounding.
|
|
324
|
+
if (kind === "spaced") {
|
|
325
|
+
return (
|
|
326
|
+
<View style={[s.spacedContainer, style]}>
|
|
327
|
+
{items.map((item, i) => (
|
|
328
|
+
<Segment
|
|
329
|
+
key={`${item}-${i}`}
|
|
330
|
+
label={item}
|
|
331
|
+
selected={false}
|
|
332
|
+
corners={skin.spacedCorners}
|
|
333
|
+
leading={false}
|
|
334
|
+
size={size}
|
|
335
|
+
disabled={disabled}
|
|
336
|
+
onPress={(e) => onSelect?.(i, item, e)}
|
|
337
|
+
/>
|
|
338
|
+
))}
|
|
339
|
+
</View>
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Split: a primary action attached to a chevron that opens a dropdown of
|
|
344
|
+
// related actions.
|
|
345
|
+
if (kind === "split") {
|
|
346
|
+
const labels = items.length > 0 ? items : DEFAULT_ITEMS;
|
|
347
|
+
const primary = labels[0] ?? "Save";
|
|
348
|
+
return (
|
|
349
|
+
<SplitButton
|
|
350
|
+
primary={primary}
|
|
351
|
+
menu={props.menu && props.menu.length > 0 ? props.menu : DEFAULT_MENU}
|
|
352
|
+
size={size}
|
|
353
|
+
disabled={disabled}
|
|
354
|
+
onSelect={onSelect}
|
|
355
|
+
style={style}
|
|
356
|
+
/>
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Stepper: a prev / current / next control that cycles through items; the
|
|
361
|
+
// component owns the chevrons and the position.
|
|
362
|
+
if (kind === "stepper") {
|
|
363
|
+
const list = items.length > 0 ? items : DEFAULT_ITEMS;
|
|
364
|
+
return (
|
|
365
|
+
<Stepper
|
|
366
|
+
items={list}
|
|
367
|
+
initial={active}
|
|
368
|
+
size={size}
|
|
369
|
+
disabled={disabled}
|
|
370
|
+
onSelect={onSelect}
|
|
371
|
+
style={style}
|
|
372
|
+
/>
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Segmented (default): attached segments, one selected. iOS wraps the row in
|
|
377
|
+
// a gray track; Android/web render the row bare (the stadium/joined borders
|
|
378
|
+
// are drawn by the segments themselves).
|
|
379
|
+
const count = items.length;
|
|
380
|
+
const wrap = skin.segmentedWrap(tokens);
|
|
381
|
+
const row = items.map((item, i) => (
|
|
382
|
+
<Segment
|
|
383
|
+
key={`${item}-${i}`}
|
|
384
|
+
label={item}
|
|
385
|
+
selected={i === active}
|
|
386
|
+
corners={skin.joinCorners(i, count)}
|
|
387
|
+
leading={i > 0}
|
|
388
|
+
size={size}
|
|
389
|
+
disabled={disabled}
|
|
390
|
+
onPress={(e) => onSelect?.(i, item, e)}
|
|
391
|
+
/>
|
|
392
|
+
));
|
|
393
|
+
if (wrap) {
|
|
394
|
+
return <View style={[wrap, style]}>{row}</View>;
|
|
395
|
+
}
|
|
396
|
+
return <View style={[s.segmentedContainer, style]}>{row}</View>;
|
|
397
|
+
};
|
|
398
|
+
}
|