@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,203 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { type GestureResponderEvent, type TextStyle } from "react-native";
|
|
3
|
+
import { View, Pressable, Text, TextInput, useTheme, type ColorTokens, type StyleProp, type ViewStyle } from "../../style/index.js";
|
|
4
|
+
import { Icon } from "../icon/icon.js";
|
|
5
|
+
import { type InputSkin, type Size } from "./input.styles.js";
|
|
6
|
+
|
|
7
|
+
// Shared Input shell. The structure (bare field vs. grouped addon layout, the
|
|
8
|
+
// prefix/suffix boxes, overlaid icons, the optional action button), the
|
|
9
|
+
// public boolean-prop API, the size precedence, the border-color precedence
|
|
10
|
+
// (error > focus > default), accessibility, refs, and handlers all live here
|
|
11
|
+
// once. A platform file supplies only its skin (field shape, fill, border,
|
|
12
|
+
// height, the Android active-indicator underline, press feedback) and calls
|
|
13
|
+
// createInput.
|
|
14
|
+
|
|
15
|
+
// Glyphs an overlaid leading/trailing icon can name. Maps the scalar `icon`
|
|
16
|
+
// string to the Icon atom's flat boolean prop, so the playground stays
|
|
17
|
+
// serializable (a name string, not a React element).
|
|
18
|
+
const ICON_BOOL: Record<string, "search" | "mail" | "lock" | "user" | "key" | "globe"> = {
|
|
19
|
+
search: "search",
|
|
20
|
+
mail: "mail",
|
|
21
|
+
lock: "lock",
|
|
22
|
+
user: "user",
|
|
23
|
+
key: "key",
|
|
24
|
+
globe: "globe",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// react-native-web paints a default focus outline on the field; in the grouped
|
|
28
|
+
// (addon) layout that ring is clipped by the rounded, overflow-hidden container
|
|
29
|
+
// and reads as half-baked, so it is suppressed there and the group shows focus
|
|
30
|
+
// on its shared border instead. No-op on native, which has no CSS outline.
|
|
31
|
+
const FIELD_OUTLINE_RESET = { outlineStyle: "none", outlineWidth: 0 } as unknown as TextStyle;
|
|
32
|
+
|
|
33
|
+
export interface InputProps {
|
|
34
|
+
/** Current text value (controlled). */
|
|
35
|
+
value?: string;
|
|
36
|
+
/** Called with the new text on each keystroke. */
|
|
37
|
+
onChangeText?: (text: string) => void;
|
|
38
|
+
/** Placeholder shown while the field is empty. */
|
|
39
|
+
placeholder?: string;
|
|
40
|
+
// State (orthogonal). `error` (alias `invalid`) flags a validation problem.
|
|
41
|
+
error?: boolean;
|
|
42
|
+
invalid?: boolean;
|
|
43
|
+
disabled?: boolean;
|
|
44
|
+
/** Read-only: shows the value but blocks editing without the dimmed look. */
|
|
45
|
+
readOnly?: boolean;
|
|
46
|
+
// Size (pick one; default is the medium field).
|
|
47
|
+
small?: boolean;
|
|
48
|
+
large?: boolean;
|
|
49
|
+
/** Full-width field (the default); pass to be explicit. */
|
|
50
|
+
block?: boolean;
|
|
51
|
+
/** Multi-line text area instead of a single-line field. Ignored when addons
|
|
52
|
+
* (prefix/suffix/icons/action) are present, which are single-line only. */
|
|
53
|
+
multiline?: boolean;
|
|
54
|
+
|
|
55
|
+
// Addons. Passing any of these switches the field to the grouped layout: a
|
|
56
|
+
// single control where a leading prefix and/or trailing suffix share one outer
|
|
57
|
+
// border with the field (squared joined edges, 1px inner separators), so they
|
|
58
|
+
// read as one piece rather than detached. Addons are plain strings, so no icon
|
|
59
|
+
// library is required at this layer.
|
|
60
|
+
/** Leading addon content (e.g. "https://", "$", an icon glyph). */
|
|
61
|
+
prefix?: string;
|
|
62
|
+
/** Trailing addon content (e.g. "@canvas.dev", "USD", "Copy"). */
|
|
63
|
+
suffix?: string;
|
|
64
|
+
// Overlaid glyph mode. Unlike prefix/suffix (a bordered addon box), these
|
|
65
|
+
// float a real Lucide glyph INSIDE the field with no separator, and pad the
|
|
66
|
+
// text away from it (pl-9 / pr-9). `icon` names which glyph (see ICON_BOOL).
|
|
67
|
+
/** Render `icon` as a passive glyph inside the left of the field. */
|
|
68
|
+
leadingIcon?: boolean;
|
|
69
|
+
/** Render `icon` as a passive glyph inside the right of the field. */
|
|
70
|
+
trailingIcon?: boolean;
|
|
71
|
+
/** Glyph name for leadingIcon/trailingIcon (e.g. "search", "mail"). */
|
|
72
|
+
icon?: string;
|
|
73
|
+
/** Render the suffix as a pressable action button rather than a passive label. */
|
|
74
|
+
action?: boolean;
|
|
75
|
+
/** Called when the action suffix is pressed (action only). */
|
|
76
|
+
onActionPress?: (event: GestureResponderEvent) => void;
|
|
77
|
+
|
|
78
|
+
/** Escape hatch for layout/positioning composition (mainly width). */
|
|
79
|
+
style?: StyleProp<ViewStyle>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Size precedence when more than one is passed: first match wins.
|
|
83
|
+
function sizeOf(p: InputProps): Size {
|
|
84
|
+
if (p.large) return "large";
|
|
85
|
+
if (p.small) return "small";
|
|
86
|
+
return "base";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Build an Input component from a platform skin. */
|
|
90
|
+
export function createInput(skin: InputSkin) {
|
|
91
|
+
return function Input(props: InputProps) {
|
|
92
|
+
const {
|
|
93
|
+
value,
|
|
94
|
+
onChangeText,
|
|
95
|
+
placeholder,
|
|
96
|
+
disabled,
|
|
97
|
+
readOnly,
|
|
98
|
+
multiline,
|
|
99
|
+
prefix,
|
|
100
|
+
suffix,
|
|
101
|
+
leadingIcon,
|
|
102
|
+
trailingIcon,
|
|
103
|
+
icon,
|
|
104
|
+
action,
|
|
105
|
+
onActionPress,
|
|
106
|
+
style,
|
|
107
|
+
} = props;
|
|
108
|
+
const isError = !!(props.error || props.invalid);
|
|
109
|
+
const size = sizeOf(props);
|
|
110
|
+
const [focused, setFocused] = useState(false);
|
|
111
|
+
const { tokens } = useTheme();
|
|
112
|
+
|
|
113
|
+
// Border-color precedence: error > focus > default input border. Shared by
|
|
114
|
+
// both layouts; in the grouped layout it lives on the outer border so prefix
|
|
115
|
+
// + field + suffix light up together as one control. The skin decides how
|
|
116
|
+
// that color is used (a full border on iOS/web, the bottom active indicator
|
|
117
|
+
// on Android).
|
|
118
|
+
const borderColor: keyof ColorTokens = isError ? "destructive" : focused ? "ring" : "input";
|
|
119
|
+
const text = skin.text(tokens, size);
|
|
120
|
+
const iconName = icon != null ? ICON_BOOL[icon] : undefined;
|
|
121
|
+
const hasAddons = prefix != null || suffix != null || !!leadingIcon || !!trailingIcon || !!action;
|
|
122
|
+
|
|
123
|
+
const common = {
|
|
124
|
+
value,
|
|
125
|
+
onChangeText,
|
|
126
|
+
placeholder,
|
|
127
|
+
placeholderTextColor: tokens["muted-foreground"],
|
|
128
|
+
editable: !disabled && !readOnly,
|
|
129
|
+
selectionColor: tokens.primary, // brand cursor / selection on every platform
|
|
130
|
+
onFocus: () => setFocused(true),
|
|
131
|
+
onBlur: () => setFocused(false),
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Bare field (and the multiline text area): no addons.
|
|
135
|
+
if (!hasAddons) {
|
|
136
|
+
return (
|
|
137
|
+
<TextInput
|
|
138
|
+
style={[
|
|
139
|
+
skin.bareField(tokens, borderColor, focused, isError),
|
|
140
|
+
skin.bareBox(size, !!multiline),
|
|
141
|
+
text,
|
|
142
|
+
disabled ? { opacity: skin.disabledOpacity } : null,
|
|
143
|
+
style,
|
|
144
|
+
]}
|
|
145
|
+
multiline={multiline}
|
|
146
|
+
textAlignVertical={multiline ? "top" : "center"}
|
|
147
|
+
{...common}
|
|
148
|
+
/>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Grouped field: prefix/suffix addons, overlaid icons, optional action button.
|
|
153
|
+
// The whole group shares one border, so it owns the focus state and the inner
|
|
154
|
+
// field's default outline is suppressed (see FIELD_OUTLINE_RESET).
|
|
155
|
+
const height = skin.groupedHeight(size);
|
|
156
|
+
return (
|
|
157
|
+
<View
|
|
158
|
+
style={[skin.groupContainer(tokens, borderColor, focused, isError), disabled ? { opacity: skin.disabledOpacity } : null, style]}
|
|
159
|
+
>
|
|
160
|
+
{prefix != null ? (
|
|
161
|
+
<View style={skin.addonBox(tokens, "left", height)}>
|
|
162
|
+
<Text style={[skin.addonText(tokens), text]}>{prefix}</Text>
|
|
163
|
+
</View>
|
|
164
|
+
) : null}
|
|
165
|
+
|
|
166
|
+
{leadingIcon && iconName != null ? (
|
|
167
|
+
<View style={skin.iconOverlay("left")} pointerEvents="none">
|
|
168
|
+
<Icon {...{ [iconName]: true }} muted size={16} />
|
|
169
|
+
</View>
|
|
170
|
+
) : null}
|
|
171
|
+
|
|
172
|
+
<TextInput style={[skin.groupField(tokens, !!leadingIcon, !!trailingIcon), text, FIELD_OUTLINE_RESET]} {...common} />
|
|
173
|
+
|
|
174
|
+
{trailingIcon && iconName != null ? (
|
|
175
|
+
<View style={skin.iconOverlay("right")} pointerEvents="none">
|
|
176
|
+
<Icon {...{ [iconName]: true }} muted size={16} />
|
|
177
|
+
</View>
|
|
178
|
+
) : null}
|
|
179
|
+
|
|
180
|
+
{suffix != null ? (
|
|
181
|
+
action ? (
|
|
182
|
+
<Pressable
|
|
183
|
+
style={({ pressed }) => [
|
|
184
|
+
skin.addonBox(tokens, "right", height),
|
|
185
|
+
skin.pressedOpacity != null && pressed ? { opacity: skin.pressedOpacity } : null,
|
|
186
|
+
]}
|
|
187
|
+
onPress={onActionPress}
|
|
188
|
+
disabled={disabled}
|
|
189
|
+
android_ripple={skin.ripple ? skin.ripple(tokens) : undefined}
|
|
190
|
+
accessibilityRole="button"
|
|
191
|
+
>
|
|
192
|
+
<Text style={[skin.actionText(tokens), text]}>{suffix}</Text>
|
|
193
|
+
</Pressable>
|
|
194
|
+
) : (
|
|
195
|
+
<View style={skin.addonBox(tokens, "right", height)}>
|
|
196
|
+
<Text style={[skin.addonText(tokens), text]}>{suffix}</Text>
|
|
197
|
+
</View>
|
|
198
|
+
)
|
|
199
|
+
) : null}
|
|
200
|
+
</View>
|
|
201
|
+
);
|
|
202
|
+
};
|
|
203
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { type ViewStyle, type TextStyle } from "react-native";
|
|
2
|
+
import { type ColorTokens } from "../../style/index.js";
|
|
3
|
+
|
|
4
|
+
// Co-located Input skins, one per platform. The BRAND survives on every platform
|
|
5
|
+
// (the cursor/selection is always the indigo `primary`, the focus accent is the
|
|
6
|
+
// `ring`, never a platform default), and only the native SHAPE, sizing, fill,
|
|
7
|
+
// border treatment, and press feedback change per OS:
|
|
8
|
+
// iOS (HIG, iOS 26+/Liquid Glass): a PLAIN text field — the value text sits on
|
|
9
|
+
// a TRANSPARENT surface with a single bottom HAIRLINE rule (1pt `border`
|
|
10
|
+
// separator) and NO fill, NO surrounding box, NO rounded capsule. Focus
|
|
11
|
+
// thickens/tints the hairline to the brand (`ring`), error to `destructive`;
|
|
12
|
+
// press (action suffix) = opacity dim (~0.8). The cursor/selection stays the
|
|
13
|
+
// indigo `primary`.
|
|
14
|
+
// Android (Material 3 filled): a subtle fill (`muted`), TOP corners ~4 radius
|
|
15
|
+
// and a flat bottom, a bottom active-indicator underline (1dp `border` at
|
|
16
|
+
// rest -> 2dp `ring` on focus, `destructive` on error), ~56dp tall; the
|
|
17
|
+
// action suffix uses android_ripple; disabled opacity 0.38.
|
|
18
|
+
// Web: the established Canvas look (the current input, lifted verbatim) — full
|
|
19
|
+
// 1px border (error > focus > input), 6 radius, background fill, 36/32/40
|
|
20
|
+
// tall, opacity 0.5 disabled, action press opacity 0.9.
|
|
21
|
+
|
|
22
|
+
export type Size = "small" | "base" | "large";
|
|
23
|
+
|
|
24
|
+
// The contract a platform skin fulfills. Both layouts (bare field, grouped addon
|
|
25
|
+
// row) and the size/state inputs the shell resolves are passed in; the skin maps
|
|
26
|
+
// them to RN style objects. `borderColor` is a token key (error > focus > input)
|
|
27
|
+
// the shell already resolved; the skin reads tokens[borderColor].
|
|
28
|
+
export interface InputSkin {
|
|
29
|
+
/** Type scale per size; the field and its addons share it so they line up. */
|
|
30
|
+
text: (t: ColorTokens, size: Size) => TextStyle;
|
|
31
|
+
/** Height (single line) or min-height (multiline) of the bare field. */
|
|
32
|
+
bareBox: (size: Size, multiline: boolean) => TextStyle;
|
|
33
|
+
/** Fixed row height for the grouped layout (addon boxes set it). */
|
|
34
|
+
groupedHeight: (size: Size) => number;
|
|
35
|
+
/** The bare field surface: shape, fill, border/underline for the active state. */
|
|
36
|
+
bareField: (t: ColorTokens, borderColor: keyof ColorTokens, focused: boolean, error: boolean) => TextStyle;
|
|
37
|
+
/** The grouped (addon) outer: the row that shares one border/underline. */
|
|
38
|
+
groupContainer: (t: ColorTokens, borderColor: keyof ColorTokens, focused: boolean, error: boolean) => ViewStyle;
|
|
39
|
+
/** The inner field inside the group (grows to fill; pads away from icons). */
|
|
40
|
+
groupField: (t: ColorTokens, leadingIcon: boolean, trailingIcon: boolean) => TextStyle;
|
|
41
|
+
/** A prefix/suffix addon box. */
|
|
42
|
+
addonBox: (t: ColorTokens, side: "left" | "right", height: number) => ViewStyle;
|
|
43
|
+
addonText: (t: ColorTokens) => TextStyle;
|
|
44
|
+
actionText: (t: ColorTokens) => TextStyle;
|
|
45
|
+
/** Overlaid icon position inside the field (left or right gutter). */
|
|
46
|
+
iconOverlay: (side: "left" | "right") => ViewStyle;
|
|
47
|
+
/** Opacity applied to the field when disabled. */
|
|
48
|
+
disabledOpacity: number;
|
|
49
|
+
/** iOS/web dim the action suffix on press; Android uses a ripple instead (null). */
|
|
50
|
+
pressedOpacity: number | null;
|
|
51
|
+
/** Android ripple over the action suffix; null on iOS/web. */
|
|
52
|
+
ripple: ((t: ColorTokens) => { color: string; borderless: boolean }) | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// --- shared type scale (identical across platforms; brand type, not a face) --
|
|
56
|
+
function webText(_t: ColorTokens, size: Size): TextStyle {
|
|
57
|
+
if (size === "large") return { fontSize: 16, lineHeight: 24 };
|
|
58
|
+
if (size === "small") return { fontSize: 12, lineHeight: 16 };
|
|
59
|
+
return { fontSize: 14, lineHeight: 20 };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------- Web: the established Canvas look ----------
|
|
63
|
+
export const webSkin: InputSkin = {
|
|
64
|
+
text: webText,
|
|
65
|
+
bareBox: (size, multiline) => {
|
|
66
|
+
if (multiline) return { minHeight: size === "large" ? 96 : size === "small" ? 64 : 80 };
|
|
67
|
+
return { height: size === "large" ? 40 : size === "small" ? 32 : 36 };
|
|
68
|
+
},
|
|
69
|
+
groupedHeight: (size) => (size === "large" ? 40 : size === "small" ? 32 : 36),
|
|
70
|
+
bareField: (t, borderColor) => ({
|
|
71
|
+
width: "100%",
|
|
72
|
+
borderRadius: 6,
|
|
73
|
+
borderWidth: 1,
|
|
74
|
+
borderColor: t[borderColor],
|
|
75
|
+
backgroundColor: t.background,
|
|
76
|
+
paddingHorizontal: 12,
|
|
77
|
+
paddingVertical: 8,
|
|
78
|
+
color: t.foreground,
|
|
79
|
+
}),
|
|
80
|
+
groupContainer: (t, borderColor) => ({
|
|
81
|
+
flexDirection: "row",
|
|
82
|
+
alignItems: "stretch",
|
|
83
|
+
width: "100%",
|
|
84
|
+
borderWidth: 1,
|
|
85
|
+
borderColor: t[borderColor],
|
|
86
|
+
borderRadius: 6,
|
|
87
|
+
overflow: "hidden",
|
|
88
|
+
backgroundColor: t.background,
|
|
89
|
+
}),
|
|
90
|
+
groupField: (t, leadingIcon, trailingIcon) => ({
|
|
91
|
+
flexGrow: 1,
|
|
92
|
+
flexShrink: 1,
|
|
93
|
+
flexBasis: "0%",
|
|
94
|
+
height: "100%",
|
|
95
|
+
paddingHorizontal: 12,
|
|
96
|
+
paddingVertical: 8,
|
|
97
|
+
color: t.foreground,
|
|
98
|
+
...(leadingIcon ? { paddingLeft: 36 } : null),
|
|
99
|
+
...(trailingIcon ? { paddingRight: 36 } : null),
|
|
100
|
+
}),
|
|
101
|
+
addonBox: (t, side, height) => ({
|
|
102
|
+
justifyContent: "center",
|
|
103
|
+
backgroundColor: t.muted,
|
|
104
|
+
paddingHorizontal: 12,
|
|
105
|
+
borderColor: t.border,
|
|
106
|
+
...(side === "left" ? { borderRightWidth: 1 } : { borderLeftWidth: 1 }),
|
|
107
|
+
height,
|
|
108
|
+
}),
|
|
109
|
+
addonText: (t) => ({ color: t["muted-foreground"] }),
|
|
110
|
+
actionText: (t) => ({ fontWeight: "500", color: t.foreground }),
|
|
111
|
+
iconOverlay: (side) => ({
|
|
112
|
+
position: "absolute",
|
|
113
|
+
top: 0,
|
|
114
|
+
bottom: 0,
|
|
115
|
+
zIndex: 10,
|
|
116
|
+
justifyContent: "center",
|
|
117
|
+
...(side === "left" ? { left: 0, paddingLeft: 12 } : { right: 0, paddingRight: 12 }),
|
|
118
|
+
}),
|
|
119
|
+
disabledOpacity: 0.5,
|
|
120
|
+
pressedOpacity: 0.9,
|
|
121
|
+
ripple: null,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// ---------- iOS (HIG, iOS 26+/Liquid Glass): plain field, transparent, bottom hairline ----------
|
|
125
|
+
// Apple's plain text field on iOS 26+: the value text sits directly on a
|
|
126
|
+
// TRANSPARENT surface with a single bottom HAIRLINE rule (1pt separator) and no
|
|
127
|
+
// fill, no surrounding box, no rounded capsule. Canvas keeps the brand by
|
|
128
|
+
// thickening + tinting that hairline to the brand `ring` on focus (and
|
|
129
|
+
// `destructive` on error); at rest it is the faint `border` separator. The
|
|
130
|
+
// cursor/selection is always the indigo `primary` (set in the shell).
|
|
131
|
+
function iosHairline(t: ColorTokens, borderColor: keyof ColorTokens, focused: boolean, error: boolean): ViewStyle {
|
|
132
|
+
// Rest = the faint `border` separator; focus/error thicken to 2pt and tint to
|
|
133
|
+
// the brand color the shell resolved (ring on focus, destructive on error).
|
|
134
|
+
const active = focused || error;
|
|
135
|
+
return {
|
|
136
|
+
borderBottomWidth: active ? 2 : 1,
|
|
137
|
+
borderBottomColor: active ? t[borderColor] : t.border,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
export const iosSkin: InputSkin = {
|
|
141
|
+
text: webText,
|
|
142
|
+
bareBox: (size, multiline) => {
|
|
143
|
+
if (multiline) return { minHeight: size === "large" ? 110 : size === "small" ? 76 : 92 };
|
|
144
|
+
return { height: size === "large" ? 50 : size === "small" ? 36 : 44 };
|
|
145
|
+
},
|
|
146
|
+
groupedHeight: (size) => (size === "large" ? 50 : size === "small" ? 36 : 44),
|
|
147
|
+
bareField: (t, borderColor, focused, error) => ({
|
|
148
|
+
width: "100%",
|
|
149
|
+
// Plain field: transparent surface, no box/radius, only a bottom hairline.
|
|
150
|
+
backgroundColor: "transparent",
|
|
151
|
+
...iosHairline(t, borderColor, focused, error),
|
|
152
|
+
// No horizontal inset so the value text aligns flush with the hairline edge,
|
|
153
|
+
// as in the iOS 27 render.
|
|
154
|
+
paddingHorizontal: 0,
|
|
155
|
+
paddingVertical: 10,
|
|
156
|
+
color: t.foreground,
|
|
157
|
+
}),
|
|
158
|
+
groupContainer: (t, borderColor, focused, error) => ({
|
|
159
|
+
flexDirection: "row",
|
|
160
|
+
alignItems: "stretch",
|
|
161
|
+
width: "100%",
|
|
162
|
+
// The whole row shares the single bottom hairline; no fill, no box, no radius.
|
|
163
|
+
backgroundColor: "transparent",
|
|
164
|
+
...iosHairline(t, borderColor, focused, error),
|
|
165
|
+
}),
|
|
166
|
+
groupField: (t, leadingIcon, trailingIcon) => ({
|
|
167
|
+
flexGrow: 1,
|
|
168
|
+
flexShrink: 1,
|
|
169
|
+
flexBasis: "0%",
|
|
170
|
+
height: "100%",
|
|
171
|
+
paddingHorizontal: 0,
|
|
172
|
+
paddingVertical: 10,
|
|
173
|
+
color: t.foreground,
|
|
174
|
+
...(leadingIcon ? { paddingLeft: 28 } : null),
|
|
175
|
+
...(trailingIcon ? { paddingRight: 28 } : null),
|
|
176
|
+
}),
|
|
177
|
+
// Addons are inline on the transparent field (no filled box, no separator) so
|
|
178
|
+
// the row reads as one plain line over the shared hairline.
|
|
179
|
+
addonBox: (_t, side, height) => ({
|
|
180
|
+
justifyContent: "center",
|
|
181
|
+
backgroundColor: "transparent",
|
|
182
|
+
...(side === "left" ? { paddingRight: 8 } : { paddingLeft: 8 }),
|
|
183
|
+
height,
|
|
184
|
+
}),
|
|
185
|
+
addonText: (t) => ({ color: t["muted-foreground"] }),
|
|
186
|
+
actionText: (t) => ({ fontWeight: "600", color: t.primary }),
|
|
187
|
+
iconOverlay: (side) => ({
|
|
188
|
+
position: "absolute",
|
|
189
|
+
top: 0,
|
|
190
|
+
bottom: 0,
|
|
191
|
+
zIndex: 10,
|
|
192
|
+
justifyContent: "center",
|
|
193
|
+
...(side === "left" ? { left: 0 } : { right: 0 }),
|
|
194
|
+
}),
|
|
195
|
+
disabledOpacity: 0.5,
|
|
196
|
+
pressedOpacity: 0.8,
|
|
197
|
+
ripple: null,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// ---------- Android (Material 3 filled): subtle fill, top radius, bottom active indicator ----------
|
|
201
|
+
// M3 filled text field: a ~56dp container with a subtle fill (`muted` ~
|
|
202
|
+
// surface-container-highest), the TOP corners rounded ~4dp and a flat bottom,
|
|
203
|
+
// and a bottom active-indicator underline — 1dp `border` at rest, 2dp `ring`
|
|
204
|
+
// (brand) on focus, `destructive` on error. The brand survives via the focused
|
|
205
|
+
// indicator color and the action suffix's primary label + ripple.
|
|
206
|
+
const ANDROID_TOP_RADIUS = 4;
|
|
207
|
+
function androidUnderline(t: ColorTokens, borderColor: keyof ColorTokens, focused: boolean, error: boolean): ViewStyle {
|
|
208
|
+
// M3 active indicator: a VISIBLE baseline at rest (on-surface-variant ~ the
|
|
209
|
+
// `muted-foreground` token), thickening to 2dp in the brand `ring` on focus /
|
|
210
|
+
// `destructive` on error. The rest color must read clearly so the filled field is
|
|
211
|
+
// distinct from the iOS lineless capsule (the regression this fixes).
|
|
212
|
+
const active = focused || error;
|
|
213
|
+
return {
|
|
214
|
+
borderBottomWidth: active ? 2 : 1,
|
|
215
|
+
borderBottomColor: active ? t[borderColor] : t["muted-foreground"],
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
export const androidSkin: InputSkin = {
|
|
219
|
+
// M3 body input is 16sp; nudge the base/large up, keep small readable.
|
|
220
|
+
text: (_t, size) => {
|
|
221
|
+
if (size === "large") return { fontSize: 18, lineHeight: 26 };
|
|
222
|
+
if (size === "small") return { fontSize: 14, lineHeight: 20 };
|
|
223
|
+
return { fontSize: 16, lineHeight: 24 };
|
|
224
|
+
},
|
|
225
|
+
bareBox: (size, multiline) => {
|
|
226
|
+
if (multiline) return { minHeight: size === "large" ? 120 : size === "small" ? 88 : 104 };
|
|
227
|
+
return { height: size === "large" ? 60 : size === "small" ? 48 : 56 };
|
|
228
|
+
},
|
|
229
|
+
groupedHeight: (size) => (size === "large" ? 60 : size === "small" ? 48 : 56),
|
|
230
|
+
bareField: (t, borderColor, focused, error) => ({
|
|
231
|
+
width: "100%",
|
|
232
|
+
borderTopLeftRadius: ANDROID_TOP_RADIUS,
|
|
233
|
+
borderTopRightRadius: ANDROID_TOP_RADIUS,
|
|
234
|
+
borderBottomLeftRadius: 0,
|
|
235
|
+
borderBottomRightRadius: 0,
|
|
236
|
+
...androidUnderline(t, borderColor, focused, error),
|
|
237
|
+
backgroundColor: t.muted,
|
|
238
|
+
paddingHorizontal: 16,
|
|
239
|
+
paddingVertical: 8,
|
|
240
|
+
color: t.foreground,
|
|
241
|
+
}),
|
|
242
|
+
groupContainer: (t, borderColor, focused, error) => ({
|
|
243
|
+
flexDirection: "row",
|
|
244
|
+
alignItems: "stretch",
|
|
245
|
+
width: "100%",
|
|
246
|
+
borderTopLeftRadius: ANDROID_TOP_RADIUS,
|
|
247
|
+
borderTopRightRadius: ANDROID_TOP_RADIUS,
|
|
248
|
+
...androidUnderline(t, borderColor, focused, error),
|
|
249
|
+
overflow: "hidden",
|
|
250
|
+
backgroundColor: t.muted,
|
|
251
|
+
}),
|
|
252
|
+
groupField: (t, leadingIcon, trailingIcon) => ({
|
|
253
|
+
flexGrow: 1,
|
|
254
|
+
flexShrink: 1,
|
|
255
|
+
flexBasis: "0%",
|
|
256
|
+
height: "100%",
|
|
257
|
+
paddingHorizontal: 16,
|
|
258
|
+
paddingVertical: 8,
|
|
259
|
+
color: t.foreground,
|
|
260
|
+
...(leadingIcon ? { paddingLeft: 44 } : null),
|
|
261
|
+
...(trailingIcon ? { paddingRight: 44 } : null),
|
|
262
|
+
}),
|
|
263
|
+
// The addon shares the field fill (M3 leading/trailing content sits inside the
|
|
264
|
+
// filled container) with a hairline separator.
|
|
265
|
+
addonBox: (t, side, height) => ({
|
|
266
|
+
justifyContent: "center",
|
|
267
|
+
backgroundColor: t.muted,
|
|
268
|
+
paddingHorizontal: 16,
|
|
269
|
+
borderColor: t.border,
|
|
270
|
+
...(side === "left" ? { borderRightWidth: 1 } : { borderLeftWidth: 1 }),
|
|
271
|
+
height,
|
|
272
|
+
}),
|
|
273
|
+
addonText: (t) => ({ color: t["muted-foreground"] }),
|
|
274
|
+
actionText: (t) => ({ fontWeight: "500", color: t.primary, textTransform: "uppercase", letterSpacing: 0.5 }),
|
|
275
|
+
iconOverlay: (side) => ({
|
|
276
|
+
position: "absolute",
|
|
277
|
+
top: 0,
|
|
278
|
+
bottom: 0,
|
|
279
|
+
zIndex: 10,
|
|
280
|
+
justifyContent: "center",
|
|
281
|
+
...(side === "left" ? { left: 0, paddingLeft: 16 } : { right: 0, paddingRight: 16 }),
|
|
282
|
+
}),
|
|
283
|
+
disabledOpacity: 0.38, // M3 disabled opacity
|
|
284
|
+
pressedOpacity: null, // Android uses a ripple instead
|
|
285
|
+
ripple: (t) => ({ color: t.primary, borderless: false }),
|
|
286
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createInput } from "./input.shared.js";
|
|
2
|
+
import { webSkin } from "./input.styles.js";
|
|
3
|
+
|
|
4
|
+
// Web Input (the base; Metro falls back to it on native, web bundlers resolve it).
|
|
5
|
+
export const Input = createInput(webSkin);
|
|
6
|
+
export type { InputProps } from "./input.shared.js";
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Kbd
|
|
2
|
+
|
|
3
|
+
Keyboard shortcut indicator badge.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 4 }}>
|
|
9
|
+
<Kbd>⌘</Kbd>
|
|
10
|
+
<Text style={{ fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] }}>+</Text>
|
|
11
|
+
<Kbd>K</Kbd>
|
|
12
|
+
</View>
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Variants
|
|
16
|
+
|
|
17
|
+
### Mode - single
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
<Kbd>⌘</Kbd>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Mode - in a sentence
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
<View style={{ flexDirection: "row", flexWrap: "wrap", alignItems: "center", gap: 4 }}>
|
|
27
|
+
<Text style={{ fontSize: 14, lineHeight: 20, color: tokens.foreground }}>Press </Text>
|
|
28
|
+
<Kbd>⌘</Kbd>
|
|
29
|
+
<Kbd>K</Kbd>
|
|
30
|
+
<Text style={{ fontSize: 14, lineHeight: 20, color: tokens.foreground }}> to search.</Text>
|
|
31
|
+
</View>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Do & Don't
|
|
35
|
+
|
|
36
|
+
### Single
|
|
37
|
+
|
|
38
|
+
**Do** — Use the single mode for one real key; give each cap exactly one key.
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
<Kbd>Esc</Kbd>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Don't** — Packing a whole shortcut into one key cap reads as a single keystroke that does not exist.
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
<Kbd>⌘K</Kbd>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Combo
|
|
51
|
+
|
|
52
|
+
**Do** — Separate each key with a + so the combo reads as keys pressed together.
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 4 }}>
|
|
56
|
+
<Kbd>⌘</Kbd>
|
|
57
|
+
<Text style={{ fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] }}>+</Text>
|
|
58
|
+
<Kbd>⇧</Kbd>
|
|
59
|
+
<Text style={{ fontSize: 12, lineHeight: 16, color: tokens["muted-foreground"] }}>+</Text>
|
|
60
|
+
<Kbd>P</Kbd>
|
|
61
|
+
</View>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Don't** — Caps butted together with no separator blur into one token and hide that it is a chord.
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
<View style={{ flexDirection: "row", alignItems: "center" }}>
|
|
68
|
+
<Kbd>⌘</Kbd>
|
|
69
|
+
<Kbd>⇧</Kbd>
|
|
70
|
+
<Kbd>P</Kbd>
|
|
71
|
+
</View>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### In a sentence
|
|
75
|
+
|
|
76
|
+
**Do** — Wrap each key in a kbd so shortcuts read as physical keys.
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 4 }}>
|
|
80
|
+
<Text style={{ fontSize: 14, lineHeight: 20 }}>Press</Text>
|
|
81
|
+
<Kbd>Ctrl</Kbd>
|
|
82
|
+
<Kbd>K</Kbd>
|
|
83
|
+
<Text style={{ fontSize: 14, lineHeight: 20 }}>to search.</Text>
|
|
84
|
+
</View>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Don't** — Plain-text shortcuts blend into the prose and are easy to miss.
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
<Text style={{ fontSize: 14, lineHeight: 20 }}>Press Ctrl+K to search.</Text>
|
|
91
|
+
```
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type ViewStyle, type TextStyle } from "react-native";
|
|
2
|
+
import { type ColorTokens } from "../../style/index.js";
|
|
3
|
+
|
|
4
|
+
// Co-located Kbd styles. The cap has one fixed look (no axes), so the layout-only
|
|
5
|
+
// box geometry is a static fragment and the colored surface (border + muted fill)
|
|
6
|
+
// plus the muted label color are functions of the active tokens (so the cap
|
|
7
|
+
// follows light/dark and reads as glass at the theming level).
|
|
8
|
+
|
|
9
|
+
// The key-cap box: a centered row, fixed cap height, a minimum width so a single
|
|
10
|
+
// glyph still reads as a key, the small radius, a hairline border, and snug
|
|
11
|
+
// horizontal padding.
|
|
12
|
+
export const capBox: ViewStyle = {
|
|
13
|
+
flexDirection: "row",
|
|
14
|
+
height: 20,
|
|
15
|
+
minWidth: 20,
|
|
16
|
+
alignItems: "center",
|
|
17
|
+
justifyContent: "center",
|
|
18
|
+
borderRadius: 4,
|
|
19
|
+
borderWidth: 1,
|
|
20
|
+
paddingHorizontal: 6,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// The cap surface: the muted fill and the hairline border color.
|
|
24
|
+
export function capSurface(tokens: ColorTokens): ViewStyle {
|
|
25
|
+
return { borderColor: tokens.border, backgroundColor: tokens.muted };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// The key label: small, medium-weight, muted text.
|
|
29
|
+
export const labelType: TextStyle = { fontSize: 12, lineHeight: 16, fontWeight: "500" };
|
|
30
|
+
|
|
31
|
+
export function labelColor(tokens: ColorTokens): TextStyle {
|
|
32
|
+
return { color: tokens["muted-foreground"] };
|
|
33
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { View, Text, useTheme, type StyleProp, type ViewStyle } from "../../style/index.js";
|
|
3
|
+
import * as s from "./kbd.styles.js";
|
|
4
|
+
|
|
5
|
+
// Kbd: a keyboard shortcut indicator. Renders a single key cap, a small
|
|
6
|
+
// bordered, slightly raised label with monospace-ish small text. The key label
|
|
7
|
+
// comes from children. One cap = one key; compose multiple Kbd caps with a
|
|
8
|
+
// separator for a chord (e.g. ⌘ + K), per the docs.
|
|
9
|
+
//
|
|
10
|
+
// The cap has one fixed look (no size or intent variants), so there are no
|
|
11
|
+
// boolean axes to map: the markup mirrors the docs' kbdCls exactly.
|
|
12
|
+
|
|
13
|
+
export interface KbdProps {
|
|
14
|
+
children?: ReactNode;
|
|
15
|
+
/** Escape hatch for layout/positioning composition (mainly width). */
|
|
16
|
+
style?: StyleProp<ViewStyle>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function Kbd({ children, style }: KbdProps) {
|
|
20
|
+
const { tokens } = useTheme();
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<View style={[s.capBox, s.capSurface(tokens), style]}>
|
|
24
|
+
{children != null ? <Text style={[s.labelType, s.labelColor(tokens)]}>{children}</Text> : null}
|
|
25
|
+
</View>
|
|
26
|
+
);
|
|
27
|
+
}
|