@olympusoss/canvas 3.2.1 → 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 +75 -65
- package/package.json +11 -5
- 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/styles/canvas.css +127 -74
- package/tsconfig.json +4 -2
- package/src/cn.ts +0 -3
- package/styles/atoms/avatar.css +0 -22
- package/styles/atoms/badge.css +0 -83
- package/styles/atoms/breadcrumb.css +0 -35
- package/styles/atoms/button-group.css +0 -23
- package/styles/atoms/button.css +0 -107
- package/styles/atoms/checkbox.css +0 -55
- package/styles/atoms/combobox.css +0 -76
- package/styles/atoms/dropdown.css +0 -54
- package/styles/atoms/icon.css +0 -8
- package/styles/atoms/input-group.css +0 -45
- package/styles/atoms/input.css +0 -56
- package/styles/atoms/kbd.css +0 -15
- package/styles/atoms/pagination.css +0 -48
- package/styles/atoms/popover.css +0 -14
- package/styles/atoms/radio.css +0 -28
- package/styles/atoms/select.css +0 -57
- package/styles/atoms/separator.css +0 -32
- package/styles/atoms/skeleton.css +0 -32
- package/styles/atoms/spinner.css +0 -26
- package/styles/atoms/switch.css +0 -45
- package/styles/atoms/textarea.css +0 -31
- package/styles/atoms/tooltip.css +0 -53
- package/styles/atoms/typography.css +0 -105
- package/styles/base.css +0 -17
- package/styles/molecules/alert.css +0 -66
- package/styles/molecules/card.css +0 -58
- package/styles/molecules/code-block.css +0 -18
- package/styles/molecules/empty-state.css +0 -17
- package/styles/molecules/field.css +0 -27
- package/styles/molecules/form.css +0 -27
- package/styles/molecules/page-header.css +0 -52
- package/styles/molecules/section-card.css +0 -49
- package/styles/molecules/stat-card.css +0 -71
- package/styles/molecules/toast.css +0 -95
- package/styles/organisms/app-shell.css +0 -46
- package/styles/organisms/calendar.css +0 -73
- package/styles/organisms/command.css +0 -95
- package/styles/organisms/data-table.css +0 -142
- package/styles/organisms/dialog.css +0 -72
- package/styles/organisms/filter-panel.css +0 -58
- package/styles/organisms/row-menu.css +0 -69
- package/styles/organisms/sheet.css +0 -70
- package/styles/organisms/sidebar.css +0 -146
- package/styles/organisms/stepper.css +0 -63
- package/styles/organisms/tabs.css +0 -40
- package/styles/organisms/topbar.css +0 -24
- package/styles/patterns/backdrops.css +0 -35
- package/styles/patterns/density.css +0 -66
- package/styles/patterns/focus.css +0 -22
- 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 -108
- 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
- package/styles/utilities/display.css +0 -66
- package/styles/utilities/flexbox.css +0 -240
- package/styles/utilities/gap.css +0 -288
- package/styles/utilities/grid.css +0 -138
- package/styles/utilities/position.css +0 -78
- package/styles/utilities/sizing.css +0 -138
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { View, Pressable, Text, useTheme, type ColorTokens, type StyleProp, type ViewStyle, type TextStyle } from "../../style/index.js";
|
|
2
|
+
import * as s from "./tabs.styles.js";
|
|
3
|
+
import { type Variant } from "./tabs.styles.js";
|
|
4
|
+
|
|
5
|
+
// Shared Tabs shell. The structure (the three looks, their layout, the active
|
|
6
|
+
// selection, the optional count badge), the accessibility, the look precedence,
|
|
7
|
+
// and the press handlers live here once; a platform file supplies only its skin
|
|
8
|
+
// (the native row/trigger shape, selected-tab treatment, indicator, label color,
|
|
9
|
+
// press feedback) and calls createTabs.
|
|
10
|
+
//
|
|
11
|
+
// Tabs are a horizontal row of pressable triggers above panel content, with the
|
|
12
|
+
// active trigger emphasized so the current view is unmistakable.
|
|
13
|
+
//
|
|
14
|
+
// Three looks, picked by boolean prop (first match wins):
|
|
15
|
+
// - underline (default): each trigger is muted text; the active one gets the
|
|
16
|
+
// foreground/brand color and an indicator beneath it. The native shape
|
|
17
|
+
// differs per OS: web/Android draw an underline rule (Android = a 3px brand
|
|
18
|
+
// `primary` bar with muted inactive labels); iOS draws a gray segmented
|
|
19
|
+
// track with a raised white pill on the selected tab (no underline).
|
|
20
|
+
// - `pills`: the row is a muted track; the active trigger is an elevated/tonal
|
|
21
|
+
// background pill while the rest sit flat and muted.
|
|
22
|
+
// - `vertical`: the triggers stack into a left-aligned column rail; the active
|
|
23
|
+
// one is filled with an accent/tonal background while the rest sit flat and
|
|
24
|
+
// muted. Use it as a settings-style side rail.
|
|
25
|
+
//
|
|
26
|
+
// Orthogonal layout modifier:
|
|
27
|
+
// - `block`: triggers share the row equally (each flex-1) and the labels
|
|
28
|
+
// center, so the group spans the full available width. Omit for triggers
|
|
29
|
+
// that hug their labels at the leading edge.
|
|
30
|
+
//
|
|
31
|
+
// Each tab may carry an optional count badge (the `{ label, badge }` item
|
|
32
|
+
// shape), rendered as a small secondary pill after the label.
|
|
33
|
+
//
|
|
34
|
+
// The active underline is drawn as an explicit indicator View under the trigger
|
|
35
|
+
// rather than as a bottom border in markup (mirroring how ButtonGroup hand-rolls
|
|
36
|
+
// its hairline divider).
|
|
37
|
+
|
|
38
|
+
// The platform-varying surface. Everything color/shape-bearing the three looks
|
|
39
|
+
// need lives here, built from the active tokens (so each follows light/dark/glass).
|
|
40
|
+
export interface TabsSkin {
|
|
41
|
+
/** iOS/web dim the trigger on press; Android uses a ripple instead (null). */
|
|
42
|
+
pressedOpacity: number | null;
|
|
43
|
+
/** Android ripple over a pressed trigger; null on iOS/web. */
|
|
44
|
+
ripple: ((t: ColorTokens) => { color: string; borderless: boolean }) | null;
|
|
45
|
+
|
|
46
|
+
// --- underline ---
|
|
47
|
+
underlineRow: (t: ColorTokens) => ViewStyle;
|
|
48
|
+
underlineTrigger: (t: ColorTokens, selected: boolean) => ViewStyle;
|
|
49
|
+
underlineIndicator: (t: ColorTokens, selected: boolean) => ViewStyle;
|
|
50
|
+
underlineLabel: (t: ColorTokens, selected: boolean) => TextStyle;
|
|
51
|
+
|
|
52
|
+
// --- pills ---
|
|
53
|
+
pillsRow: (t: ColorTokens) => ViewStyle;
|
|
54
|
+
pillsTrigger: (t: ColorTokens, selected: boolean) => ViewStyle;
|
|
55
|
+
pillsFill: (t: ColorTokens, selected: boolean) => ViewStyle;
|
|
56
|
+
pillsLabel: (t: ColorTokens, selected: boolean) => TextStyle;
|
|
57
|
+
|
|
58
|
+
// --- vertical ---
|
|
59
|
+
verticalTrigger: (t: ColorTokens, selected: boolean) => ViewStyle;
|
|
60
|
+
verticalFill: (t: ColorTokens, selected: boolean) => ViewStyle;
|
|
61
|
+
verticalLabel: (t: ColorTokens, selected: boolean) => TextStyle;
|
|
62
|
+
|
|
63
|
+
// --- count badge ---
|
|
64
|
+
countBadgeBox: (t: ColorTokens) => ViewStyle;
|
|
65
|
+
countBadgeLabel: (t: ColorTokens, muted: boolean) => TextStyle;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A tab is either a bare label or a label paired with a count badge. */
|
|
69
|
+
export type TabItem = string | { label: string; badge?: string };
|
|
70
|
+
|
|
71
|
+
export interface TabsProps {
|
|
72
|
+
/** Triggers, left to right. Strings, or `{ label, badge }` for a count. */
|
|
73
|
+
tabs?: TabItem[];
|
|
74
|
+
/** Index of the active trigger. */
|
|
75
|
+
active?: number;
|
|
76
|
+
/** Called with the pressed trigger's index. */
|
|
77
|
+
onChange?: (index: number) => void;
|
|
78
|
+
|
|
79
|
+
// Look (pick one; default is the underline look). Precedence when more than
|
|
80
|
+
// one is passed: pills, then vertical, then underline.
|
|
81
|
+
pills?: boolean;
|
|
82
|
+
vertical?: boolean;
|
|
83
|
+
underline?: boolean;
|
|
84
|
+
|
|
85
|
+
// Layout: equal full-width triggers vs. leading-aligned hugging triggers.
|
|
86
|
+
block?: boolean;
|
|
87
|
+
|
|
88
|
+
disabled?: boolean;
|
|
89
|
+
/** Escape hatch for layout/positioning composition (mainly width). */
|
|
90
|
+
style?: StyleProp<ViewStyle>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Variant precedence when more than one is passed: first match wins.
|
|
94
|
+
function variantOf(p: TabsProps): Variant {
|
|
95
|
+
if (p.pills) return "pills";
|
|
96
|
+
if (p.vertical) return "vertical";
|
|
97
|
+
if (p.underline) return "underline";
|
|
98
|
+
return "underline";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const DEFAULT_TABS: TabItem[] = ["General", "Security", "Notifications", "Billing"];
|
|
102
|
+
|
|
103
|
+
function labelOf(item: TabItem): string {
|
|
104
|
+
return typeof item === "string" ? item : item.label;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function badgeOf(item: TabItem): string | undefined {
|
|
108
|
+
return typeof item === "string" ? undefined : item.badge;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// vertical: flex-col items-stretch gap-1; width w-full (block) or w-[180px].
|
|
112
|
+
function verticalRail(block: boolean): ViewStyle {
|
|
113
|
+
return {
|
|
114
|
+
flexDirection: "column",
|
|
115
|
+
alignItems: "stretch",
|
|
116
|
+
gap: 4,
|
|
117
|
+
width: block ? "100%" : 180,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Build a Tabs component from a platform skin. */
|
|
122
|
+
export function createTabs(skin: TabsSkin) {
|
|
123
|
+
const ripple = skin.ripple;
|
|
124
|
+
|
|
125
|
+
// A small secondary count pill shown after a trigger label.
|
|
126
|
+
function CountBadge({ children, muted }: { children: string; muted: boolean }) {
|
|
127
|
+
const { tokens } = useTheme();
|
|
128
|
+
return (
|
|
129
|
+
<View style={skin.countBadgeBox(tokens)}>
|
|
130
|
+
<Text style={skin.countBadgeLabel(tokens, muted)}>{children}</Text>
|
|
131
|
+
</View>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
interface TriggerProps {
|
|
136
|
+
label: string;
|
|
137
|
+
badge?: string;
|
|
138
|
+
selected: boolean;
|
|
139
|
+
variant: Variant;
|
|
140
|
+
block?: boolean;
|
|
141
|
+
disabled?: boolean;
|
|
142
|
+
onPress?: () => void;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function Trigger({ label, badge, selected, variant, block, disabled, onPress }: TriggerProps) {
|
|
146
|
+
const { tokens } = useTheme();
|
|
147
|
+
|
|
148
|
+
if (variant === "vertical") {
|
|
149
|
+
// Vertical rail: a full-width, left-aligned row; the active item is filled
|
|
150
|
+
// with an accent/tonal background.
|
|
151
|
+
const container: StyleProp<ViewStyle> = [
|
|
152
|
+
skin.verticalTrigger(tokens, selected),
|
|
153
|
+
skin.verticalFill(tokens, selected),
|
|
154
|
+
disabled ? s.disabledDim : null,
|
|
155
|
+
];
|
|
156
|
+
return (
|
|
157
|
+
<Pressable
|
|
158
|
+
onPress={onPress}
|
|
159
|
+
disabled={disabled}
|
|
160
|
+
android_ripple={ripple ? ripple(tokens) : undefined}
|
|
161
|
+
accessibilityRole="tab"
|
|
162
|
+
accessibilityState={{ selected, disabled: !!disabled }}
|
|
163
|
+
style={({ pressed }) => [container, skin.pressedOpacity != null && pressed ? { opacity: skin.pressedOpacity } : null]}
|
|
164
|
+
>
|
|
165
|
+
<Text style={skin.verticalLabel(tokens, selected)}>{label}</Text>
|
|
166
|
+
{badge != null ? <CountBadge muted={!selected}>{badge}</CountBadge> : null}
|
|
167
|
+
</Pressable>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (variant === "pills") {
|
|
172
|
+
const container: StyleProp<ViewStyle> = [
|
|
173
|
+
skin.pillsTrigger(tokens, selected),
|
|
174
|
+
block ? s.flex1 : null,
|
|
175
|
+
skin.pillsFill(tokens, selected),
|
|
176
|
+
disabled ? s.disabledDim : null,
|
|
177
|
+
];
|
|
178
|
+
return (
|
|
179
|
+
<Pressable
|
|
180
|
+
onPress={onPress}
|
|
181
|
+
disabled={disabled}
|
|
182
|
+
android_ripple={ripple ? ripple(tokens) : undefined}
|
|
183
|
+
accessibilityRole="tab"
|
|
184
|
+
accessibilityState={{ selected, disabled: !!disabled }}
|
|
185
|
+
style={({ pressed }) => [container, skin.pressedOpacity != null && pressed ? { opacity: skin.pressedOpacity } : null]}
|
|
186
|
+
>
|
|
187
|
+
<Text style={skin.pillsLabel(tokens, selected)}>{label}</Text>
|
|
188
|
+
{badge != null ? <CountBadge muted={!selected}>{badge}</CountBadge> : null}
|
|
189
|
+
</Pressable>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Underline: the active trigger gets the emphasized label and an indicator
|
|
194
|
+
// drawn as an explicit sliver pinned to the trigger's bottom edge (iOS draws
|
|
195
|
+
// a raised pill instead, supplied through underlineTrigger).
|
|
196
|
+
const container: StyleProp<ViewStyle> = [
|
|
197
|
+
skin.underlineTrigger(tokens, selected),
|
|
198
|
+
block ? s.flex1 : null,
|
|
199
|
+
disabled ? s.disabledDim : null,
|
|
200
|
+
];
|
|
201
|
+
return (
|
|
202
|
+
<Pressable
|
|
203
|
+
onPress={onPress}
|
|
204
|
+
disabled={disabled}
|
|
205
|
+
android_ripple={ripple ? ripple(tokens) : undefined}
|
|
206
|
+
accessibilityRole="tab"
|
|
207
|
+
accessibilityState={{ selected, disabled: !!disabled }}
|
|
208
|
+
style={({ pressed }) => [container, skin.pressedOpacity != null && pressed ? { opacity: skin.pressedOpacity } : null]}
|
|
209
|
+
>
|
|
210
|
+
<Text style={skin.underlineLabel(tokens, selected)}>{label}</Text>
|
|
211
|
+
{badge != null ? <CountBadge muted={!selected}>{badge}</CountBadge> : null}
|
|
212
|
+
<View style={skin.underlineIndicator(tokens, selected)} />
|
|
213
|
+
</Pressable>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return function Tabs(props: TabsProps) {
|
|
218
|
+
const { tabs = DEFAULT_TABS, active = 0, onChange, disabled, style } = props;
|
|
219
|
+
const variant = variantOf(props);
|
|
220
|
+
const { tokens } = useTheme();
|
|
221
|
+
|
|
222
|
+
if (variant === "vertical") {
|
|
223
|
+
// A left-aligned column rail of stacked triggers; width hugs its content
|
|
224
|
+
// unless `block` stretches it to fill the available column.
|
|
225
|
+
return (
|
|
226
|
+
<View style={[verticalRail(!!props.block), style]}>
|
|
227
|
+
{tabs.map((item, i) => (
|
|
228
|
+
<Trigger
|
|
229
|
+
key={`${labelOf(item)}-${i}`}
|
|
230
|
+
label={labelOf(item)}
|
|
231
|
+
badge={badgeOf(item)}
|
|
232
|
+
selected={i === active}
|
|
233
|
+
variant="vertical"
|
|
234
|
+
block={props.block}
|
|
235
|
+
disabled={disabled}
|
|
236
|
+
onPress={() => onChange?.(i)}
|
|
237
|
+
/>
|
|
238
|
+
))}
|
|
239
|
+
</View>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (variant === "pills") {
|
|
244
|
+
return (
|
|
245
|
+
<View style={[skin.pillsRow(tokens), s.blockWidth(!!props.block), style]}>
|
|
246
|
+
{tabs.map((item, i) => (
|
|
247
|
+
<Trigger
|
|
248
|
+
key={`${labelOf(item)}-${i}`}
|
|
249
|
+
label={labelOf(item)}
|
|
250
|
+
badge={badgeOf(item)}
|
|
251
|
+
selected={i === active}
|
|
252
|
+
variant="pills"
|
|
253
|
+
block={props.block}
|
|
254
|
+
disabled={disabled}
|
|
255
|
+
onPress={() => onChange?.(i)}
|
|
256
|
+
/>
|
|
257
|
+
))}
|
|
258
|
+
</View>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Underline: the row sits on a hairline bottom border (web/Android) or a gray
|
|
263
|
+
// segmented track (iOS).
|
|
264
|
+
return (
|
|
265
|
+
<View style={[skin.underlineRow(tokens), s.blockWidth(!!props.block), style]}>
|
|
266
|
+
{tabs.map((item, i) => (
|
|
267
|
+
<Trigger
|
|
268
|
+
key={`${labelOf(item)}-${i}`}
|
|
269
|
+
label={labelOf(item)}
|
|
270
|
+
badge={badgeOf(item)}
|
|
271
|
+
selected={i === active}
|
|
272
|
+
variant="underline"
|
|
273
|
+
block={props.block}
|
|
274
|
+
disabled={disabled}
|
|
275
|
+
onPress={() => onChange?.(i)}
|
|
276
|
+
/>
|
|
277
|
+
))}
|
|
278
|
+
</View>
|
|
279
|
+
);
|
|
280
|
+
};
|
|
281
|
+
}
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import { type ViewStyle, type TextStyle } from "react-native";
|
|
2
|
+
import { type ColorTokens, shadow, alpha } from "../../style/index.js";
|
|
3
|
+
import { type TabsSkin } from "./tabs.shared.js";
|
|
4
|
+
|
|
5
|
+
// Co-located Tabs skins, one per platform. The shell resolves the look axis
|
|
6
|
+
// (underline / pills / vertical), the selection/block/disabled state, and the
|
|
7
|
+
// badges; the skin supplies only the native SHAPE, sizing, label weight, fill,
|
|
8
|
+
// indicator, and press feedback. The BRAND survives on every platform (the
|
|
9
|
+
// indigo `primary` token and the semantic tokens, never a platform default), so
|
|
10
|
+
// each follows light/dark and the glass surface.
|
|
11
|
+
//
|
|
12
|
+
// iOS (iOS 27 / Liquid Glass segmented control): the default underline look
|
|
13
|
+
// becomes a CAPSULE gray track (radius 9999, muted fill, 3px inset); the
|
|
14
|
+
// SELECTED tab is a raised white/elevated CAPSULE pill (radius 9999, small
|
|
15
|
+
// shadow); labels ~13pt; no underline rule. Both labels stay on-foreground
|
|
16
|
+
// (the white pill is the selected affordance, not a brand fill), mirroring
|
|
17
|
+
// the button-group iOS 27 segmented treatment. Press = dim.
|
|
18
|
+
// Android (M3 underline tabs): no container; each tab is text with a 3px brand
|
|
19
|
+
// `primary` indicator bar under the active tab; inactive labels read in
|
|
20
|
+
// `muted-foreground`; title-case ~14sp; press = android_ripple.
|
|
21
|
+
// Web: the established Canvas look (underline rule / muted pill track / accent
|
|
22
|
+
// rail), lifted verbatim from the original file.
|
|
23
|
+
|
|
24
|
+
export type Variant = "underline" | "pills" | "vertical";
|
|
25
|
+
|
|
26
|
+
// --- shared layout fragments (color-free; identical across platforms) --------
|
|
27
|
+
|
|
28
|
+
// flex-1: an equal-flex trigger in block mode (shares the row width).
|
|
29
|
+
export const flex1: ViewStyle = { flexGrow: 1, flexShrink: 1, flexBasis: "0%" };
|
|
30
|
+
|
|
31
|
+
// opacity-50: the dimmed disabled look the component applies per trigger.
|
|
32
|
+
export const disabledDim: ViewStyle = { opacity: 0.5 };
|
|
33
|
+
|
|
34
|
+
// w-full vs self-start: in block mode the row fills the available width so
|
|
35
|
+
// equal-flex triggers stretch it; otherwise the row hugs its triggers.
|
|
36
|
+
export function blockWidth(block: boolean): ViewStyle {
|
|
37
|
+
return block ? { width: "100%" } : { alignSelf: "flex-start" };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// =============================================================================
|
|
41
|
+
// Web: the established Canvas look (lifted verbatim from the original file).
|
|
42
|
+
// =============================================================================
|
|
43
|
+
|
|
44
|
+
export const webSkin: TabsSkin = {
|
|
45
|
+
// The selected underline tab carries no track ripple; press dims the trigger.
|
|
46
|
+
pressedOpacity: 0.9,
|
|
47
|
+
ripple: null,
|
|
48
|
+
|
|
49
|
+
// --- underline ---
|
|
50
|
+
underlineRow(tokens) {
|
|
51
|
+
return {
|
|
52
|
+
flexDirection: "row",
|
|
53
|
+
alignItems: "center",
|
|
54
|
+
borderBottomWidth: 1,
|
|
55
|
+
borderColor: tokens.border,
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
underlineTrigger() {
|
|
59
|
+
return {
|
|
60
|
+
flexDirection: "row",
|
|
61
|
+
alignItems: "center",
|
|
62
|
+
justifyContent: "center",
|
|
63
|
+
gap: 6,
|
|
64
|
+
paddingHorizontal: 16,
|
|
65
|
+
paddingVertical: 10,
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
// A 2px primary rule drawn as an explicit sliver pinned to the trigger's
|
|
69
|
+
// bottom edge (absolute bottom-0 left-0 right-0 h-0.5 rounded-full).
|
|
70
|
+
underlineIndicator(tokens, selected) {
|
|
71
|
+
return {
|
|
72
|
+
position: "absolute",
|
|
73
|
+
bottom: 0,
|
|
74
|
+
left: 0,
|
|
75
|
+
right: 0,
|
|
76
|
+
height: 2,
|
|
77
|
+
borderRadius: 9999,
|
|
78
|
+
backgroundColor: selected ? tokens.primary : "transparent",
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
underlineLabel(tokens, selected) {
|
|
82
|
+
return { fontSize: 14, lineHeight: 20, fontWeight: "500", color: selected ? tokens.foreground : tokens["muted-foreground"] };
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// --- pills ---
|
|
86
|
+
pillsRow(tokens) {
|
|
87
|
+
return {
|
|
88
|
+
flexDirection: "row",
|
|
89
|
+
alignItems: "center",
|
|
90
|
+
gap: 4,
|
|
91
|
+
alignSelf: "flex-start",
|
|
92
|
+
borderRadius: 8,
|
|
93
|
+
backgroundColor: tokens.muted,
|
|
94
|
+
padding: 4,
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
pillsTrigger() {
|
|
98
|
+
return {
|
|
99
|
+
flexDirection: "row",
|
|
100
|
+
alignItems: "center",
|
|
101
|
+
justifyContent: "center",
|
|
102
|
+
gap: 6,
|
|
103
|
+
borderRadius: 6,
|
|
104
|
+
paddingHorizontal: 12,
|
|
105
|
+
paddingVertical: 6,
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
pillsFill(tokens, selected) {
|
|
109
|
+
return selected
|
|
110
|
+
? { backgroundColor: tokens.background, ...shadow("sm") }
|
|
111
|
+
: { backgroundColor: "transparent" };
|
|
112
|
+
},
|
|
113
|
+
pillsLabel(tokens, selected) {
|
|
114
|
+
return { fontSize: 14, lineHeight: 20, fontWeight: "500", color: selected ? tokens.foreground : tokens["muted-foreground"] };
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
// --- vertical ---
|
|
118
|
+
verticalTrigger() {
|
|
119
|
+
return {
|
|
120
|
+
width: "100%",
|
|
121
|
+
flexDirection: "row",
|
|
122
|
+
alignItems: "center",
|
|
123
|
+
gap: 6,
|
|
124
|
+
borderRadius: 6,
|
|
125
|
+
paddingHorizontal: 12,
|
|
126
|
+
paddingVertical: 8,
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
verticalFill(tokens, selected) {
|
|
130
|
+
return { backgroundColor: selected ? tokens.accent : "transparent" };
|
|
131
|
+
},
|
|
132
|
+
verticalLabel(tokens, selected) {
|
|
133
|
+
return { fontSize: 14, lineHeight: 20, fontWeight: "500", color: selected ? tokens["accent-foreground"] : tokens["muted-foreground"] };
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
// --- count badge ---
|
|
137
|
+
countBadgeBox(tokens) {
|
|
138
|
+
return {
|
|
139
|
+
flexDirection: "row",
|
|
140
|
+
alignItems: "center",
|
|
141
|
+
alignSelf: "flex-start",
|
|
142
|
+
borderRadius: 6,
|
|
143
|
+
borderWidth: 1,
|
|
144
|
+
borderColor: "transparent",
|
|
145
|
+
backgroundColor: tokens.secondary,
|
|
146
|
+
paddingHorizontal: 6,
|
|
147
|
+
paddingVertical: 2,
|
|
148
|
+
};
|
|
149
|
+
},
|
|
150
|
+
countBadgeLabel(tokens, muted) {
|
|
151
|
+
return { fontSize: 12, lineHeight: 16, fontWeight: "500", color: muted ? tokens["muted-foreground"] : tokens["secondary-foreground"] };
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// =============================================================================
|
|
156
|
+
// iOS (iOS 27 / Liquid Glass segmented control): the in-page tab strip is a
|
|
157
|
+
// CAPSULE segmented control (mirroring button-group's iOS 27 treatment). A
|
|
158
|
+
// capsule gray track (radius 9999) holds raised white CAPSULE pills (radius
|
|
159
|
+
// 9999); the pills look stays a segmented track too; vertical stays a left
|
|
160
|
+
// rail. Press = opacity dim.
|
|
161
|
+
// =============================================================================
|
|
162
|
+
|
|
163
|
+
const IOS_PILL_SHADOW: ViewStyle = {
|
|
164
|
+
shadowColor: "#000000",
|
|
165
|
+
shadowOffset: { width: 0, height: 1 },
|
|
166
|
+
shadowOpacity: 0.18,
|
|
167
|
+
shadowRadius: 2,
|
|
168
|
+
elevation: 2,
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export const iosSkin: TabsSkin = {
|
|
172
|
+
pressedOpacity: 0.8, // HIG: dim on press
|
|
173
|
+
ripple: null,
|
|
174
|
+
|
|
175
|
+
// --- underline -> capsule segmented control (gray track + raised pill) ---
|
|
176
|
+
underlineRow(tokens) {
|
|
177
|
+
// The capsule gray track (radius 9999, muted fill, 3px inset).
|
|
178
|
+
return {
|
|
179
|
+
flexDirection: "row",
|
|
180
|
+
alignItems: "center",
|
|
181
|
+
gap: 0,
|
|
182
|
+
padding: 3,
|
|
183
|
+
borderRadius: 9999,
|
|
184
|
+
backgroundColor: tokens.muted,
|
|
185
|
+
};
|
|
186
|
+
},
|
|
187
|
+
underlineTrigger(tokens, selected) {
|
|
188
|
+
// Each tab is an independent capsule pill (radius 9999) inside the track;
|
|
189
|
+
// the selected one is raised white/elevated.
|
|
190
|
+
return {
|
|
191
|
+
flexDirection: "row",
|
|
192
|
+
alignItems: "center",
|
|
193
|
+
justifyContent: "center",
|
|
194
|
+
gap: 6,
|
|
195
|
+
borderRadius: 9999,
|
|
196
|
+
paddingHorizontal: 14,
|
|
197
|
+
paddingVertical: 7,
|
|
198
|
+
...(selected ? { ...IOS_PILL_SHADOW, backgroundColor: tokens.background } : { backgroundColor: "transparent" }),
|
|
199
|
+
};
|
|
200
|
+
},
|
|
201
|
+
// No underline rule on iOS: the raised capsule pill is the selected affordance.
|
|
202
|
+
underlineIndicator() {
|
|
203
|
+
return { display: "none" };
|
|
204
|
+
},
|
|
205
|
+
underlineLabel(tokens, selected) {
|
|
206
|
+
// ~13pt SF label; selected reads slightly heavier. Both stay on-foreground
|
|
207
|
+
// (the white pill is the selected affordance, not a brand fill).
|
|
208
|
+
return { fontSize: 13, lineHeight: 18, fontWeight: selected ? "600" : "500", color: tokens.foreground };
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
// --- pills (capsule segmented track, same gray-track + raised pill) ---
|
|
212
|
+
pillsRow(tokens) {
|
|
213
|
+
return {
|
|
214
|
+
flexDirection: "row",
|
|
215
|
+
alignItems: "center",
|
|
216
|
+
gap: 0,
|
|
217
|
+
alignSelf: "flex-start",
|
|
218
|
+
borderRadius: 9999,
|
|
219
|
+
backgroundColor: tokens.muted,
|
|
220
|
+
padding: 3,
|
|
221
|
+
};
|
|
222
|
+
},
|
|
223
|
+
pillsTrigger() {
|
|
224
|
+
return {
|
|
225
|
+
flexDirection: "row",
|
|
226
|
+
alignItems: "center",
|
|
227
|
+
justifyContent: "center",
|
|
228
|
+
gap: 6,
|
|
229
|
+
borderRadius: 9999,
|
|
230
|
+
paddingHorizontal: 14,
|
|
231
|
+
paddingVertical: 7,
|
|
232
|
+
};
|
|
233
|
+
},
|
|
234
|
+
pillsFill(tokens, selected) {
|
|
235
|
+
return selected
|
|
236
|
+
? { ...IOS_PILL_SHADOW, backgroundColor: tokens.background }
|
|
237
|
+
: { backgroundColor: "transparent" };
|
|
238
|
+
},
|
|
239
|
+
pillsLabel(tokens, selected) {
|
|
240
|
+
return { fontSize: 13, lineHeight: 18, fontWeight: selected ? "600" : "500", color: tokens.foreground };
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
// --- vertical (HIG grouped rail; active item is an accent-filled row) ---
|
|
244
|
+
verticalTrigger() {
|
|
245
|
+
return {
|
|
246
|
+
width: "100%",
|
|
247
|
+
flexDirection: "row",
|
|
248
|
+
alignItems: "center",
|
|
249
|
+
gap: 6,
|
|
250
|
+
borderRadius: 8,
|
|
251
|
+
paddingHorizontal: 12,
|
|
252
|
+
paddingVertical: 9,
|
|
253
|
+
};
|
|
254
|
+
},
|
|
255
|
+
verticalFill(tokens, selected) {
|
|
256
|
+
return { backgroundColor: selected ? tokens.accent : "transparent" };
|
|
257
|
+
},
|
|
258
|
+
verticalLabel(tokens, selected) {
|
|
259
|
+
return { fontSize: 15, lineHeight: 20, fontWeight: selected ? "600" : "400", color: selected ? tokens["accent-foreground"] : tokens.foreground };
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
// --- count badge ---
|
|
263
|
+
countBadgeBox(tokens) {
|
|
264
|
+
return {
|
|
265
|
+
flexDirection: "row",
|
|
266
|
+
alignItems: "center",
|
|
267
|
+
alignSelf: "flex-start",
|
|
268
|
+
borderRadius: 9999,
|
|
269
|
+
borderWidth: 0,
|
|
270
|
+
borderColor: "transparent",
|
|
271
|
+
backgroundColor: tokens.secondary,
|
|
272
|
+
paddingHorizontal: 7,
|
|
273
|
+
paddingVertical: 1,
|
|
274
|
+
};
|
|
275
|
+
},
|
|
276
|
+
countBadgeLabel(tokens, muted) {
|
|
277
|
+
return { fontSize: 12, lineHeight: 16, fontWeight: "600", color: muted ? tokens["muted-foreground"] : tokens["secondary-foreground"] };
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
// =============================================================================
|
|
282
|
+
// Android (Material 3): underline tabs. No container; each tab is text with a
|
|
283
|
+
// 3px brand `primary` indicator bar under the active tab; inactive labels read
|
|
284
|
+
// muted; press = android_ripple.
|
|
285
|
+
// =============================================================================
|
|
286
|
+
|
|
287
|
+
export const androidSkin: TabsSkin = {
|
|
288
|
+
pressedOpacity: null, // Android uses a ripple instead
|
|
289
|
+
ripple: (tokens) => ({ color: alpha(tokens.primary, 0.12), borderless: false }),
|
|
290
|
+
|
|
291
|
+
// --- underline (M3 primary tabs) ---
|
|
292
|
+
underlineRow(tokens) {
|
|
293
|
+
// M3 tabs sit on a hairline divider; no track fill.
|
|
294
|
+
return {
|
|
295
|
+
flexDirection: "row",
|
|
296
|
+
alignItems: "stretch",
|
|
297
|
+
borderBottomWidth: 1,
|
|
298
|
+
borderColor: tokens.border,
|
|
299
|
+
};
|
|
300
|
+
},
|
|
301
|
+
underlineTrigger() {
|
|
302
|
+
// Taller M3 tab target; the indicator hugs the bottom edge.
|
|
303
|
+
return {
|
|
304
|
+
flexDirection: "row",
|
|
305
|
+
alignItems: "center",
|
|
306
|
+
justifyContent: "center",
|
|
307
|
+
gap: 6,
|
|
308
|
+
paddingHorizontal: 16,
|
|
309
|
+
paddingVertical: 12,
|
|
310
|
+
};
|
|
311
|
+
},
|
|
312
|
+
// M3 indicator: a 3px brand `primary` bar with a slight top rounding under
|
|
313
|
+
// the active tab.
|
|
314
|
+
underlineIndicator(tokens, selected) {
|
|
315
|
+
return {
|
|
316
|
+
position: "absolute",
|
|
317
|
+
bottom: 0,
|
|
318
|
+
left: 0,
|
|
319
|
+
right: 0,
|
|
320
|
+
height: 3,
|
|
321
|
+
borderTopLeftRadius: 3,
|
|
322
|
+
borderTopRightRadius: 3,
|
|
323
|
+
backgroundColor: selected ? tokens.primary : "transparent",
|
|
324
|
+
};
|
|
325
|
+
},
|
|
326
|
+
underlineLabel(tokens, selected) {
|
|
327
|
+
// M3 titleSmall ~14sp; active label carries the brand indigo, inactive muted.
|
|
328
|
+
return { fontSize: 14, lineHeight: 20, fontWeight: "500", color: selected ? tokens.primary : tokens["muted-foreground"] };
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
// --- pills (M3 keeps the muted-track + tonal selected fill) ---
|
|
332
|
+
pillsRow(tokens) {
|
|
333
|
+
return {
|
|
334
|
+
flexDirection: "row",
|
|
335
|
+
alignItems: "center",
|
|
336
|
+
gap: 4,
|
|
337
|
+
alignSelf: "flex-start",
|
|
338
|
+
borderRadius: 9999,
|
|
339
|
+
backgroundColor: tokens.muted,
|
|
340
|
+
padding: 4,
|
|
341
|
+
};
|
|
342
|
+
},
|
|
343
|
+
pillsTrigger() {
|
|
344
|
+
return {
|
|
345
|
+
flexDirection: "row",
|
|
346
|
+
alignItems: "center",
|
|
347
|
+
justifyContent: "center",
|
|
348
|
+
gap: 6,
|
|
349
|
+
borderRadius: 9999,
|
|
350
|
+
paddingHorizontal: 14,
|
|
351
|
+
paddingVertical: 7,
|
|
352
|
+
};
|
|
353
|
+
},
|
|
354
|
+
pillsFill(tokens, selected) {
|
|
355
|
+
// Tonal selected fill (secondaryContainer ~ alpha(primary, .12)).
|
|
356
|
+
return selected ? { backgroundColor: alpha(tokens.primary, 0.12) } : { backgroundColor: "transparent" };
|
|
357
|
+
},
|
|
358
|
+
pillsLabel(tokens, selected) {
|
|
359
|
+
return { fontSize: 14, lineHeight: 20, fontWeight: "500", color: selected ? tokens.primary : tokens["muted-foreground"] };
|
|
360
|
+
},
|
|
361
|
+
|
|
362
|
+
// --- vertical (M3 navigation rail row; active item is a tonal pill) ---
|
|
363
|
+
verticalTrigger() {
|
|
364
|
+
return {
|
|
365
|
+
width: "100%",
|
|
366
|
+
flexDirection: "row",
|
|
367
|
+
alignItems: "center",
|
|
368
|
+
gap: 6,
|
|
369
|
+
borderRadius: 9999,
|
|
370
|
+
paddingHorizontal: 14,
|
|
371
|
+
paddingVertical: 9,
|
|
372
|
+
};
|
|
373
|
+
},
|
|
374
|
+
verticalFill(tokens, selected) {
|
|
375
|
+
return { backgroundColor: selected ? alpha(tokens.primary, 0.12) : "transparent" };
|
|
376
|
+
},
|
|
377
|
+
verticalLabel(tokens, selected) {
|
|
378
|
+
return { fontSize: 14, lineHeight: 20, fontWeight: "500", color: selected ? tokens.primary : tokens["muted-foreground"] };
|
|
379
|
+
},
|
|
380
|
+
|
|
381
|
+
// --- count badge ---
|
|
382
|
+
countBadgeBox(tokens) {
|
|
383
|
+
return {
|
|
384
|
+
flexDirection: "row",
|
|
385
|
+
alignItems: "center",
|
|
386
|
+
alignSelf: "flex-start",
|
|
387
|
+
borderRadius: 9999,
|
|
388
|
+
borderWidth: 0,
|
|
389
|
+
borderColor: "transparent",
|
|
390
|
+
backgroundColor: tokens.secondary,
|
|
391
|
+
paddingHorizontal: 7,
|
|
392
|
+
paddingVertical: 1,
|
|
393
|
+
};
|
|
394
|
+
},
|
|
395
|
+
countBadgeLabel(tokens, muted) {
|
|
396
|
+
return { fontSize: 12, lineHeight: 16, fontWeight: "500", color: muted ? tokens["muted-foreground"] : tokens["secondary-foreground"] };
|
|
397
|
+
},
|
|
398
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createTabs } from "./tabs.shared.js";
|
|
2
|
+
import { webSkin } from "./tabs.styles.js";
|
|
3
|
+
|
|
4
|
+
// Web Tabs (the base; Metro falls back to it on native, web bundlers resolve it).
|
|
5
|
+
export const Tabs = createTabs(webSkin);
|
|
6
|
+
export type { TabsProps, TabItem } from "./tabs.shared.js";
|