@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,158 @@
|
|
|
1
|
+
import { type DimensionValue } from "react-native";
|
|
2
|
+
import { View, Pressable, Text, useTheme, type StyleProp, type ViewStyle } from "../../style/index.js";
|
|
3
|
+
import * as s from "./stepper.styles.js";
|
|
4
|
+
import { type State, type StepperSkin } from "./stepper.styles.js";
|
|
5
|
+
|
|
6
|
+
// Shared Stepper shell. The structure (the numbered/check circles joined by
|
|
7
|
+
// connectors, plus the vertical and progress-bar layouts), the layout
|
|
8
|
+
// precedence, the per-step state derivation, the press handlers, and the
|
|
9
|
+
// accessibility all live here once; a platform file supplies only its skin (the
|
|
10
|
+
// circle/connector colors, the connector cap, the current-step emphasis, and
|
|
11
|
+
// the press feedback) and calls createStepper.
|
|
12
|
+
//
|
|
13
|
+
// Boolean-prop API: layout is a single axis with `progress` and `vertical`
|
|
14
|
+
// opting out of the default horizontal layout (first-match precedence,
|
|
15
|
+
// mirroring Divider). `progress` beats `vertical` beats the horizontal default.
|
|
16
|
+
|
|
17
|
+
export interface Step {
|
|
18
|
+
label: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface StepperProps {
|
|
23
|
+
/** Ordered steps to render. Each is a label with an optional one-line note. */
|
|
24
|
+
steps: Step[];
|
|
25
|
+
/** Index of the active step. Earlier steps read completed, later ones muted. */
|
|
26
|
+
current: number;
|
|
27
|
+
// Layout (pick one; default is horizontal).
|
|
28
|
+
vertical?: boolean;
|
|
29
|
+
/** Render a labeled percentage progress bar instead of discrete steps. */
|
|
30
|
+
progress?: boolean;
|
|
31
|
+
/** Progress mode only: filled fraction, 0-100 (clamped). Defaults to 0. */
|
|
32
|
+
value?: number;
|
|
33
|
+
/** Progress mode only: caption shown left of the percentage. */
|
|
34
|
+
label?: string;
|
|
35
|
+
/** When set, each step circle is pressable, reporting the step index. */
|
|
36
|
+
onStepPress?: (index: number) => void;
|
|
37
|
+
/** Escape hatch for layout/positioning composition (width, margins). */
|
|
38
|
+
style?: StyleProp<ViewStyle>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type Layout = "horizontal" | "vertical" | "progress";
|
|
42
|
+
|
|
43
|
+
// First match wins when more than one layout flag is passed.
|
|
44
|
+
function layoutOf(p: StepperProps): Layout {
|
|
45
|
+
if (p.progress) return "progress";
|
|
46
|
+
if (p.vertical) return "vertical";
|
|
47
|
+
return "horizontal";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Each step's visual state derives from its index relative to `current`.
|
|
51
|
+
function stateOf(index: number, current: number): State {
|
|
52
|
+
if (index < current) return "completed";
|
|
53
|
+
if (index === current) return "current";
|
|
54
|
+
return "upcoming";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Build a Stepper component from a platform skin. */
|
|
58
|
+
export function createStepper(skin: StepperSkin) {
|
|
59
|
+
// The numbered/check disc. Pressable (and so opacity-dim / ripple) only when an
|
|
60
|
+
// onStepPress handler is supplied; otherwise a plain View.
|
|
61
|
+
function Circle({ index, state, onPress }: { index: number; state: State; onPress?: () => void }) {
|
|
62
|
+
const { tokens } = useTheme();
|
|
63
|
+
const glyph = (
|
|
64
|
+
<Text style={[s.glyphBase, skin.glyphState(tokens, state)]}>
|
|
65
|
+
{state === "completed" ? "✓" : String(index + 1)}
|
|
66
|
+
</Text>
|
|
67
|
+
);
|
|
68
|
+
if (onPress) {
|
|
69
|
+
const ripple = skin.ripple ? skin.ripple(tokens, state) : undefined;
|
|
70
|
+
return (
|
|
71
|
+
<Pressable
|
|
72
|
+
style={({ pressed }) => [
|
|
73
|
+
s.circleBase,
|
|
74
|
+
skin.circleState(tokens, state),
|
|
75
|
+
skin.pressedOpacity != null && pressed ? { opacity: skin.pressedOpacity } : null,
|
|
76
|
+
]}
|
|
77
|
+
android_ripple={ripple}
|
|
78
|
+
onPress={onPress}
|
|
79
|
+
accessibilityRole="button"
|
|
80
|
+
>
|
|
81
|
+
{glyph}
|
|
82
|
+
</Pressable>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return <View style={[s.circleBase, skin.circleState(tokens, state)]}>{glyph}</View>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return function Stepper(props: StepperProps) {
|
|
89
|
+
const { steps, current, value, label, onStepPress, style } = props;
|
|
90
|
+
const { tokens } = useTheme();
|
|
91
|
+
const layout = layoutOf(props);
|
|
92
|
+
|
|
93
|
+
if (layout === "progress") {
|
|
94
|
+
const pct = Math.max(0, Math.min(100, Math.round(value ?? 0)));
|
|
95
|
+
return (
|
|
96
|
+
<View style={[s.fullWidth, style]}>
|
|
97
|
+
<View style={s.progressHeader}>
|
|
98
|
+
<Text style={skin.progressCaption(tokens)}>{label ?? "Setup progress"}</Text>
|
|
99
|
+
<Text style={skin.progressPercent(tokens)}>{pct}%</Text>
|
|
100
|
+
</View>
|
|
101
|
+
<View style={skin.progressTrack(tokens)}>
|
|
102
|
+
<View style={[skin.progressFill(tokens), { width: `${pct}%` as DimensionValue }]} />
|
|
103
|
+
</View>
|
|
104
|
+
</View>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (layout === "vertical") {
|
|
109
|
+
return (
|
|
110
|
+
<View style={[s.fullWidth, style]}>
|
|
111
|
+
{steps.map((step, i) => {
|
|
112
|
+
const state = stateOf(i, current);
|
|
113
|
+
const isLast = i === steps.length - 1;
|
|
114
|
+
return (
|
|
115
|
+
<View key={i} style={s.verticalRow}>
|
|
116
|
+
<View style={s.verticalRail}>
|
|
117
|
+
<Circle index={i} state={state} onPress={onStepPress ? () => onStepPress(i) : undefined} />
|
|
118
|
+
{!isLast ? (
|
|
119
|
+
<View style={[s.verticalConnector, skin.connector(tokens, state === "completed")]} />
|
|
120
|
+
) : null}
|
|
121
|
+
</View>
|
|
122
|
+
<View style={[s.flex1, !isLast ? s.verticalContentSpacing : null]}>
|
|
123
|
+
<Text style={[s.labelBase, skin.labelState(tokens, state)]}>{step.label}</Text>
|
|
124
|
+
{step.description != null ? (
|
|
125
|
+
<Text style={skin.verticalDescription(tokens)}>{step.description}</Text>
|
|
126
|
+
) : null}
|
|
127
|
+
</View>
|
|
128
|
+
</View>
|
|
129
|
+
);
|
|
130
|
+
})}
|
|
131
|
+
</View>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Horizontal: a row of circle + label columns, joined by flex-filling rules.
|
|
136
|
+
return (
|
|
137
|
+
<View style={[s.horizontalRow, style]}>
|
|
138
|
+
{steps.map((step, i) => {
|
|
139
|
+
const state = stateOf(i, current);
|
|
140
|
+
const isLast = i === steps.length - 1;
|
|
141
|
+
return (
|
|
142
|
+
<View key={i} style={[s.horizontalRow, !isLast ? s.flex1 : null]}>
|
|
143
|
+
<View style={s.horizontalColumn}>
|
|
144
|
+
<Circle index={i} state={state} onPress={onStepPress ? () => onStepPress(i) : undefined} />
|
|
145
|
+
<Text style={[s.labelBaseXs, skin.labelState(tokens, state)]}>{step.label}</Text>
|
|
146
|
+
</View>
|
|
147
|
+
{!isLast ? (
|
|
148
|
+
// The connector after a step is "filled" once that step is
|
|
149
|
+
// completed (i.e. the next step has been reached).
|
|
150
|
+
<View style={[s.horizontalConnector, skin.connector(tokens, i < current)]} />
|
|
151
|
+
) : null}
|
|
152
|
+
</View>
|
|
153
|
+
);
|
|
154
|
+
})}
|
|
155
|
+
</View>
|
|
156
|
+
);
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { type ViewStyle, type TextStyle } from "react-native";
|
|
2
|
+
import { type ColorTokens, alpha } from "../../style/index.js";
|
|
3
|
+
|
|
4
|
+
// Co-located Stepper skins, one per platform. The Stepper is a LIGHT treatment:
|
|
5
|
+
// the same multi-step progress STRUCTURE on every platform (numbered/check
|
|
6
|
+
// circles joined by connectors, plus the vertical and progress-bar layouts),
|
|
7
|
+
// with only small native touches per OS. The BRAND survives everywhere (the
|
|
8
|
+
// indigo `primary` token and the semantic tokens, never a platform default);
|
|
9
|
+
// only the indicator shape, the connector cap, the current-step emphasis, and
|
|
10
|
+
// the press feedback change per OS:
|
|
11
|
+
// iOS (HIG): circular step indicators with ROUNDED connector caps; completed =
|
|
12
|
+
// filled `primary`; current = a `primary` ring on a transparent fill (a
|
|
13
|
+
// subtle inner ring via a 1.5px `ring`-token halo); press = opacity dim.
|
|
14
|
+
// Android (M3): step indicators with FLAT (square-capped) connectors; completed
|
|
15
|
+
// = filled `primary`; current = `primary` ring; press = android_ripple on
|
|
16
|
+
// interactive step circles.
|
|
17
|
+
// Web: the established Canvas look (lifted verbatim from the original file).
|
|
18
|
+
//
|
|
19
|
+
// The State axis (completed / current / upcoming) maps each colored part to a
|
|
20
|
+
// token set. Layout-only fragments are shared static objects (below); anything
|
|
21
|
+
// reading a color is a function of the active tokens.
|
|
22
|
+
|
|
23
|
+
export type State = "completed" | "current" | "upcoming";
|
|
24
|
+
|
|
25
|
+
// =============================================================================
|
|
26
|
+
// Shared layout fragments (color-free; identical across platforms).
|
|
27
|
+
// =============================================================================
|
|
28
|
+
|
|
29
|
+
// flex-1 (grow + shrink, zero basis) shared by connectors and the columns that
|
|
30
|
+
// fill the remaining row width.
|
|
31
|
+
export const flex1: ViewStyle = { flexGrow: 1, flexShrink: 1, flexBasis: "0%" };
|
|
32
|
+
|
|
33
|
+
// h-8 w-8 shrink-0 flex-row items-center justify-center rounded-full border-2
|
|
34
|
+
export const circleBase: ViewStyle = {
|
|
35
|
+
height: 32,
|
|
36
|
+
width: 32,
|
|
37
|
+
flexShrink: 0,
|
|
38
|
+
flexDirection: "row",
|
|
39
|
+
alignItems: "center",
|
|
40
|
+
justifyContent: "center",
|
|
41
|
+
borderRadius: 9999,
|
|
42
|
+
borderWidth: 2,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// text-sm font-medium (the check / number inside the circle).
|
|
46
|
+
export const glyphBase: TextStyle = { fontSize: 14, lineHeight: 20, fontWeight: "500" };
|
|
47
|
+
|
|
48
|
+
// text-sm font-medium (vertical layout label).
|
|
49
|
+
export const labelBase: TextStyle = { fontSize: 14, lineHeight: 20, fontWeight: "500" };
|
|
50
|
+
|
|
51
|
+
// text-xs font-medium (horizontal layout label), colored by labelState.
|
|
52
|
+
export const labelBaseXs: TextStyle = { fontSize: 12, lineHeight: 16, fontWeight: "500" };
|
|
53
|
+
|
|
54
|
+
// w-full
|
|
55
|
+
export const fullWidth: ViewStyle = { width: "100%" };
|
|
56
|
+
|
|
57
|
+
// mb-1.5 flex-row items-center justify-between
|
|
58
|
+
export const progressHeader: ViewStyle = {
|
|
59
|
+
marginBottom: 6,
|
|
60
|
+
flexDirection: "row",
|
|
61
|
+
alignItems: "center",
|
|
62
|
+
justifyContent: "space-between",
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// flex-row gap-3 (the row pairing the rail with the content column).
|
|
66
|
+
export const verticalRow: ViewStyle = { flexDirection: "row", gap: 12 };
|
|
67
|
+
|
|
68
|
+
// items-center (the left rail: circle stacked over its connector).
|
|
69
|
+
export const verticalRail: ViewStyle = { alignItems: "center" };
|
|
70
|
+
|
|
71
|
+
// my-1 w-px flex-1 (the vertical connector) — color/cap come from the skin.
|
|
72
|
+
export const verticalConnector: ViewStyle = {
|
|
73
|
+
marginVertical: 4,
|
|
74
|
+
width: 1,
|
|
75
|
+
flexGrow: 1,
|
|
76
|
+
flexShrink: 1,
|
|
77
|
+
flexBasis: "0%",
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// pb-6 (bottom inset on a content column that has a step after it).
|
|
81
|
+
export const verticalContentSpacing: ViewStyle = { paddingBottom: 24 };
|
|
82
|
+
|
|
83
|
+
// flex-row items-start (the outer row and each step item).
|
|
84
|
+
export const horizontalRow: ViewStyle = { flexDirection: "row", alignItems: "flex-start" };
|
|
85
|
+
|
|
86
|
+
// items-center gap-1.5 (the circle + label column).
|
|
87
|
+
export const horizontalColumn: ViewStyle = { alignItems: "center", gap: 6 };
|
|
88
|
+
|
|
89
|
+
// mx-2 mt-4 h-px flex-1 (the horizontal connector) — color/cap come from the skin.
|
|
90
|
+
export const horizontalConnector: ViewStyle = {
|
|
91
|
+
marginHorizontal: 8,
|
|
92
|
+
marginTop: 16,
|
|
93
|
+
height: 1,
|
|
94
|
+
flexGrow: 1,
|
|
95
|
+
flexShrink: 1,
|
|
96
|
+
flexBasis: "0%",
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// =============================================================================
|
|
100
|
+
// The skin contract: only the colored, platform-varying parts.
|
|
101
|
+
// =============================================================================
|
|
102
|
+
|
|
103
|
+
export interface StepperSkin {
|
|
104
|
+
/** Fill + border of the numbered/check circle for a given step state. */
|
|
105
|
+
circleState: (t: ColorTokens, state: State) => ViewStyle;
|
|
106
|
+
/** Color of the glyph (check / number) inside the circle. */
|
|
107
|
+
glyphState: (t: ColorTokens, state: State) => TextStyle;
|
|
108
|
+
/** Color of the step label for a given state (shared across platforms). */
|
|
109
|
+
labelState: (t: ColorTokens, state: State) => TextStyle;
|
|
110
|
+
/** Background + line cap of a connector; `done` fills it primary. */
|
|
111
|
+
connector: (t: ColorTokens, done: boolean) => ViewStyle;
|
|
112
|
+
/** Caption left of the percentage in the progress layout. */
|
|
113
|
+
progressCaption: (t: ColorTokens) => TextStyle;
|
|
114
|
+
/** The percentage label in the progress layout. */
|
|
115
|
+
progressPercent: (t: ColorTokens) => TextStyle;
|
|
116
|
+
/** The progress track (unfilled). */
|
|
117
|
+
progressTrack: (t: ColorTokens) => ViewStyle;
|
|
118
|
+
/** The filled portion of the progress track (width set inline as %). */
|
|
119
|
+
progressFill: (t: ColorTokens) => ViewStyle;
|
|
120
|
+
/** The optional description line in the vertical layout. */
|
|
121
|
+
verticalDescription: (t: ColorTokens) => TextStyle;
|
|
122
|
+
/** iOS/web dim the circle on press; Android uses a ripple (null here). */
|
|
123
|
+
pressedOpacity: number | null;
|
|
124
|
+
/** Android ripple over a pressable circle; null on iOS/web. */
|
|
125
|
+
ripple: ((t: ColorTokens, state: State) => { color: string; borderless: boolean }) | null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// --- shared brand mapping (label colors are identical across platforms) ------
|
|
129
|
+
|
|
130
|
+
// Upcoming steps drop to the muted foreground; completed/current stay foreground.
|
|
131
|
+
function labelState(t: ColorTokens, state: State): TextStyle {
|
|
132
|
+
switch (state) {
|
|
133
|
+
case "completed": return { color: t.foreground };
|
|
134
|
+
case "current": return { color: t.foreground };
|
|
135
|
+
case "upcoming": return { color: t["muted-foreground"] };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// =============================================================================
|
|
140
|
+
// Web: the established Canvas look (lifted verbatim from the original file).
|
|
141
|
+
// =============================================================================
|
|
142
|
+
|
|
143
|
+
export const webSkin: StepperSkin = {
|
|
144
|
+
// Completed fills primary; current is outlined in primary on a transparent fill;
|
|
145
|
+
// upcoming sits on the muted token behind a border-toned ring.
|
|
146
|
+
circleState(t, state) {
|
|
147
|
+
switch (state) {
|
|
148
|
+
case "completed": return { borderColor: t.primary, backgroundColor: t.primary };
|
|
149
|
+
case "current": return { borderColor: t.primary, backgroundColor: "transparent" };
|
|
150
|
+
case "upcoming": return { borderColor: t.border, backgroundColor: t.muted };
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
glyphState(t, state) {
|
|
154
|
+
switch (state) {
|
|
155
|
+
case "completed": return { color: t["primary-foreground"] };
|
|
156
|
+
case "current": return { color: t.primary };
|
|
157
|
+
case "upcoming": return { color: t["muted-foreground"] };
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
labelState,
|
|
161
|
+
// A connector fills primary once the step it leads out of is completed, else it
|
|
162
|
+
// tracks the border token. Square caps (web rules are plain rects).
|
|
163
|
+
connector(t, done) {
|
|
164
|
+
return { backgroundColor: done ? t.primary : t.border };
|
|
165
|
+
},
|
|
166
|
+
progressCaption(t) {
|
|
167
|
+
return { fontSize: 12, lineHeight: 16, fontWeight: "500", color: t.foreground };
|
|
168
|
+
},
|
|
169
|
+
progressPercent(t) {
|
|
170
|
+
return { fontSize: 12, lineHeight: 16, color: t["muted-foreground"] };
|
|
171
|
+
},
|
|
172
|
+
progressTrack(t) {
|
|
173
|
+
return { height: 6, overflow: "hidden", borderRadius: 9999, backgroundColor: t.muted };
|
|
174
|
+
},
|
|
175
|
+
progressFill(t) {
|
|
176
|
+
return { height: "100%", borderRadius: 9999, backgroundColor: t.primary };
|
|
177
|
+
},
|
|
178
|
+
verticalDescription(t) {
|
|
179
|
+
return { fontSize: 12, lineHeight: 16, color: t["muted-foreground"] };
|
|
180
|
+
},
|
|
181
|
+
pressedOpacity: 0.9,
|
|
182
|
+
ripple: null,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// =============================================================================
|
|
186
|
+
// iOS (HIG): circular indicators, rounded connector caps, primary ring current.
|
|
187
|
+
// =============================================================================
|
|
188
|
+
|
|
189
|
+
export const iosSkin: StepperSkin = {
|
|
190
|
+
circleState(t, state) {
|
|
191
|
+
switch (state) {
|
|
192
|
+
// Completed reads as a solid filled `primary` disc.
|
|
193
|
+
case "completed": return { borderColor: t.primary, backgroundColor: t.primary };
|
|
194
|
+
// Current = a clean `primary` ring on a transparent fill (no muted backing,
|
|
195
|
+
// so it reads as an open ring the way iOS step dots do).
|
|
196
|
+
case "current": return { borderColor: t.primary, backgroundColor: "transparent" };
|
|
197
|
+
// Upcoming = a soft muted disc behind a border-toned ring.
|
|
198
|
+
case "upcoming": return { borderColor: t.border, backgroundColor: t.muted };
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
glyphState(t, state) {
|
|
202
|
+
switch (state) {
|
|
203
|
+
case "completed": return { color: t["primary-foreground"] };
|
|
204
|
+
case "current": return { color: t.primary };
|
|
205
|
+
case "upcoming": return { color: t["muted-foreground"] };
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
labelState,
|
|
209
|
+
// iOS connectors get rounded caps so the rule reads as a soft rounded line.
|
|
210
|
+
connector(t, done) {
|
|
211
|
+
return { backgroundColor: done ? t.primary : t.border, borderRadius: 9999 };
|
|
212
|
+
},
|
|
213
|
+
progressCaption(t) {
|
|
214
|
+
return { fontSize: 13, lineHeight: 18, fontWeight: "600", color: t.foreground };
|
|
215
|
+
},
|
|
216
|
+
progressPercent(t) {
|
|
217
|
+
return { fontSize: 13, lineHeight: 18, color: t["muted-foreground"] };
|
|
218
|
+
},
|
|
219
|
+
progressTrack(t) {
|
|
220
|
+
return { height: 6, overflow: "hidden", borderRadius: 9999, backgroundColor: t.muted };
|
|
221
|
+
},
|
|
222
|
+
progressFill(t) {
|
|
223
|
+
return { height: "100%", borderRadius: 9999, backgroundColor: t.primary };
|
|
224
|
+
},
|
|
225
|
+
verticalDescription(t) {
|
|
226
|
+
return { fontSize: 13, lineHeight: 18, color: t["muted-foreground"] };
|
|
227
|
+
},
|
|
228
|
+
// HIG press = opacity dim.
|
|
229
|
+
pressedOpacity: 0.8,
|
|
230
|
+
ripple: null,
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// =============================================================================
|
|
234
|
+
// Android (M3): flat connector caps, filled-primary complete, primary ring current.
|
|
235
|
+
// =============================================================================
|
|
236
|
+
|
|
237
|
+
export const androidSkin: StepperSkin = {
|
|
238
|
+
circleState(t, state) {
|
|
239
|
+
switch (state) {
|
|
240
|
+
// Completed = filled primary.
|
|
241
|
+
case "completed": return { borderColor: t.primary, backgroundColor: t.primary };
|
|
242
|
+
// Current = a primary ring; M3 keeps a faint tonal backing so the active
|
|
243
|
+
// indicator reads against the surface (secondaryContainer ≈ alpha(primary,.12)).
|
|
244
|
+
case "current": return { borderColor: t.primary, backgroundColor: alpha(t.primary, 0.12) };
|
|
245
|
+
// Upcoming = a muted disc behind a border-toned ring.
|
|
246
|
+
case "upcoming": return { borderColor: t.border, backgroundColor: t.muted };
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
glyphState(t, state) {
|
|
250
|
+
switch (state) {
|
|
251
|
+
case "completed": return { color: t["primary-foreground"] };
|
|
252
|
+
case "current": return { color: t.primary };
|
|
253
|
+
case "upcoming": return { color: t["muted-foreground"] };
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
labelState,
|
|
257
|
+
// M3 connectors are flat (square caps) — explicit radius 0 so the rule reads
|
|
258
|
+
// as a crisp Material divider, not a rounded line.
|
|
259
|
+
connector(t, done) {
|
|
260
|
+
return { backgroundColor: done ? t.primary : t.border, borderRadius: 0 };
|
|
261
|
+
},
|
|
262
|
+
progressCaption(t) {
|
|
263
|
+
return { fontSize: 12, lineHeight: 16, fontWeight: "500", color: t.foreground };
|
|
264
|
+
},
|
|
265
|
+
progressPercent(t) {
|
|
266
|
+
return { fontSize: 12, lineHeight: 16, color: t["muted-foreground"] };
|
|
267
|
+
},
|
|
268
|
+
progressTrack(t) {
|
|
269
|
+
return { height: 4, overflow: "hidden", borderRadius: 9999, backgroundColor: t.muted };
|
|
270
|
+
},
|
|
271
|
+
progressFill(t) {
|
|
272
|
+
return { height: "100%", borderRadius: 9999, backgroundColor: t.primary };
|
|
273
|
+
},
|
|
274
|
+
verticalDescription(t) {
|
|
275
|
+
return { fontSize: 12, lineHeight: 16, color: t["muted-foreground"] };
|
|
276
|
+
},
|
|
277
|
+
// M3 press = android_ripple over the interactive circle.
|
|
278
|
+
pressedOpacity: null,
|
|
279
|
+
ripple: (t) => ({ color: alpha(t.primary, 0.12), borderless: false }),
|
|
280
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createStepper } from "./stepper.shared.js";
|
|
2
|
+
import { webSkin } from "./stepper.styles.js";
|
|
3
|
+
|
|
4
|
+
// Web Stepper (the base; Metro falls back to it on native, web bundlers resolve it).
|
|
5
|
+
export const Stepper = createStepper(webSkin);
|
|
6
|
+
export type { Step, StepperProps } from "./stepper.shared.js";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createTabs } from "./tabs.shared.js";
|
|
2
|
+
import { androidSkin } from "./tabs.styles.js";
|
|
3
|
+
|
|
4
|
+
// Material 3 (underline tabs) Tabs. Metro resolves this file on Android; the docs import it for preview.
|
|
5
|
+
export const Tabs = createTabs(androidSkin);
|
|
6
|
+
export type { TabsProps, TabItem } from "./tabs.shared.js";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createTabs } from "./tabs.shared.js";
|
|
2
|
+
import { iosSkin } from "./tabs.styles.js";
|
|
3
|
+
|
|
4
|
+
// iOS (HIG segmented tab strip) Tabs. Metro resolves this file on iOS; the docs import it for preview.
|
|
5
|
+
export const Tabs = createTabs(iosSkin);
|
|
6
|
+
export type { TabsProps, TabItem } from "./tabs.shared.js";
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Tabs
|
|
2
|
+
|
|
3
|
+
Underline, pill, vertical, with badges.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
<Tabs
|
|
9
|
+
tabs={[
|
|
10
|
+
"General",
|
|
11
|
+
"Security",
|
|
12
|
+
"Notifications",
|
|
13
|
+
"Billing",
|
|
14
|
+
"Integrations"
|
|
15
|
+
]}
|
|
16
|
+
active={0}
|
|
17
|
+
/>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Variants
|
|
21
|
+
|
|
22
|
+
### Variant - pill
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
<Tabs pills tabs={["All", "Active", "Archived", "Deleted"]} active={0} />
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Variant - vertical
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
<Tabs
|
|
32
|
+
vertical
|
|
33
|
+
tabs={["General", "Security", "Notifications", "API Keys", "Billing"]}
|
|
34
|
+
active={0}
|
|
35
|
+
/>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Badge counts
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
<Tabs
|
|
42
|
+
tabs={[
|
|
43
|
+
{ label: "All", badge: "142" },
|
|
44
|
+
{ label: "Active", badge: "89" },
|
|
45
|
+
{ label: "Pending", badge: "12" },
|
|
46
|
+
{ label: "Archived", badge: "53" }
|
|
47
|
+
]}
|
|
48
|
+
active={0}
|
|
49
|
+
/>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Do & Don't
|
|
53
|
+
|
|
54
|
+
### Underline
|
|
55
|
+
|
|
56
|
+
**Do** — Underline and foreground-color only the active tab; leave the rest muted with no rule.
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
<Tabs tabs={["Overview", "Activity", "Settings"]} active={0} />
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Don't** — Underlining every tab erases the active indicator: there is no way to tell which view is current.
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
<View style={{ flexDirection: "row", alignItems: "center", borderBottomWidth: 1, borderColor: tokens.border, alignSelf: "flex-start" }}>
|
|
66
|
+
<Pressable style={({ pressed }) => [{ flexDirection: "row", alignItems: "center", justifyContent: "center", paddingHorizontal: 16, paddingVertical: 10 }, pressed ? { opacity: 0.9 } : null]}>
|
|
67
|
+
<Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>Overview</Text>
|
|
68
|
+
<View style={{ position: "absolute", bottom: 0, left: 0, right: 0, height: 2, borderRadius: 9999, backgroundColor: tokens.primary }} />
|
|
69
|
+
</Pressable>
|
|
70
|
+
<Pressable style={({ pressed }) => [{ flexDirection: "row", alignItems: "center", justifyContent: "center", paddingHorizontal: 16, paddingVertical: 10 }, pressed ? { opacity: 0.9 } : null]}>
|
|
71
|
+
<Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>Activity</Text>
|
|
72
|
+
<View style={{ position: "absolute", bottom: 0, left: 0, right: 0, height: 2, borderRadius: 9999, backgroundColor: tokens.primary }} />
|
|
73
|
+
</Pressable>
|
|
74
|
+
<Pressable style={({ pressed }) => [{ flexDirection: "row", alignItems: "center", justifyContent: "center", paddingHorizontal: 16, paddingVertical: 10 }, pressed ? { opacity: 0.9 } : null]}>
|
|
75
|
+
<Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>Settings</Text>
|
|
76
|
+
<View style={{ position: "absolute", bottom: 0, left: 0, right: 0, height: 2, borderRadius: 9999, backgroundColor: tokens.primary }} />
|
|
77
|
+
</Pressable>
|
|
78
|
+
</View>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Pill
|
|
82
|
+
|
|
83
|
+
**Do** — Exactly one pill gets the elevated background; the rest sit flat on the muted track.
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
<Tabs tabs={["All", "Active", "Archived"]} active={0} pills />
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Don't** — Giving every pill the raised background makes the group read as three buttons, not one selection.
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 4, alignSelf: "flex-start", borderRadius: 8, backgroundColor: tokens.muted, padding: 4 }}>
|
|
93
|
+
<Pressable style={({ pressed }) => [{ flexDirection: "row", alignItems: "center", justifyContent: "center", borderRadius: 6, backgroundColor: tokens.background, ...shadow("sm"), paddingHorizontal: 12, paddingVertical: 6 }, pressed ? { opacity: 0.9 } : null]}>
|
|
94
|
+
<Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>All</Text>
|
|
95
|
+
</Pressable>
|
|
96
|
+
<Pressable style={({ pressed }) => [{ flexDirection: "row", alignItems: "center", justifyContent: "center", borderRadius: 6, backgroundColor: tokens.background, ...shadow("sm"), paddingHorizontal: 12, paddingVertical: 6 }, pressed ? { opacity: 0.9 } : null]}>
|
|
97
|
+
<Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>Active</Text>
|
|
98
|
+
</Pressable>
|
|
99
|
+
<Pressable style={({ pressed }) => [{ flexDirection: "row", alignItems: "center", justifyContent: "center", borderRadius: 6, backgroundColor: tokens.background, ...shadow("sm"), paddingHorizontal: 12, paddingVertical: 6 }, pressed ? { opacity: 0.9 } : null]}>
|
|
100
|
+
<Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>Archived</Text>
|
|
101
|
+
</Pressable>
|
|
102
|
+
</View>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Vertical
|
|
106
|
+
|
|
107
|
+
**Do** — Fill the active rail item with the accent background so the selected pane is unmistakable.
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
<Tabs tabs={["General", "Security", "Notifications"]} active={0} vertical />
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Don't** — With no filled active item the rail collapses into a plain link list and loses its current selection.
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
<View style={{ flexDirection: "column", alignItems: "stretch", gap: 4, width: 180 }}>
|
|
117
|
+
<Pressable style={({ pressed }) => [{ width: "100%", flexDirection: "row", alignItems: "center", borderRadius: 6, backgroundColor: "transparent", paddingHorizontal: 12, paddingVertical: 8 }, pressed ? { opacity: 0.9 } : null]}>
|
|
118
|
+
<Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens["muted-foreground"] }}>General</Text>
|
|
119
|
+
</Pressable>
|
|
120
|
+
<Pressable style={({ pressed }) => [{ width: "100%", flexDirection: "row", alignItems: "center", borderRadius: 6, backgroundColor: "transparent", paddingHorizontal: 12, paddingVertical: 8 }, pressed ? { opacity: 0.9 } : null]}>
|
|
121
|
+
<Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens["muted-foreground"] }}>Security</Text>
|
|
122
|
+
</Pressable>
|
|
123
|
+
<Pressable style={({ pressed }) => [{ width: "100%", flexDirection: "row", alignItems: "center", borderRadius: 6, backgroundColor: "transparent", paddingHorizontal: 12, paddingVertical: 8 }, pressed ? { opacity: 0.9 } : null]}>
|
|
124
|
+
<Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens["muted-foreground"] }}>Notifications</Text>
|
|
125
|
+
</Pressable>
|
|
126
|
+
</View>
|
|
127
|
+
```
|