@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,115 @@
|
|
|
1
|
+
import { ActivityIndicator, Animated } from "react-native";
|
|
2
|
+
import Svg, { Circle, Line } from "react-native-svg";
|
|
3
|
+
import { type ColorTokens } from "../../style/index.js";
|
|
4
|
+
import { type SpinnerSkin } from "./spinner.shared.js";
|
|
5
|
+
|
|
6
|
+
// Co-located Spinner skins, one per platform. The BRAND survives on every
|
|
7
|
+
// platform (the arc/spoke color is a semantic token, never a platform default),
|
|
8
|
+
// and only the native SHAPE of the loader changes per OS:
|
|
9
|
+
// iOS (UIActivityIndicatorView): a ring of 12 short rounded SPOKES arranged
|
|
10
|
+
// radially around the center, each with a graduated opacity (a fading
|
|
11
|
+
// "clock" of lines), the whole ring rotating continuously. Color = the tone
|
|
12
|
+
// token (default the muted/foreground arc color).
|
|
13
|
+
// Android (M3 CircularProgressIndicator, indeterminate): a SINGLE ARC, a ~270deg
|
|
14
|
+
// stroke of a ring with a rounded cap, in the brand color, sweeping
|
|
15
|
+
// continuously around the circle.
|
|
16
|
+
// Web: the established Canvas look, a React Native ActivityIndicator driven by
|
|
17
|
+
// the same tone/size props, lifted verbatim from the original component.
|
|
18
|
+
|
|
19
|
+
export type Tone = "primary" | "muted" | "foreground";
|
|
20
|
+
|
|
21
|
+
// Arc/spoke color token per tone: reads from the active brand tokens (via
|
|
22
|
+
// useTheme) so it follows light/dark and the glass surface.
|
|
23
|
+
export const TONE_TOKEN: Record<Tone, keyof ColorTokens> = {
|
|
24
|
+
primary: "primary",
|
|
25
|
+
muted: "muted-foreground",
|
|
26
|
+
foreground: "foreground",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// react-native-svg's Circle/Line aren't Animated components; wrapping the SVG in
|
|
30
|
+
// the shell's Animated.View and spinning that is enough, but the rotation has to
|
|
31
|
+
// live on an Animated.View. The skins below return a static SVG; the shell's
|
|
32
|
+
// Animated.View carries the spin. To make the *shape itself* rotate (not just the
|
|
33
|
+
// bounding box), each spinning skin wraps its SVG in an Animated.View whose
|
|
34
|
+
// transform interpolates the shared 0..1 value to 0..360deg.
|
|
35
|
+
|
|
36
|
+
const AnimatedSvg = Animated.createAnimatedComponent(Svg);
|
|
37
|
+
|
|
38
|
+
// iOS: 12 spokes laid out every 30deg around the center, each a short rounded
|
|
39
|
+
// line from an inner radius to the rim. Opacity steps down around the ring so the
|
|
40
|
+
// leading spoke is solid and the trailing ones fade, the classic iOS look. The
|
|
41
|
+
// whole SVG rotates via the shared spin value.
|
|
42
|
+
const SPOKE_COUNT = 12;
|
|
43
|
+
|
|
44
|
+
export const iosSkin: SpinnerSkin = {
|
|
45
|
+
render: ({ size, color, rotate }) => {
|
|
46
|
+
const c = size / 2;
|
|
47
|
+
const outer = size / 2;
|
|
48
|
+
const inner = size * 0.28;
|
|
49
|
+
const strokeWidth = Math.max(1, size * 0.08);
|
|
50
|
+
const spin = rotate.interpolate({ inputRange: [0, 1], outputRange: ["0deg", "360deg"] });
|
|
51
|
+
const spokes = Array.from({ length: SPOKE_COUNT }, (_, i) => {
|
|
52
|
+
const angle = (i / SPOKE_COUNT) * 2 * Math.PI - Math.PI / 2;
|
|
53
|
+
const x1 = c + inner * Math.cos(angle);
|
|
54
|
+
const y1 = c + inner * Math.sin(angle);
|
|
55
|
+
const x2 = c + outer * Math.cos(angle);
|
|
56
|
+
const y2 = c + outer * Math.sin(angle);
|
|
57
|
+
// Leading spoke solid, fading around the ring (min ~0.2).
|
|
58
|
+
const opacity = 0.2 + (0.8 * i) / (SPOKE_COUNT - 1);
|
|
59
|
+
return (
|
|
60
|
+
<Line
|
|
61
|
+
key={i}
|
|
62
|
+
x1={x1}
|
|
63
|
+
y1={y1}
|
|
64
|
+
x2={x2}
|
|
65
|
+
y2={y2}
|
|
66
|
+
stroke={color}
|
|
67
|
+
strokeWidth={strokeWidth}
|
|
68
|
+
strokeLinecap="round"
|
|
69
|
+
opacity={opacity}
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
return (
|
|
74
|
+
<AnimatedSvg width={size} height={size} viewBox={`0 0 ${size} ${size}`} style={{ transform: [{ rotate: spin }] }}>
|
|
75
|
+
{spokes}
|
|
76
|
+
</AnimatedSvg>
|
|
77
|
+
);
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Android (M3): one ~270deg arc of a ring, rounded cap, brand color, sweeping
|
|
82
|
+
// continuously. Drawn as a stroked circle whose dash covers 3/4 of the
|
|
83
|
+
// circumference; the whole SVG rotates via the shared spin value.
|
|
84
|
+
export const androidSkin: SpinnerSkin = {
|
|
85
|
+
render: ({ size, color, rotate }) => {
|
|
86
|
+
const strokeWidth = Math.max(2, size * 0.1);
|
|
87
|
+
const r = (size - strokeWidth) / 2;
|
|
88
|
+
const c = size / 2;
|
|
89
|
+
const circumference = 2 * Math.PI * r;
|
|
90
|
+
const arc = circumference * 0.75; // 270deg sweep
|
|
91
|
+
const spin = rotate.interpolate({ inputRange: [0, 1], outputRange: ["0deg", "360deg"] });
|
|
92
|
+
return (
|
|
93
|
+
<AnimatedSvg width={size} height={size} viewBox={`0 0 ${size} ${size}`} style={{ transform: [{ rotate: spin }] }}>
|
|
94
|
+
<Circle
|
|
95
|
+
cx={c}
|
|
96
|
+
cy={c}
|
|
97
|
+
r={r}
|
|
98
|
+
stroke={color}
|
|
99
|
+
strokeWidth={strokeWidth}
|
|
100
|
+
strokeLinecap="round"
|
|
101
|
+
fill="none"
|
|
102
|
+
strokeDasharray={`${arc} ${circumference}`}
|
|
103
|
+
/>
|
|
104
|
+
</AnimatedSvg>
|
|
105
|
+
);
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Web: the current Canvas look, a React Native ActivityIndicator driven by the
|
|
110
|
+
// same tone/size props (numeric size + token color), lifted verbatim from the
|
|
111
|
+
// original single-file component. It animates itself, so it ignores the shared
|
|
112
|
+
// rotate value.
|
|
113
|
+
export const webSkin: SpinnerSkin = {
|
|
114
|
+
render: ({ size, color }) => <ActivityIndicator size={size} color={color} />,
|
|
115
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createSpinner } from "./spinner.shared.js";
|
|
2
|
+
import { webSkin } from "./spinner.styles.js";
|
|
3
|
+
|
|
4
|
+
// Web Spinner (the base; Metro falls back to it on native, web bundlers resolve
|
|
5
|
+
// it). Keeps the original ActivityIndicator look verbatim.
|
|
6
|
+
export const Spinner = createSpinner(webSkin);
|
|
7
|
+
export type { SpinnerProps } from "./spinner.shared.js";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createSwitch } from "./switch.shared.js";
|
|
2
|
+
import { androidSkin } from "./switch.styles.js";
|
|
3
|
+
|
|
4
|
+
// Material 3 Switch. Metro resolves this file on Android; the docs import it for preview.
|
|
5
|
+
export const Switch = createSwitch(androidSkin);
|
|
6
|
+
export type { SwitchProps } from "./switch.shared.js";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createSwitch } from "./switch.shared.js";
|
|
2
|
+
import { iosSkin } from "./switch.styles.js";
|
|
3
|
+
|
|
4
|
+
// iOS (HIG) Switch. Metro resolves this file on iOS; the docs import it for preview.
|
|
5
|
+
export const Switch = createSwitch(iosSkin);
|
|
6
|
+
export type { SwitchProps } from "./switch.shared.js";
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Toggles
|
|
2
|
+
|
|
3
|
+
On / off switch, isolated or grouped in a settings list.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
<Switch checked>Available to chat</Switch>
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Variants
|
|
12
|
+
|
|
13
|
+
### State - off
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
<Switch>Available to chat</Switch>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### With description
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
<Switch checked description="Show your availability to teammates.">Available to chat</Switch>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Disabled
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
<Switch checked disabled>Available to chat</Switch>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Do & Don't
|
|
32
|
+
|
|
33
|
+
### Off
|
|
34
|
+
|
|
35
|
+
**Do** — Keep the standard bg-input off track so off stays clearly interactive and distinct from a disabled control.
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
<Switch style={{ maxWidth: 280 }}>Two-factor auth</Switch>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Don't** — A washed-out off track reads as disabled, so users can't tell the switch is simply off versus locked.
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
<Pressable style={{ maxWidth: 280, flexDirection: "row", alignItems: "center", justifyContent: "space-between", gap: 16 }}>
|
|
45
|
+
<Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>Two-factor auth</Text>
|
|
46
|
+
<View style={{ position: "relative", flexShrink: 0, borderRadius: 9999, width: 36, height: 20, backgroundColor: alpha(tokens.muted, 0.3) }}>
|
|
47
|
+
<View style={{ position: "absolute", top: 2, left: 2, borderRadius: 9999, width: 16, height: 16, backgroundColor: tokens.muted }} />
|
|
48
|
+
</View>
|
|
49
|
+
</Pressable>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### On
|
|
53
|
+
|
|
54
|
+
**Do** — Reserve the on switch for instantly reversible settings; route irreversible actions through a button plus confirmation.
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
<View style={{ maxWidth: 280, flexDirection: "column", gap: 12 }}>
|
|
58
|
+
<Switch checked>Auto-save drafts</Switch>
|
|
59
|
+
<Button destructive small style={{ alignSelf: "flex-start" }}>Delete account…</Button>
|
|
60
|
+
</View>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Don't** — A switch applies instantly; wiring an on toggle to an irreversible action invites accidental, unconfirmed data loss.
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
<Pressable style={{ maxWidth: 280, flexDirection: "row", alignItems: "center", justifyContent: "space-between", gap: 16 }}>
|
|
67
|
+
<Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.destructive }}>Permanently delete account</Text>
|
|
68
|
+
<View style={{ position: "relative", flexShrink: 0, borderRadius: 9999, width: 36, height: 20, backgroundColor: tokens.primary }}>
|
|
69
|
+
<View style={{ position: "absolute", top: 2, right: 2, borderRadius: 9999, width: 16, height: 16, backgroundColor: tokens.background, ...shadow() }} />
|
|
70
|
+
</View>
|
|
71
|
+
</Pressable>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Do** — Label the setting, not the state; the switch communicates on or off.
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
<Switch checked style={{ maxWidth: 280 }}>Notifications</Switch>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Don't** — An On/Off label duplicates what the switch position already shows.
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
<View style={{ maxWidth: 280, flexDirection: "row", alignItems: "center", justifyContent: "space-between" }}>
|
|
84
|
+
<Text style={{ fontSize: 13 }}>Notifications</Text>
|
|
85
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
86
|
+
<Text style={{ fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] }}>Off</Text>
|
|
87
|
+
<Switch />
|
|
88
|
+
<Text style={{ fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] }}>On</Text>
|
|
89
|
+
</View>
|
|
90
|
+
</View>
|
|
91
|
+
```
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type GestureResponderEvent } from "react-native";
|
|
3
|
+
import { Pressable, View, Text, useTheme, type ColorTokens, type ViewStyle } from "../../style/index.js";
|
|
4
|
+
|
|
5
|
+
// Shared Switch shell. Uses React Native's primitives DIRECTLY (no engine className
|
|
6
|
+
// layer) and reads the active brand tokens via useTheme, so colors follow light/dark.
|
|
7
|
+
// The shared structure (label/description row, accessibility, toggle, sizing) lives
|
|
8
|
+
// here once; a platform file supplies only its track + thumb styles (a SwitchSkin)
|
|
9
|
+
// and calls createSwitch. Styles are plain RN objects, which work on iOS, Android,
|
|
10
|
+
// and web alike (a real .css file would only work on the web).
|
|
11
|
+
|
|
12
|
+
export interface SwitchProps {
|
|
13
|
+
/** Controlled on/off state. The component renders exactly this value. */
|
|
14
|
+
checked?: boolean;
|
|
15
|
+
/** Fired with the next checked value when the switch is toggled. */
|
|
16
|
+
onChange?: (next: boolean) => void;
|
|
17
|
+
/** Alias of onChange, for parity with RN's value-style callbacks. */
|
|
18
|
+
onValueChange?: (next: boolean) => void;
|
|
19
|
+
// Size (pick one; default is the standard track).
|
|
20
|
+
small?: boolean;
|
|
21
|
+
large?: boolean;
|
|
22
|
+
// State.
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
/** Optional label, rendered to the left of the track. */
|
|
25
|
+
children?: ReactNode;
|
|
26
|
+
/** Optional muted description line, rendered under the label. */
|
|
27
|
+
description?: ReactNode;
|
|
28
|
+
/** Extra style on the row, applied last. */
|
|
29
|
+
style?: ViewStyle;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type Size = "small" | "base" | "large";
|
|
33
|
+
|
|
34
|
+
function sizeOf(p: SwitchProps): Size {
|
|
35
|
+
if (p.large) return "large";
|
|
36
|
+
if (p.small) return "small";
|
|
37
|
+
return "base";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// The only thing a platform skin owns: the track and thumb styles for a given
|
|
41
|
+
// on/off state and size, built from the active tokens. Everything else is the shell.
|
|
42
|
+
export interface SwitchSkin {
|
|
43
|
+
track: (tokens: ColorTokens, checked: boolean, size: Size) => ViewStyle;
|
|
44
|
+
thumb: (tokens: ColorTokens, checked: boolean, size: Size) => ViewStyle;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const LABEL_FONT: Record<Size, number> = { small: 12, base: 14, large: 16 };
|
|
48
|
+
const DESC_FONT: Record<Size, number> = { small: 11, base: 12, large: 14 };
|
|
49
|
+
|
|
50
|
+
/** Build a Switch component from a platform skin. */
|
|
51
|
+
export function createSwitch(skin: SwitchSkin) {
|
|
52
|
+
return function Switch(props: SwitchProps) {
|
|
53
|
+
const { checked = false, onChange, onValueChange, disabled, children, description, style } = props;
|
|
54
|
+
const { tokens } = useTheme();
|
|
55
|
+
const size = sizeOf(props);
|
|
56
|
+
|
|
57
|
+
const handlePress = (_event: GestureResponderEvent) => {
|
|
58
|
+
const next = !checked;
|
|
59
|
+
onChange?.(next);
|
|
60
|
+
onValueChange?.(next);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<Pressable
|
|
65
|
+
onPress={handlePress}
|
|
66
|
+
disabled={disabled}
|
|
67
|
+
accessibilityRole="switch"
|
|
68
|
+
accessibilityState={{ checked, disabled: !!disabled }}
|
|
69
|
+
style={({ pressed }) => [
|
|
70
|
+
{
|
|
71
|
+
flexDirection: "row",
|
|
72
|
+
justifyContent: "space-between",
|
|
73
|
+
gap: 16,
|
|
74
|
+
alignItems: description != null ? "flex-start" : "center",
|
|
75
|
+
},
|
|
76
|
+
disabled ? { opacity: 0.5 } : null,
|
|
77
|
+
pressed ? { opacity: 0.9 } : null,
|
|
78
|
+
style,
|
|
79
|
+
]}
|
|
80
|
+
>
|
|
81
|
+
{children != null || description != null ? (
|
|
82
|
+
<View>
|
|
83
|
+
{children != null ? (
|
|
84
|
+
<Text style={{ fontWeight: "500", color: tokens.foreground, fontSize: LABEL_FONT[size] }}>{children}</Text>
|
|
85
|
+
) : null}
|
|
86
|
+
{description != null ? (
|
|
87
|
+
<Text style={{ color: tokens["muted-foreground"], fontSize: DESC_FONT[size] }}>{description}</Text>
|
|
88
|
+
) : null}
|
|
89
|
+
</View>
|
|
90
|
+
) : null}
|
|
91
|
+
<View style={skin.track(tokens, checked, size)}>
|
|
92
|
+
<View style={skin.thumb(tokens, checked, size)} />
|
|
93
|
+
</View>
|
|
94
|
+
</Pressable>
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { type ViewStyle } from "react-native";
|
|
2
|
+
import { type SwitchSkin, type Size } from "./switch.shared.js";
|
|
3
|
+
|
|
4
|
+
// Co-located Switch styles, one skin per platform, all driven by the brand tokens
|
|
5
|
+
// (passed in from useTheme so they follow light/dark). Plain RN style objects, so
|
|
6
|
+
// they apply on iOS, Android, and web alike. The on-track is always the brand
|
|
7
|
+
// `primary`, never the platform default, so the control reads native but stays yours.
|
|
8
|
+
|
|
9
|
+
const NATIVE_TRACK: Record<Size, { width: number; height: number }> = {
|
|
10
|
+
small: { width: 44, height: 24 },
|
|
11
|
+
base: { width: 48, height: 28 },
|
|
12
|
+
large: { width: 56, height: 32 },
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const WEB_TRACK: Record<Size, { width: number; height: number }> = {
|
|
16
|
+
small: { width: 32, height: 20 },
|
|
17
|
+
base: { width: 36, height: 20 },
|
|
18
|
+
large: { width: 44, height: 24 },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const WEB_THUMB: Record<Size, number> = { small: 14, base: 16, large: 20 };
|
|
22
|
+
|
|
23
|
+
const PILL: ViewStyle = { borderRadius: 999, position: "relative" };
|
|
24
|
+
const ABS: ViewStyle = { position: "absolute", borderRadius: 999 };
|
|
25
|
+
const IOS_SHADOW: ViewStyle = {
|
|
26
|
+
shadowColor: "#000000",
|
|
27
|
+
shadowOffset: { width: 0, height: 1 },
|
|
28
|
+
shadowOpacity: 0.2,
|
|
29
|
+
shadowRadius: 2,
|
|
30
|
+
elevation: 2,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// iOS (HIG): a solid pill with a near-full white thumb.
|
|
34
|
+
export const iosSkin: SwitchSkin = {
|
|
35
|
+
track: (t, checked, size) => ({
|
|
36
|
+
...PILL,
|
|
37
|
+
width: NATIVE_TRACK[size].width,
|
|
38
|
+
height: NATIVE_TRACK[size].height,
|
|
39
|
+
backgroundColor: checked ? t.primary : t.input,
|
|
40
|
+
}),
|
|
41
|
+
thumb: (_t, checked, size) => {
|
|
42
|
+
const d = NATIVE_TRACK[size].height - 4;
|
|
43
|
+
return { ...ABS, ...IOS_SHADOW, top: 2, width: d, height: d, backgroundColor: "#ffffff", ...(checked ? { right: 2 } : { left: 2 }) };
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Material 3: an outlined track with a small dot when off; a filled brand track with
|
|
48
|
+
// a larger white thumb when on.
|
|
49
|
+
export const androidSkin: SwitchSkin = {
|
|
50
|
+
track: (t, checked, size) => ({
|
|
51
|
+
...PILL,
|
|
52
|
+
width: NATIVE_TRACK[size].width,
|
|
53
|
+
height: NATIVE_TRACK[size].height,
|
|
54
|
+
...(checked ? { backgroundColor: t.primary } : { backgroundColor: t.muted, borderWidth: 2, borderColor: t.border }),
|
|
55
|
+
}),
|
|
56
|
+
thumb: (t, checked, size) => {
|
|
57
|
+
const h = NATIVE_TRACK[size].height;
|
|
58
|
+
if (checked) {
|
|
59
|
+
const d = h - 8;
|
|
60
|
+
return { ...ABS, top: (h - d) / 2, right: 4, width: d, height: d, backgroundColor: "#ffffff" };
|
|
61
|
+
}
|
|
62
|
+
const d = Math.round(h / 2.6);
|
|
63
|
+
return { ...ABS, top: (h - 4 - d) / 2, left: 4, width: d, height: d, backgroundColor: t["muted-foreground"] };
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Web: the current Canvas look, a compact pill with a surface-colored thumb.
|
|
68
|
+
export const webSkin: SwitchSkin = {
|
|
69
|
+
track: (t, checked, size) => ({
|
|
70
|
+
...PILL,
|
|
71
|
+
width: WEB_TRACK[size].width,
|
|
72
|
+
height: WEB_TRACK[size].height,
|
|
73
|
+
backgroundColor: checked ? t.primary : t.input,
|
|
74
|
+
}),
|
|
75
|
+
thumb: (t, checked, size) => {
|
|
76
|
+
const d = WEB_THUMB[size];
|
|
77
|
+
return { ...ABS, top: 2, width: d, height: d, backgroundColor: t.background, ...(checked ? { right: 2 } : { left: 2 }) };
|
|
78
|
+
},
|
|
79
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createSwitch } from "./switch.shared.js";
|
|
2
|
+
import { webSkin } from "./switch.styles.js";
|
|
3
|
+
|
|
4
|
+
// Web Switch (the base; Metro falls back to it on native, web bundlers resolve it).
|
|
5
|
+
export const Switch = createSwitch(webSkin);
|
|
6
|
+
export type { SwitchProps } from "./switch.shared.js";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createTextarea } from "./textarea.shared.js";
|
|
2
|
+
import { androidSkin } from "./textarea.styles.js";
|
|
3
|
+
|
|
4
|
+
// Material 3 filled Textarea. Metro resolves this file on Android; the docs import it for preview.
|
|
5
|
+
export const Textarea = createTextarea(androidSkin);
|
|
6
|
+
export type { TextareaProps } from "./textarea.shared.js";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createTextarea } from "./textarea.shared.js";
|
|
2
|
+
import { iosSkin } from "./textarea.styles.js";
|
|
3
|
+
|
|
4
|
+
// iOS (HIG text view) Textarea. Metro resolves this file on iOS; the docs import it for preview.
|
|
5
|
+
export const Textarea = createTextarea(iosSkin);
|
|
6
|
+
export type { TextareaProps } from "./textarea.shared.js";
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Textareas
|
|
2
|
+
|
|
3
|
+
Multi-line input, with character count, with toolbar.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
<Textarea rows={4} placeholder="A few words about this project" />
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Variants
|
|
12
|
+
|
|
13
|
+
### Character counter
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
<View style={{ maxWidth: 400 }}>
|
|
17
|
+
<Text style={{ marginBottom: 6, fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>Description</Text>
|
|
18
|
+
<Textarea rows={4} placeholder="A few words about this project…" />
|
|
19
|
+
<View style={{ marginTop: 4, flexDirection: "row", justifyContent: "flex-end" }}>
|
|
20
|
+
<Text style={{ fontSize: 11, color: tokens["muted-foreground"] }}>0 / 280</Text>
|
|
21
|
+
</View>
|
|
22
|
+
</View>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Formatting toolbar
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
<View style={{ maxWidth: 400, overflow: "hidden", borderRadius: 6, borderWidth: 1, borderColor: tokens.border }}>
|
|
29
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 4, borderBottomWidth: 1, borderColor: tokens.border, backgroundColor: alpha(tokens.muted, 0.3), paddingHorizontal: 12, paddingVertical: 8 }}>
|
|
30
|
+
<Button ghost small style={{ minWidth: 32, paddingHorizontal: 8 }}>B</Button>
|
|
31
|
+
<Button ghost small style={{ minWidth: 32, paddingHorizontal: 8 }}>I</Button>
|
|
32
|
+
<Button ghost small style={{ minWidth: 32, paddingHorizontal: 8 }}>{"</>"}</Button>
|
|
33
|
+
<View style={{ marginHorizontal: 4, height: 16, width: 1, backgroundColor: tokens.border }} />
|
|
34
|
+
<Button ghost small style={{ paddingHorizontal: 12 }}>Comment</Button>
|
|
35
|
+
</View>
|
|
36
|
+
<Textarea rows={4} placeholder="Leave a comment…" style={{ minHeight: 104, width: "100%", borderRadius: 0, borderWidth: 0 }} />
|
|
37
|
+
</View>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Disabled
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
<Textarea rows={4} disabled placeholder="A few words about this project" />
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Do & Don't
|
|
47
|
+
|
|
48
|
+
### With label
|
|
49
|
+
|
|
50
|
+
**Do** — Give a sensible min-height and allow vertical resize so users can see and grow their text.
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
<View style={{ maxWidth: 400, flexDirection: "column", gap: 6 }}>
|
|
54
|
+
<Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>Description</Text>
|
|
55
|
+
<Textarea rows={3} value="This is a longer description that runs past one line and stays readable." />
|
|
56
|
+
</View>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Don't** — A locked, single-line textarea hides long content with no way to expand.
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
<View style={{ maxWidth: 400, flexDirection: "column", gap: 6 }}>
|
|
63
|
+
<Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>Description</Text>
|
|
64
|
+
<TextInput numberOfLines={1} value="This is a longer description that runs past one line and gets clipped." style={{ height: 32, width: "100%", borderRadius: 6, borderWidth: 1, borderColor: tokens.input, backgroundColor: tokens.background, paddingHorizontal: 12, paddingVertical: 4, fontSize: 14, lineHeight: 20, color: tokens.foreground }} />
|
|
65
|
+
</View>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Character counter
|
|
69
|
+
|
|
70
|
+
**Do** — Show the live count against the cap and turn it destructive past the limit so the overage is precise.
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
<View style={{ maxWidth: 400, flexDirection: "column", gap: 6 }}>
|
|
74
|
+
<Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>Bio</Text>
|
|
75
|
+
<Textarea rows={3} invalid value="I have been building things on the web for fifteen years and counting, across teams large and small, shipping product end to end." />
|
|
76
|
+
<View style={{ marginTop: 4, flexDirection: "row", justifyContent: "flex-end" }}>
|
|
77
|
+
<Text style={{ fontSize: 11, color: tokens.destructive }}>123 / 120</Text>
|
|
78
|
+
</View>
|
|
79
|
+
</View>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Don't** — A vague "over limit" message gives no number, so users cannot tell how much to trim.
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
<View style={{ maxWidth: 400, flexDirection: "column", gap: 6 }}>
|
|
86
|
+
<Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>Bio</Text>
|
|
87
|
+
<Textarea rows={3} value="I have been building things on the web for fifteen years and counting, across teams large and small, shipping product end to end." />
|
|
88
|
+
<View style={{ marginTop: 4, flexDirection: "row", justifyContent: "flex-end" }}>
|
|
89
|
+
<Text style={{ fontSize: 11, color: tokens["muted-foreground"] }}>over limit</Text>
|
|
90
|
+
</View>
|
|
91
|
+
</View>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Formatting toolbar
|
|
95
|
+
|
|
96
|
+
**Do** — Make each control a real focusable button that toggles an active state when pressed.
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
<View style={{ maxWidth: 400, overflow: "hidden", borderRadius: 6, borderWidth: 1, borderColor: tokens.border }}>
|
|
100
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 4, borderBottomWidth: 1, borderColor: tokens.border, backgroundColor: alpha(tokens.muted, 0.3), paddingHorizontal: 12, paddingVertical: 8 }}>
|
|
101
|
+
<Button ghost small style={{ fontWeight: "700" }}>B</Button>
|
|
102
|
+
<Button ghost small style={{ fontStyle: "italic" }}>I</Button>
|
|
103
|
+
<Button ghost small style={{ fontFamily: "monospace", fontSize: 11 }}>{"</>"}</Button>
|
|
104
|
+
</View>
|
|
105
|
+
<Textarea rows={4} placeholder="Leave a comment" style={{ borderRadius: 0, borderWidth: 0, ...shadow("none") }} />
|
|
106
|
+
</View>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Don't** — Static, unclickable glyphs look like a toolbar but cannot be pressed or focused.
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
<View style={{ maxWidth: 400, overflow: "hidden", borderRadius: 6, borderWidth: 1, borderColor: tokens.border }}>
|
|
113
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 4, borderBottomWidth: 1, borderColor: tokens.border, backgroundColor: alpha(tokens.muted, 0.3), paddingHorizontal: 12, paddingVertical: 8 }}>
|
|
114
|
+
<Text style={{ paddingHorizontal: 8, fontSize: 14, lineHeight: 20, fontWeight: "700" }}>B</Text>
|
|
115
|
+
<Text style={{ paddingHorizontal: 8, fontSize: 14, lineHeight: 20, fontStyle: "italic" }}>I</Text>
|
|
116
|
+
<Text style={{ paddingHorizontal: 8, fontFamily: "monospace", fontSize: 11 }}>{"</>"}</Text>
|
|
117
|
+
</View>
|
|
118
|
+
<Textarea rows={4} placeholder="Leave a comment" style={{ borderRadius: 0, borderWidth: 0, ...shadow("none") }} />
|
|
119
|
+
</View>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Disabled
|
|
123
|
+
|
|
124
|
+
**Do** — Use the disabled attribute so the field blocks editing and focus, matching its dimmed look.
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
<View style={{ maxWidth: 400, flexDirection: "column", gap: 6 }}>
|
|
128
|
+
<Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>Description</Text>
|
|
129
|
+
<Textarea rows={3} disabled value="Read-only content the user must not change." />
|
|
130
|
+
</View>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Don't** — Dimming a textarea while leaving it editable looks disabled but still accepts input.
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
<View style={{ maxWidth: 400, flexDirection: "column", gap: 6 }}>
|
|
137
|
+
<Text style={{ fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground }}>Description</Text>
|
|
138
|
+
<TextInput multiline editable textAlignVertical="top" value="Read-only content the user must not change." style={{ minHeight: 80, width: "100%", borderRadius: 6, borderWidth: 1, borderColor: tokens.input, backgroundColor: tokens.background, paddingHorizontal: 12, paddingVertical: 8, fontSize: 14, lineHeight: 20, color: tokens.foreground, opacity: 0.5 }} />
|
|
139
|
+
</View>
|
|
140
|
+
```
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { TextInput, useTheme, type StyleProp, type TextStyle } from "../../style/index.js";
|
|
3
|
+
import { type TextareaSkin, type Size, sizeText, minHeight } from "./textarea.styles.js";
|
|
4
|
+
|
|
5
|
+
// Shared Textarea shell. The structure (a multiline TextInput), the public
|
|
6
|
+
// boolean-prop API, the size precedence, the error/focus state resolution, the
|
|
7
|
+
// accessibility, the handlers, and the disabled dim live here once; a platform
|
|
8
|
+
// file supplies only its skin (fill, shape, border/underline, focus feedback)
|
|
9
|
+
// and calls createTextarea.
|
|
10
|
+
|
|
11
|
+
export interface TextareaProps {
|
|
12
|
+
/** Controlled text value. */
|
|
13
|
+
value?: string;
|
|
14
|
+
/** Fired with the next text value on each edit. */
|
|
15
|
+
onChangeText?: (next: string) => void;
|
|
16
|
+
/** Placeholder shown while the field is empty. */
|
|
17
|
+
placeholder?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Visible rows the field sizes to. Sets the min height (rows * ~22px + 16px
|
|
20
|
+
* padding); the field still grows with content past this floor.
|
|
21
|
+
*/
|
|
22
|
+
rows?: number;
|
|
23
|
+
// State (pick one; orthogonal to size).
|
|
24
|
+
/** Error/validation state: a destructive cue. `invalid` is an alias. */
|
|
25
|
+
error?: boolean;
|
|
26
|
+
invalid?: boolean;
|
|
27
|
+
// Size (pick one; default is the base text-sm field).
|
|
28
|
+
small?: boolean;
|
|
29
|
+
large?: boolean;
|
|
30
|
+
/** Blocks editing and focus, and dims the field. */
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
/** Escape hatch for layout/positioning composition (mainly width). */
|
|
33
|
+
style?: StyleProp<TextStyle>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Size precedence when more than one is passed: first match wins.
|
|
37
|
+
function sizeOf(p: TextareaProps): Size {
|
|
38
|
+
if (p.large) return "large";
|
|
39
|
+
if (p.small) return "small";
|
|
40
|
+
return "base";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Build a Textarea component from a platform skin. */
|
|
44
|
+
export function createTextarea(skin: TextareaSkin) {
|
|
45
|
+
return function Textarea(props: TextareaProps) {
|
|
46
|
+
const { value, onChangeText, placeholder, rows, disabled, style } = props;
|
|
47
|
+
const isError = !!props.error || !!props.invalid;
|
|
48
|
+
const size = sizeOf(props);
|
|
49
|
+
const [focused, setFocused] = useState(false);
|
|
50
|
+
const { tokens } = useTheme();
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<TextInput
|
|
54
|
+
multiline
|
|
55
|
+
value={value}
|
|
56
|
+
onChangeText={onChangeText}
|
|
57
|
+
placeholder={placeholder}
|
|
58
|
+
placeholderTextColor={tokens["muted-foreground"]}
|
|
59
|
+
selectionColor={tokens.primary} // brand cursor / selection on every platform
|
|
60
|
+
editable={!disabled}
|
|
61
|
+
textAlignVertical="top"
|
|
62
|
+
onFocus={() => setFocused(true)}
|
|
63
|
+
onBlur={() => setFocused(false)}
|
|
64
|
+
style={[
|
|
65
|
+
skin.field(tokens, { error: isError, focused }),
|
|
66
|
+
sizeText(size),
|
|
67
|
+
minHeight(rows),
|
|
68
|
+
disabled ? { opacity: 0.5 } : null,
|
|
69
|
+
style,
|
|
70
|
+
]}
|
|
71
|
+
/>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
}
|