@mrmeg/expo-ui 0.14.0 → 0.16.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/CHANGELOG.md +81 -0
- package/README.md +50 -6
- package/dist/components/Accordion.d.ts +1 -1
- package/dist/components/Accordion.js +25 -19
- package/dist/components/Badge.js +10 -7
- package/dist/components/BottomSheet.js +21 -19
- package/dist/components/Button.d.ts +2 -1
- package/dist/components/Button.js +22 -6
- package/dist/components/Card.js +5 -3
- package/dist/components/Checkbox.js +49 -19
- package/dist/components/Collapsible.d.ts +1 -1
- package/dist/components/Collapsible.js +22 -16
- package/dist/components/Drawer.js +8 -7
- package/dist/components/DropdownMenu.d.ts +1 -1
- package/dist/components/DropdownMenu.js +80 -59
- package/dist/components/EmptyState.d.ts +7 -1
- package/dist/components/EmptyState.js +26 -9
- package/dist/components/InputOTP.js +10 -4
- package/dist/components/Item.d.ts +93 -0
- package/dist/components/Item.js +125 -0
- package/dist/components/Label.js +10 -4
- package/dist/components/Notification.js +14 -24
- package/dist/components/Popover.d.ts +3 -18
- package/dist/components/Popover.js +10 -6
- package/dist/components/RadioGroup.js +49 -22
- package/dist/components/SectionHeader.d.ts +42 -0
- package/dist/components/SectionHeader.js +41 -0
- package/dist/components/Select.js +73 -41
- package/dist/components/Skeleton.js +5 -3
- package/dist/components/StatCard.d.ts +45 -0
- package/dist/components/StatCard.js +41 -0
- package/dist/components/StyledText.d.ts +20 -10
- package/dist/components/StyledText.js +85 -29
- package/dist/components/Switch.js +84 -57
- package/dist/components/Tabs.js +43 -12
- package/dist/components/TextInput.js +17 -11
- package/dist/components/Toggle.js +69 -42
- package/dist/components/ToggleGroup.js +82 -55
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.js +3 -0
- package/dist/constants/fonts.d.ts +44 -5
- package/dist/constants/fonts.js +42 -12
- package/dist/constants/index.d.ts +1 -0
- package/dist/constants/index.js +1 -0
- package/dist/constants/motion.d.ts +24 -0
- package/dist/constants/motion.js +24 -0
- package/dist/constants/spacing.d.ts +7 -7
- package/dist/constants/spacing.js +7 -7
- package/dist/hooks/useResources.d.ts +4 -2
- package/dist/hooks/useResources.js +30 -8
- package/dist/hooks/useStaggeredEntrance.d.ts +1 -1
- package/dist/hooks/useStaggeredEntrance.js +2 -1
- package/dist/hooks/useTheme.d.ts +1 -1
- package/dist/hooks/useTheme.js +53 -16
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.js +1 -0
- package/dist/lib/themedStyles.d.ts +21 -0
- package/dist/lib/themedStyles.js +33 -0
- package/dist/state/themeStore.d.ts +17 -0
- package/dist/state/themeStore.js +6 -0
- package/package.json +20 -19
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { type Ref } from "react";
|
|
2
2
|
import { Text as RNText, TextProps as RNTextProps } from "react-native";
|
|
3
3
|
/**
|
|
4
|
-
* Font size variants
|
|
4
|
+
* Font size variants for the type scale (Inter sans-serif / Georgia serif)
|
|
5
5
|
*/
|
|
6
6
|
export type FontSize = "xs" | "sm" | "base" | "body" | "lg" | "xl" | "xxl" | "display";
|
|
7
7
|
/**
|
|
8
8
|
* Semantic text variants for different use cases
|
|
9
9
|
*/
|
|
10
|
-
export type SemanticVariant = "title" | "heading" | "subheading" | "body" | "caption" | "label";
|
|
10
|
+
export type SemanticVariant = "title" | "heading" | "subheading" | "body" | "caption" | "label" | "eyebrow";
|
|
11
11
|
/**
|
|
12
12
|
* Font weight options
|
|
13
13
|
*/
|
|
@@ -18,7 +18,7 @@ export type FontWeight = "light" | "regular" | "medium" | "semibold" | "bold";
|
|
|
18
18
|
export type TextAlign = "left" | "center" | "right" | "justify" | "auto";
|
|
19
19
|
export type TextProps = RNTextProps & {
|
|
20
20
|
/**
|
|
21
|
-
* Font variant - serif (
|
|
21
|
+
* Font variant - serif (Georgia) or sans-serif (Inter)
|
|
22
22
|
*/
|
|
23
23
|
variant?: "serif" | "sansSerif";
|
|
24
24
|
/**
|
|
@@ -62,9 +62,12 @@ export type TextProps = RNTextProps & {
|
|
|
62
62
|
*
|
|
63
63
|
* Features:
|
|
64
64
|
* - Size variants (xs, sm, base, body, lg, xl, xxl, display)
|
|
65
|
-
* - Semantic variants (title, heading, subheading, body, caption, label)
|
|
65
|
+
* - Semantic variants (title, heading, subheading, body, caption, label, eyebrow)
|
|
66
66
|
* - Text alignment prop
|
|
67
|
-
* - Font weight options (light, regular, medium, semibold, bold)
|
|
67
|
+
* - Font weight options (light, regular, medium, semibold, bold) — resolves to
|
|
68
|
+
* real Inter font files on native, and family + numeric fontWeight on web
|
|
69
|
+
* - Per-size letter-spacing scale (tight on large headings, positive on micro
|
|
70
|
+
* labels); explicit `style` letterSpacing always wins
|
|
68
71
|
* - Text selection enabled by default; pass `selectable={false}` for control
|
|
69
72
|
* chrome such as button labels, tabs, badges, and field labels
|
|
70
73
|
* - numberOfLines and ellipsizeMode support from RN TextProps
|
|
@@ -72,27 +75,28 @@ export type TextProps = RNTextProps & {
|
|
|
72
75
|
export declare function StyledText(props: TextProps): import("react").JSX.Element;
|
|
73
76
|
/**
|
|
74
77
|
* Serif Text Component
|
|
75
|
-
* Uses serif font family (
|
|
78
|
+
* Uses the serif font family (Georgia)
|
|
76
79
|
*/
|
|
77
80
|
export declare function SerifText(props: TextProps): import("react").JSX.Element;
|
|
78
81
|
/**
|
|
79
82
|
* Sans-Serif Text Component
|
|
80
|
-
* Uses sans-serif font family (
|
|
83
|
+
* Uses the sans-serif font family (Inter)
|
|
81
84
|
*/
|
|
82
85
|
export declare function SansSerifText(props: TextProps): import("react").JSX.Element;
|
|
83
86
|
/**
|
|
84
87
|
* Serif Bold Text Component
|
|
85
|
-
* Uses serif font family —
|
|
88
|
+
* Uses the serif font family — Georgia's only shipped weight is regular
|
|
86
89
|
*/
|
|
87
90
|
export declare function SerifBoldText(props: TextProps): import("react").JSX.Element;
|
|
88
91
|
/**
|
|
89
92
|
* Sans-Serif Bold Text Component
|
|
90
|
-
* Uses sans-serif font family
|
|
93
|
+
* Uses the sans-serif font family at semibold weight (Inter 600)
|
|
91
94
|
*/
|
|
92
95
|
export declare function SansSerifBoldText(props: TextProps): import("react").JSX.Element;
|
|
93
96
|
/**
|
|
94
97
|
* Display Text Component
|
|
95
|
-
* Uses
|
|
98
|
+
* Uses the serif font family (Georgia) at display size — for hero text and
|
|
99
|
+
* splash screens
|
|
96
100
|
*/
|
|
97
101
|
export declare function DisplayText(props: TextProps): import("react").JSX.Element;
|
|
98
102
|
/**
|
|
@@ -119,3 +123,9 @@ export declare function CaptionText(props: TextProps): import("react").JSX.Eleme
|
|
|
119
123
|
* Label Text - Medium weight text for form labels
|
|
120
124
|
*/
|
|
121
125
|
export declare function LabelText(props: TextProps): import("react").JSX.Element;
|
|
126
|
+
/**
|
|
127
|
+
* Eyebrow Text - Small, semibold, uppercase section label with wide positive
|
|
128
|
+
* tracking (the overline idiom above a heading). Consumed by the SectionHeader
|
|
129
|
+
* primitive (separate spec).
|
|
130
|
+
*/
|
|
131
|
+
export declare function EyebrowText(props: TextProps): import("react").JSX.Element;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { use } from "react";
|
|
3
|
-
import { Text as RNText, StyleSheet } from "react-native";
|
|
3
|
+
import { Platform, Text as RNText, StyleSheet } from "react-native";
|
|
4
4
|
import { useTheme } from "../hooks/useTheme.js";
|
|
5
|
-
import { fontFamilies } from "../constants/fonts.js";
|
|
5
|
+
import { fontFamilies, defaultWebWeightStrategy } from "../constants/fonts.js";
|
|
6
|
+
import { useThemeStore } from "../state/themeStore.js";
|
|
6
7
|
import { translateText } from "../lib/i18n.js";
|
|
7
8
|
import { TextColorContext, TextSelectabilityContext, TextStyleContext } from "./StyledText.context";
|
|
8
9
|
const FONT_SIZES = {
|
|
@@ -25,11 +26,24 @@ const LINE_HEIGHTS = {
|
|
|
25
26
|
xxl: 33.6,
|
|
26
27
|
display: 40.8,
|
|
27
28
|
};
|
|
29
|
+
// Per-size tracking (px), derived from em targets (em × fontSize, rounded to
|
|
30
|
+
// 2 decimals): tight tracking on large headings, neutral through body text,
|
|
31
|
+
// positive tracking on small/micro labels so they don't read cramped at size.
|
|
32
|
+
// RN's `letterSpacing` is px, not em, hence the pre-multiplied values here.
|
|
28
33
|
const LETTER_SPACING = {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
xs: 0.11, // +0.01em × 11
|
|
35
|
+
sm: 0.06, // +0.005em × 12
|
|
36
|
+
base: 0,
|
|
37
|
+
body: 0,
|
|
38
|
+
lg: -0.09, // -0.005em × 18
|
|
39
|
+
xl: -0.22, // -0.01em × 22
|
|
40
|
+
xxl: -0.42, // -0.015em × 28
|
|
41
|
+
display: -0.68, // -0.02em × 34
|
|
32
42
|
};
|
|
43
|
+
// Eyebrow/overline tracking is deliberately wider than the "sm" default
|
|
44
|
+
// above — it's a distinct uppercase-label treatment, not the same tracking
|
|
45
|
+
// used for ordinary small text at that size. +0.08em × 12px (sm fontSize).
|
|
46
|
+
const EYEBROW_LETTER_SPACING = 0.96;
|
|
33
47
|
const SEMANTIC_CONFIGS = {
|
|
34
48
|
title: { size: "xxl", weight: "semibold" },
|
|
35
49
|
heading: { size: "xl", weight: "semibold" },
|
|
@@ -37,19 +51,25 @@ const SEMANTIC_CONFIGS = {
|
|
|
37
51
|
body: { size: "body", weight: "regular" },
|
|
38
52
|
caption: { size: "sm", weight: "regular" },
|
|
39
53
|
label: { size: "base", weight: "medium" },
|
|
54
|
+
// Section-label idiom (uppercase, widely tracked) — see EYEBROW_LETTER_SPACING
|
|
55
|
+
// and the textTransform applied in StyledText below.
|
|
56
|
+
eyebrow: { size: "sm", weight: "semibold" },
|
|
40
57
|
};
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
// Font family weight keys now map 1:1 onto FontWeight — every weight has a
|
|
59
|
+
// real family entry (native: a distinct static Inter file; web: shared
|
|
60
|
+
// "Inter" family + numeric fontWeight, see WEB_FONT_WEIGHTS). Defaults to
|
|
61
|
+
// "regular" when no weight is requested.
|
|
62
|
+
const getFontFamilyWeight = (weight) => weight ?? "regular";
|
|
63
|
+
// Web-only: the numeric fontWeight that picks the right @font-face variant
|
|
64
|
+
// out of the single shared "Inter" CSS family. "light" has no loaded 300
|
|
65
|
+
// weight (mirrors the native light->regular mapping in constants/fonts.ts),
|
|
66
|
+
// so it renders at 400 like "regular".
|
|
67
|
+
const WEB_FONT_WEIGHTS = {
|
|
68
|
+
light: "400",
|
|
69
|
+
regular: "400",
|
|
70
|
+
medium: "500",
|
|
71
|
+
semibold: "600",
|
|
72
|
+
bold: "700",
|
|
53
73
|
};
|
|
54
74
|
/**
|
|
55
75
|
* Base Text component with theme and variant support
|
|
@@ -57,9 +77,12 @@ const getFontFamilyWeight = (weight) => {
|
|
|
57
77
|
*
|
|
58
78
|
* Features:
|
|
59
79
|
* - Size variants (xs, sm, base, body, lg, xl, xxl, display)
|
|
60
|
-
* - Semantic variants (title, heading, subheading, body, caption, label)
|
|
80
|
+
* - Semantic variants (title, heading, subheading, body, caption, label, eyebrow)
|
|
61
81
|
* - Text alignment prop
|
|
62
|
-
* - Font weight options (light, regular, medium, semibold, bold)
|
|
82
|
+
* - Font weight options (light, regular, medium, semibold, bold) — resolves to
|
|
83
|
+
* real Inter font files on native, and family + numeric fontWeight on web
|
|
84
|
+
* - Per-size letter-spacing scale (tight on large headings, positive on micro
|
|
85
|
+
* labels); explicit `style` letterSpacing always wins
|
|
63
86
|
* - Text selection enabled by default; pass `selectable={false}` for control
|
|
64
87
|
* chrome such as button labels, tabs, badges, and field labels
|
|
65
88
|
* - numberOfLines and ellipsizeMode support from RN TextProps
|
|
@@ -67,6 +90,9 @@ const getFontFamilyWeight = (weight) => {
|
|
|
67
90
|
export function StyledText(props) {
|
|
68
91
|
const { tx, text, txOptions, style, variant = "sansSerif", fontWeight, size, semantic, align, selectable, children, ref, ...otherProps } = props;
|
|
69
92
|
const { theme } = useTheme();
|
|
93
|
+
// Subscribed, not read via getState(), so a `setFonts` call after mount
|
|
94
|
+
// re-renders text instead of leaving stale families on screen.
|
|
95
|
+
const fontOverrides = useThemeStore((s) => s.fontOverrides);
|
|
70
96
|
// Check if there's a color override from parent context (e.g., Button)
|
|
71
97
|
const contextColor = use(TextColorContext);
|
|
72
98
|
const contextTextStyle = use(TextStyleContext);
|
|
@@ -78,16 +104,35 @@ export function StyledText(props) {
|
|
|
78
104
|
const semanticConfig = semantic ? SEMANTIC_CONFIGS[semantic] : undefined;
|
|
79
105
|
const finalSize = semanticConfig?.size ?? size ?? "body";
|
|
80
106
|
const finalFontWeight = semanticConfig?.weight ?? fontWeight ?? "regular";
|
|
81
|
-
// Get font family based on variant and weight
|
|
107
|
+
// Get font family based on variant and weight. Host apps can replace the
|
|
108
|
+
// package's bundled faces via `setFonts` (see constants/fonts.ts); merging
|
|
109
|
+
// per-group means an app overriding only `sansSerif` keeps the default serif.
|
|
82
110
|
const fontFamilyWeight = getFontFamilyWeight(finalFontWeight);
|
|
83
|
-
|
|
111
|
+
const overriddenFamilies = fontOverrides.families;
|
|
112
|
+
const serifFamilies = overriddenFamilies?.serif ?? fontFamilies.serif;
|
|
113
|
+
const sansSerifFamilies = overriddenFamilies?.sansSerif ?? fontFamilies.sansSerif;
|
|
114
|
+
// The serif slot has a single usable weight — use it regardless of the
|
|
115
|
+
// requested weight.
|
|
84
116
|
const fontFamily = variant === "serif"
|
|
85
|
-
?
|
|
86
|
-
:
|
|
117
|
+
? serifFamilies.regular
|
|
118
|
+
: sansSerifFamilies[fontFamilyWeight] ?? sansSerifFamilies.regular;
|
|
119
|
+
// Weight is carried either by a numeric `fontWeight` (one multi-weight CSS
|
|
120
|
+
// family) or by the family name itself (discrete per-weight faces). Native
|
|
121
|
+
// is always the latter: adding a numeric weight on top of an already-bold
|
|
122
|
+
// file faux-bolds it. Web defaults to numeric for the package's own single
|
|
123
|
+
// "Inter" family, but an app loading per-weight faces through `expo-font`
|
|
124
|
+
// gets its own family per weight on web too, and must declare
|
|
125
|
+
// `webWeightStrategy: "family"` to avoid a second synthesised layer of bold.
|
|
126
|
+
const weightStrategy = Platform.OS === "web"
|
|
127
|
+
? fontOverrides.webWeightStrategy ?? defaultWebWeightStrategy
|
|
128
|
+
: "family";
|
|
129
|
+
const resolvedFontWeight = weightStrategy === "numeric"
|
|
130
|
+
? WEB_FONT_WEIGHTS[finalFontWeight]
|
|
131
|
+
: undefined;
|
|
87
132
|
// Get fontSize and lineHeight from size variant
|
|
88
133
|
const fontSize = FONT_SIZES[finalSize];
|
|
89
134
|
const lineHeight = LINE_HEIGHTS[finalSize];
|
|
90
|
-
const letterSpacing = LETTER_SPACING[finalSize];
|
|
135
|
+
const letterSpacing = semantic === "eyebrow" ? EYEBROW_LETTER_SPACING : LETTER_SPACING[finalSize];
|
|
91
136
|
// If style overrides fontSize without lineHeight, drop the preset lineHeight
|
|
92
137
|
// so RN uses its default (avoids clipping text with a mismatched lineHeight)
|
|
93
138
|
const flatStyle = style ? StyleSheet.flatten(style) : undefined;
|
|
@@ -107,10 +152,12 @@ export function StyledText(props) {
|
|
|
107
152
|
color,
|
|
108
153
|
fontFamily,
|
|
109
154
|
fontSize,
|
|
155
|
+
...(resolvedFontWeight !== undefined && { fontWeight: resolvedFontWeight }),
|
|
110
156
|
...(resolvedLineHeight !== undefined && { lineHeight: resolvedLineHeight }),
|
|
111
157
|
userSelect: resolvedSelectable ? "auto" : "none",
|
|
112
158
|
...(letterSpacing !== undefined && { letterSpacing }),
|
|
113
159
|
...(align && { textAlign: align }),
|
|
160
|
+
...(semantic === "eyebrow" && { textTransform: "uppercase" }),
|
|
114
161
|
},
|
|
115
162
|
resolvedContextTextStyle,
|
|
116
163
|
style,
|
|
@@ -118,35 +165,36 @@ export function StyledText(props) {
|
|
|
118
165
|
}
|
|
119
166
|
/**
|
|
120
167
|
* Serif Text Component
|
|
121
|
-
* Uses serif font family (
|
|
168
|
+
* Uses the serif font family (Georgia)
|
|
122
169
|
*/
|
|
123
170
|
export function SerifText(props) {
|
|
124
171
|
return _jsx(StyledText, { ...props, variant: "serif" });
|
|
125
172
|
}
|
|
126
173
|
/**
|
|
127
174
|
* Sans-Serif Text Component
|
|
128
|
-
* Uses sans-serif font family (
|
|
175
|
+
* Uses the sans-serif font family (Inter)
|
|
129
176
|
*/
|
|
130
177
|
export function SansSerifText(props) {
|
|
131
178
|
return _jsx(StyledText, { ...props, variant: "sansSerif" });
|
|
132
179
|
}
|
|
133
180
|
/**
|
|
134
181
|
* Serif Bold Text Component
|
|
135
|
-
* Uses serif font family —
|
|
182
|
+
* Uses the serif font family — Georgia's only shipped weight is regular
|
|
136
183
|
*/
|
|
137
184
|
export function SerifBoldText(props) {
|
|
138
185
|
return _jsx(StyledText, { ...props, variant: "serif", fontWeight: "regular" });
|
|
139
186
|
}
|
|
140
187
|
/**
|
|
141
188
|
* Sans-Serif Bold Text Component
|
|
142
|
-
* Uses sans-serif font family
|
|
189
|
+
* Uses the sans-serif font family at semibold weight (Inter 600)
|
|
143
190
|
*/
|
|
144
191
|
export function SansSerifBoldText(props) {
|
|
145
192
|
return _jsx(StyledText, { ...props, variant: "sansSerif", fontWeight: "semibold" });
|
|
146
193
|
}
|
|
147
194
|
/**
|
|
148
195
|
* Display Text Component
|
|
149
|
-
* Uses
|
|
196
|
+
* Uses the serif font family (Georgia) at display size — for hero text and
|
|
197
|
+
* splash screens
|
|
150
198
|
*/
|
|
151
199
|
export function DisplayText(props) {
|
|
152
200
|
return _jsx(StyledText, { ...props, variant: "serif", size: "display" });
|
|
@@ -188,3 +236,11 @@ export function CaptionText(props) {
|
|
|
188
236
|
export function LabelText(props) {
|
|
189
237
|
return _jsx(StyledText, { ...props, semantic: "label" });
|
|
190
238
|
}
|
|
239
|
+
/**
|
|
240
|
+
* Eyebrow Text - Small, semibold, uppercase section label with wide positive
|
|
241
|
+
* tracking (the overline idiom above a heading). Consumed by the SectionHeader
|
|
242
|
+
* primitive (separate spec).
|
|
243
|
+
*/
|
|
244
|
+
export function EyebrowText(props) {
|
|
245
|
+
return _jsx(StyledText, { ...props, semantic: "eyebrow" });
|
|
246
|
+
}
|
|
@@ -5,15 +5,41 @@ import { spacing } from "../constants/spacing.js";
|
|
|
5
5
|
import { useTheme } from "../hooks/useTheme.js";
|
|
6
6
|
import { hapticLight } from "../lib/haptics.js";
|
|
7
7
|
import * as SwitchPrimitives from "@rn-primitives/switch";
|
|
8
|
-
import { useCallback, useEffect, useRef } from "react";
|
|
8
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
9
9
|
import { ActivityIndicator, Platform, StyleSheet, View, Animated } from "react-native";
|
|
10
10
|
import { StyledText } from "./StyledText.js";
|
|
11
11
|
import { useReducedMotion } from "../hooks/useReduceMotion.js";
|
|
12
|
+
import { useScalePress } from "../hooks/useScalePress.js";
|
|
12
13
|
const DEFAULT_HIT_SLOP = 8;
|
|
13
14
|
function Switch({ variant = "default", labelOn, labelOff, size = { width: 44, height: 24 }, thumbSize = 20, loading = false, style: styleOverride, ...props }) {
|
|
14
|
-
const { theme, getContrastingColor, getShadowStyle, withAlpha } = useTheme();
|
|
15
|
+
const { theme, getContrastingColor, getShadowStyle, getFocusRingStyle, withAlpha } = useTheme();
|
|
15
16
|
const reduceMotion = useReducedMotion();
|
|
16
17
|
const hasMounted = useRef(false);
|
|
18
|
+
const [focused, setFocused] = useState(false);
|
|
19
|
+
const focusRingStyle = getFocusRingStyle();
|
|
20
|
+
const { animatedStyle: scaleStyle, pressHandlers } = useScalePress({
|
|
21
|
+
disabled: !!props.disabled,
|
|
22
|
+
scaleTo: 0.92,
|
|
23
|
+
haptic: false,
|
|
24
|
+
});
|
|
25
|
+
const showFocusRing = (event) => {
|
|
26
|
+
let ringVisible = true;
|
|
27
|
+
if (Platform.OS === "web") {
|
|
28
|
+
const target = event?.nativeEvent?.target;
|
|
29
|
+
if (target && typeof target.matches === "function") {
|
|
30
|
+
try {
|
|
31
|
+
ringVisible = target.matches(":focus-visible");
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
ringVisible = true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
setFocused(ringVisible);
|
|
39
|
+
};
|
|
40
|
+
const hideFocusRing = () => {
|
|
41
|
+
setFocused(false);
|
|
42
|
+
};
|
|
17
43
|
// Fire haptic on user-initiated toggles (skip initial mount)
|
|
18
44
|
const wrappedOnCheckedChange = useCallback((checked) => {
|
|
19
45
|
if (hasMounted.current)
|
|
@@ -67,61 +93,62 @@ function Switch({ variant = "default", labelOn, labelOff, size = { width: 44, he
|
|
|
67
93
|
const labelFontSize = size.height / 3;
|
|
68
94
|
// Flatten style override for web compatibility
|
|
69
95
|
const flattenedStyle = styleOverride ? StyleSheet.flatten(styleOverride) : undefined;
|
|
70
|
-
return (_jsxs(SwitchPrimitives.Root, { ...props, onCheckedChange: wrappedOnCheckedChange, style: {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
{
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
96
|
+
return (_jsx(Animated.View, { style: scaleStyle, children: _jsxs(SwitchPrimitives.Root, { ...props, onCheckedChange: wrappedOnCheckedChange, onPressIn: pressHandlers.onPressIn, onPressOut: pressHandlers.onPressOut, onFocus: showFocusRing, onBlur: hideFocusRing, style: {
|
|
97
|
+
position: "relative",
|
|
98
|
+
width: size.width,
|
|
99
|
+
height: size.height,
|
|
100
|
+
borderRadius: size.height / 2,
|
|
101
|
+
justifyContent: "center",
|
|
102
|
+
opacity: props.disabled ? 0.5 : 1,
|
|
103
|
+
...(Platform.OS === "web" && { cursor: props.disabled ? "not-allowed" : "pointer" }),
|
|
104
|
+
...(focused && !props.disabled ? focusRingStyle : null),
|
|
105
|
+
...(flattenedStyle || {}),
|
|
106
|
+
}, hitSlop: DEFAULT_HIT_SLOP, accessibilityRole: "switch", accessibilityState: {
|
|
107
|
+
checked: props.checked,
|
|
108
|
+
disabled: !!props.disabled,
|
|
109
|
+
busy: loading,
|
|
110
|
+
}, children: [_jsx(View, { style: {
|
|
111
|
+
...StyleSheet.absoluteFill,
|
|
112
|
+
borderRadius: size.height / 2,
|
|
113
|
+
backgroundColor: trackBg,
|
|
114
|
+
borderWidth: 1,
|
|
115
|
+
borderColor: trackBorderColor,
|
|
116
|
+
pointerEvents: "none",
|
|
117
|
+
} }), labelOn && !isIOS && (_jsx(View, { style: {
|
|
118
|
+
...styles.label,
|
|
119
|
+
left: labelHorizontalInset,
|
|
120
|
+
right: thumbInset + thumbSize + labelGap,
|
|
121
|
+
opacity: props.checked ? 1 : 0,
|
|
122
|
+
}, children: _jsx(StyledText, { selectable: false, style: {
|
|
123
|
+
fontFamily: fontFamilies.sansSerif.bold,
|
|
124
|
+
fontSize: labelFontSize,
|
|
125
|
+
color: labelOnColor,
|
|
126
|
+
userSelect: "none",
|
|
127
|
+
}, children: labelOn }) })), _jsx(SwitchPrimitives.Thumb, { children: _jsx(Animated.View, { style: [
|
|
128
|
+
{
|
|
129
|
+
width: thumbSize,
|
|
130
|
+
height: thumbSize,
|
|
131
|
+
marginLeft: thumbInset,
|
|
132
|
+
borderRadius: thumbSize / 2,
|
|
133
|
+
backgroundColor: thumbBackgroundColor,
|
|
134
|
+
borderWidth: 1,
|
|
135
|
+
borderColor: thumbBorderColor,
|
|
136
|
+
justifyContent: "center",
|
|
137
|
+
alignItems: "center",
|
|
138
|
+
...getShadowStyle("subtle"),
|
|
139
|
+
},
|
|
140
|
+
thumbAnimatedStyle,
|
|
141
|
+
], children: loading && (_jsx(ActivityIndicator, { size: "small", color: thumbIndicatorColor })) }) }), labelOff && !isIOS && (_jsx(View, { style: {
|
|
142
|
+
...styles.label,
|
|
143
|
+
left: thumbInset + thumbSize + labelGap,
|
|
144
|
+
right: labelHorizontalInset,
|
|
145
|
+
opacity: props.checked ? 0 : 1,
|
|
146
|
+
}, children: _jsx(StyledText, { selectable: false, style: {
|
|
147
|
+
fontFamily: fontFamilies.sansSerif.bold,
|
|
148
|
+
fontSize: labelFontSize,
|
|
149
|
+
color: theme.colors.text,
|
|
150
|
+
userSelect: "none",
|
|
151
|
+
}, children: labelOff }) }))] }) }));
|
|
125
152
|
}
|
|
126
153
|
const styles = StyleSheet.create({
|
|
127
154
|
label: {
|
package/dist/components/Tabs.js
CHANGED
|
@@ -7,6 +7,7 @@ import { TextClassContext, TextColorContext } from "./StyledText.context";
|
|
|
7
7
|
import { Icon } from "./Icon.js";
|
|
8
8
|
import { useTheme } from "../hooks/useTheme.js";
|
|
9
9
|
import { useReducedMotion } from "../hooks/useReduceMotion.js";
|
|
10
|
+
import { useScalePress } from "../hooks/useScalePress.js";
|
|
10
11
|
import { spacing } from "../constants/spacing.js";
|
|
11
12
|
// ============================================================================
|
|
12
13
|
// Size configs
|
|
@@ -56,7 +57,7 @@ function TabsList({ style, children, ...props }) {
|
|
|
56
57
|
return (_jsx(TabsPrimitive.List, { style: StyleSheet.flatten([listStyle, style]), ...props, children: children }));
|
|
57
58
|
}
|
|
58
59
|
function TabsTriggerInner({ icon, style, children, value, ...props }) {
|
|
59
|
-
const { theme, getShadowStyle } = useTheme();
|
|
60
|
+
const { theme, getShadowStyle, getFocusRingStyle } = useTheme();
|
|
60
61
|
const { variant, size } = useTabsContext();
|
|
61
62
|
const sizeConfig = SIZE_CONFIGS[size];
|
|
62
63
|
const reduceMotion = useReducedMotion();
|
|
@@ -77,6 +78,31 @@ function TabsTriggerInner({ icon, style, children, value, ...props }) {
|
|
|
77
78
|
: isSelected
|
|
78
79
|
? theme.colors.foreground
|
|
79
80
|
: theme.colors.mutedForeground;
|
|
81
|
+
const focusRingStyle = getFocusRingStyle();
|
|
82
|
+
const [focused, setFocused] = React.useState(false);
|
|
83
|
+
const { animatedStyle: scaleStyle, pressHandlers } = useScalePress({
|
|
84
|
+
disabled: isDisabled,
|
|
85
|
+
scaleTo: 0.97,
|
|
86
|
+
haptic: false,
|
|
87
|
+
});
|
|
88
|
+
const showFocusRing = (event) => {
|
|
89
|
+
let ringVisible = true;
|
|
90
|
+
if (Platform.OS === "web") {
|
|
91
|
+
const target = event?.nativeEvent?.target;
|
|
92
|
+
if (target && typeof target.matches === "function") {
|
|
93
|
+
try {
|
|
94
|
+
ringVisible = target.matches(":focus-visible");
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
ringVisible = true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
setFocused(ringVisible);
|
|
102
|
+
};
|
|
103
|
+
const hideFocusRing = () => {
|
|
104
|
+
setFocused(false);
|
|
105
|
+
};
|
|
80
106
|
const triggerBaseStyle = {
|
|
81
107
|
flex: 1,
|
|
82
108
|
// Allow the trigger to shrink below its content's intrinsic width so long
|
|
@@ -97,17 +123,22 @@ function TabsTriggerInner({ icon, style, children, value, ...props }) {
|
|
|
97
123
|
borderRadius: spacing.radiusSm,
|
|
98
124
|
...getShadowStyle("subtle"),
|
|
99
125
|
} : {};
|
|
100
|
-
return (_jsx(TextColorContext.Provider, { value: textColor, children: _jsx(TextClassContext.Provider, { value: "", children:
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
126
|
+
return (_jsx(TextColorContext.Provider, { value: textColor, children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(Animated.View, { style: [{ flex: 1, minWidth: 0 }, scaleStyle], children: _jsxs(TabsPrimitive.Trigger, { value: value, ...props, onPressIn: pressHandlers.onPressIn, onPressOut: pressHandlers.onPressOut, onFocus: showFocusRing, onBlur: hideFocusRing, style: StyleSheet.flatten([
|
|
127
|
+
triggerBaseStyle,
|
|
128
|
+
pillActiveStyle,
|
|
129
|
+
focused && !isDisabled && focusRingStyle,
|
|
130
|
+
style,
|
|
131
|
+
]), children: [_jsxs(View, { style: triggerContentStyles.container, children: [icon && (_jsx(Icon, { name: icon, size: sizeConfig.iconSize, color: textColor, decorative: true })), children] }), variant === "underline" && (_jsx(Animated.View, { style: [
|
|
132
|
+
{
|
|
133
|
+
position: "absolute",
|
|
134
|
+
bottom: 0,
|
|
135
|
+
left: 0,
|
|
136
|
+
right: 0,
|
|
137
|
+
height: 2,
|
|
138
|
+
backgroundColor: theme.colors.foreground,
|
|
139
|
+
},
|
|
140
|
+
{ opacity: activeOpacity },
|
|
141
|
+
] }))] }) }) }) }));
|
|
111
142
|
}
|
|
112
143
|
const TabsTrigger = TabsTriggerInner;
|
|
113
144
|
/**
|
|
@@ -8,6 +8,7 @@ import { fontFamilies } from "../constants/fonts.js";
|
|
|
8
8
|
import { StyledText } from "./StyledText.js";
|
|
9
9
|
import { Icon } from "./Icon.js";
|
|
10
10
|
import { hapticLight } from "../lib/haptics.js";
|
|
11
|
+
import { createThemedStyles } from "../lib/themedStyles.js";
|
|
11
12
|
import { palette } from "../constants/colors.js";
|
|
12
13
|
import { clearKeyboardFocusedInputLayout, setKeyboardFocusedInputLayout, } from "./keyboardFocusRegistry.js";
|
|
13
14
|
const NUMERIC_REGEX = /^[0-9]*$/;
|
|
@@ -91,7 +92,7 @@ export function TextInput(props) {
|
|
|
91
92
|
*/
|
|
92
93
|
function WebTextInput({ variant = "outline", size = "md", label, helperText, errorText, error, required, rows, showSecureEntryToggle, leftElement, rightElement, clearable = false, wrapperStyle, focusedStyle, forceLight, secureTextEntry, inputMode, style, onChangeText, onFocus, onBlur, value, multiline, editable = true, ref, ...rest }) {
|
|
93
94
|
const { theme, getContrastingColor, getFocusRingStyle } = useTheme();
|
|
94
|
-
const styles =
|
|
95
|
+
const styles = themedStyles(theme)[variant][size];
|
|
95
96
|
const [focused, setFocused] = useState(false);
|
|
96
97
|
const [contentHeight, setContentHeight] = useState(0);
|
|
97
98
|
const [passwordVisible, setPasswordVisible] = useState(false);
|
|
@@ -146,7 +147,7 @@ function WebTextInput({ variant = "outline", size = "md", label, helperText, err
|
|
|
146
147
|
const togglePasswordVisible = () => {
|
|
147
148
|
setPasswordVisible(v => !v);
|
|
148
149
|
};
|
|
149
|
-
return (_jsxs(View, { style: wrapperStyle, children: [!!label && (_jsx(View, { style: styles.labelContainer, children: _jsxs(StyledText, { selectable: false, style: styles.label, children: [label, required && _jsx(StyledText, { selectable: false, style: styles.required, children: " *" })] }) })), _jsxs(View, { style: [styles.wrapper, focused && getFocusRingStyle()], children: [leftElement && _jsx(View, { style: styles.leftElement, children: leftElement }), _jsx(RNTextInput, { ref: ref, ...rest, editable: editable, inputMode: inputMode || "text", multiline: multiline, numberOfLines: rows, secureTextEntry: secureTextEntry && !passwordVisible, onChangeText: inputMode === "numeric" ? handleNumericChange : handleTextChange, onFocus: showInputFocusRing, onBlur: hideInputFocusRing, onContentSizeChange: (e) => setContentHeight(e.nativeEvent.contentSize.height), scrollEnabled: shouldScroll, placeholderTextColor: theme.colors.textDim, style: [
|
|
150
|
+
return (_jsxs(View, { style: wrapperStyle, children: [!!label && (_jsx(View, { style: styles.labelContainer, children: _jsxs(StyledText, { selectable: false, size: "base", fontWeight: "medium", style: styles.label, children: [label, required && _jsx(StyledText, { selectable: false, fontWeight: "bold", style: styles.required, children: " *" })] }) })), _jsxs(View, { style: [styles.wrapper, focused && getFocusRingStyle()], children: [leftElement && _jsx(View, { style: styles.leftElement, children: leftElement }), _jsx(RNTextInput, { ref: ref, ...rest, editable: editable, inputMode: inputMode || "text", multiline: multiline, numberOfLines: rows, secureTextEntry: secureTextEntry && !passwordVisible, onChangeText: inputMode === "numeric" ? handleNumericChange : handleTextChange, onFocus: showInputFocusRing, onBlur: hideInputFocusRing, onContentSizeChange: (e) => setContentHeight(e.nativeEvent.contentSize.height), scrollEnabled: shouldScroll, placeholderTextColor: theme.colors.textDim, style: [
|
|
150
151
|
styles.input,
|
|
151
152
|
{
|
|
152
153
|
backgroundColor,
|
|
@@ -171,7 +172,7 @@ function WebTextInput({ variant = "outline", size = "md", label, helperText, err
|
|
|
171
172
|
}, accessibilityLabel: "Clear input", accessibilityRole: "button", children: _jsx(Icon, { name: "x", size: spacing.iconSm, color: "textDim", decorative: true }) })), showClearButton && rightElement && !hasSecureToggle && (_jsxs(View, { style: styles.rightElements, children: [_jsx(Pressable, { onPress: () => {
|
|
172
173
|
hapticLight();
|
|
173
174
|
onChangeText?.("");
|
|
174
|
-
}, accessibilityLabel: "Clear input", accessibilityRole: "button", children: _jsx(Icon, { name: "x", size: spacing.iconSm, color: "textDim", decorative: true }) }), rightElement] })), !showClearButton && rightElement && !hasSecureToggle && (_jsx(View, { style: styles.rightElement, children: rightElement })), secureTextEntry && showSecureEntryToggle && (_jsx(Pressable, { style: styles.passwordToggle, onPress: togglePasswordVisible, accessibilityLabel: passwordVisible ? "Hide password" : "Show password", accessibilityRole: "button", children: _jsx(Icon, { name: passwordVisible ? "eye-off" : "eye", size: spacing.iconSm + 4, color: "textDim" }) })), showErrorIcon && (_jsx(View, { style: [styles.errorIcon, { pointerEvents: "none" }], accessibilityLabel: "Error", children: _jsx(Icon, { name: "alert-circle", size: spacing.iconSm, color: "destructive", decorative: true }) }))] }), !!(helperText || errorText) && (_jsx(StyledText, { selectable: false, style: [
|
|
175
|
+
}, accessibilityLabel: "Clear input", accessibilityRole: "button", children: _jsx(Icon, { name: "x", size: spacing.iconSm, color: "textDim", decorative: true }) }), rightElement] })), !showClearButton && rightElement && !hasSecureToggle && (_jsx(View, { style: styles.rightElement, children: rightElement })), secureTextEntry && showSecureEntryToggle && (_jsx(Pressable, { style: styles.passwordToggle, onPress: togglePasswordVisible, accessibilityLabel: passwordVisible ? "Hide password" : "Show password", accessibilityRole: "button", children: _jsx(Icon, { name: passwordVisible ? "eye-off" : "eye", size: spacing.iconSm + 4, color: "textDim" }) })), showErrorIcon && (_jsx(View, { style: [styles.errorIcon, { pointerEvents: "none" }], accessibilityLabel: "Error", children: _jsx(Icon, { name: "alert-circle", size: spacing.iconSm, color: "destructive", decorative: true }) }))] }), !!(helperText || errorText) && (_jsx(StyledText, { selectable: false, size: "sm", style: [
|
|
175
176
|
styles.helperText,
|
|
176
177
|
hasError && styles.errorText,
|
|
177
178
|
], children: errorText || helperText }))] }));
|
|
@@ -197,7 +198,7 @@ function NativeTextInput({ variant = "outline", size = "md", label, helperText,
|
|
|
197
198
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
198
199
|
leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
199
200
|
const { theme, getContrastingColor } = useTheme();
|
|
200
|
-
const styles =
|
|
201
|
+
const styles = themedStyles(theme)[variant][size];
|
|
201
202
|
const [passwordVisible, setPasswordVisible] = useState(false);
|
|
202
203
|
const hasError = error || !!errorText;
|
|
203
204
|
const sizeConfig = SIZE_CONFIGS[size];
|
|
@@ -327,7 +328,7 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
327
328
|
fontSize: sizeConfig.fontSize,
|
|
328
329
|
...(nativeFontFamily ? { fontFamily: nativeFontFamily } : null),
|
|
329
330
|
};
|
|
330
|
-
return (_jsxs(View, { style: wrapperStyle, children: [!!label && (_jsx(View, { style: styles.labelContainer, children: _jsxs(StyledText, { selectable: false, style: styles.label, children: [label, required && _jsx(StyledText, { selectable: false, style: styles.required, children: " *" })] }) })), _jsxs(View, { ref: surfaceRef, style: [surfaceStyle, hasSecureToggle && styles.nativeRow], onLayout: syncFocusedInputLayout, children: [_jsx(Host, { matchContents: { vertical: true }, style: hasSecureToggle ? styles.nativeHostFlex : styles.nativeHost, children: _jsx(ExpoTextInput, { ...rest, ref: innerRef, value: state, defaultValue: defaultValue, onChangeText: onChangeText, onFocus: handleFocus, onBlur: handleBlur, editable: editable, multiline: multiline, rows: rows, inputMode: inputMode, secureTextEntry: effectiveSecureTextEntry, placeholderTextColor: theme.colors.textDim, style: boxStyle, textStyle: textStyle }) }), hasSecureToggle && (_jsx(Pressable, { style: styles.nativePasswordToggle, onPress: () => setPasswordVisible((v) => !v), accessibilityLabel: passwordVisible ? "Hide password" : "Show password", accessibilityRole: "button", children: _jsx(Icon, { name: passwordVisible ? "eye-off" : "eye", size: spacing.iconSm + 4, color: "textDim" }) }))] }), !!(helperText || errorText) && (_jsx(StyledText, { selectable: false, style: [styles.helperText, hasError && styles.errorText], children: errorText || helperText }))] }));
|
|
331
|
+
return (_jsxs(View, { style: wrapperStyle, children: [!!label && (_jsx(View, { style: styles.labelContainer, children: _jsxs(StyledText, { selectable: false, size: "base", fontWeight: "medium", style: styles.label, children: [label, required && _jsx(StyledText, { selectable: false, fontWeight: "bold", style: styles.required, children: " *" })] }) })), _jsxs(View, { ref: surfaceRef, style: [surfaceStyle, hasSecureToggle && styles.nativeRow], onLayout: syncFocusedInputLayout, children: [_jsx(Host, { matchContents: { vertical: true }, style: hasSecureToggle ? styles.nativeHostFlex : styles.nativeHost, children: _jsx(ExpoTextInput, { ...rest, ref: innerRef, value: state, defaultValue: defaultValue, onChangeText: onChangeText, onFocus: handleFocus, onBlur: handleBlur, editable: editable, multiline: multiline, rows: rows, inputMode: inputMode, secureTextEntry: effectiveSecureTextEntry, placeholderTextColor: theme.colors.textDim, style: boxStyle, textStyle: textStyle }) }), hasSecureToggle && (_jsx(Pressable, { style: styles.nativePasswordToggle, onPress: () => setPasswordVisible((v) => !v), accessibilityLabel: passwordVisible ? "Hide password" : "Show password", accessibilityRole: "button", children: _jsx(Icon, { name: passwordVisible ? "eye-off" : "eye", size: spacing.iconSm + 4, color: "textDim" }) }))] }), !!(helperText || errorText) && (_jsx(StyledText, { selectable: false, size: "sm", style: [styles.helperText, hasError && styles.errorText], children: errorText || helperText }))] }));
|
|
331
332
|
}
|
|
332
333
|
const createStyles = (theme, variant, size) => StyleSheet.create({
|
|
333
334
|
nativeHost: {
|
|
@@ -378,18 +379,17 @@ const createStyles = (theme, variant, size) => StyleSheet.create({
|
|
|
378
379
|
marginBottom: spacing.xs,
|
|
379
380
|
},
|
|
380
381
|
label: {
|
|
381
|
-
fontFamily
|
|
382
|
-
|
|
383
|
-
|
|
382
|
+
// fontFamily/fontSize/fontWeight now come from StyledText's
|
|
383
|
+
// size="base" fontWeight="medium" props (see JSX below), which resolve
|
|
384
|
+
// the platform-correct family + weight instead of a hardcoded pair.
|
|
384
385
|
color: theme.colors.text,
|
|
385
386
|
},
|
|
386
387
|
required: {
|
|
388
|
+
// fontFamily/fontWeight now come from StyledText's fontWeight="bold" prop.
|
|
387
389
|
color: theme.colors.destructive,
|
|
388
|
-
fontFamily: fontFamilies.sansSerif.bold,
|
|
389
390
|
},
|
|
390
391
|
helperText: {
|
|
391
|
-
fontFamily
|
|
392
|
-
fontSize: 12,
|
|
392
|
+
// fontFamily/fontSize now come from StyledText's size="sm" prop (see JSX below).
|
|
393
393
|
color: theme.colors.textDim,
|
|
394
394
|
marginTop: spacing.xs,
|
|
395
395
|
},
|
|
@@ -444,3 +444,9 @@ const createStyles = (theme, variant, size) => StyleSheet.create({
|
|
|
444
444
|
zIndex: 1,
|
|
445
445
|
},
|
|
446
446
|
});
|
|
447
|
+
const VARIANT_KEYS = ["outline", "filled", "underlined"];
|
|
448
|
+
const SIZE_KEYS = ["sm", "md", "lg"];
|
|
449
|
+
const themedStyles = createThemedStyles((theme) => Object.fromEntries(VARIANT_KEYS.map((variant) => [
|
|
450
|
+
variant,
|
|
451
|
+
Object.fromEntries(SIZE_KEYS.map((size) => [size, createStyles(theme, variant, size)])),
|
|
452
|
+
])));
|