@medway-ui/native 1.7.2 → 1.8.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 +4 -1
- package/lib/commonjs/components/Badge.js +159 -36
- package/lib/commonjs/components/Badge.js.map +1 -1
- package/lib/commonjs/components/Choice.js +35 -37
- package/lib/commonjs/components/Choice.js.map +1 -1
- package/lib/commonjs/components/SegmentControl.js +197 -0
- package/lib/commonjs/components/SegmentControl.js.map +1 -0
- package/lib/commonjs/editor/editorHtml.generated.js +1 -1
- package/lib/commonjs/editor/editorHtml.generated.js.map +1 -1
- package/lib/commonjs/hooks/useThemeColor.js +42 -42
- package/lib/commonjs/hooks/useThemeColor.js.map +1 -1
- package/lib/commonjs/index.js +31 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/lib/raised-surface.js +24 -0
- package/lib/commonjs/lib/raised-surface.js.map +1 -0
- package/lib/module/components/Badge.js +159 -36
- package/lib/module/components/Badge.js.map +1 -1
- package/lib/module/components/Choice.js +35 -37
- package/lib/module/components/Choice.js.map +1 -1
- package/lib/module/components/SegmentControl.js +189 -0
- package/lib/module/components/SegmentControl.js.map +1 -0
- package/lib/module/editor/editorHtml.generated.js +1 -1
- package/lib/module/editor/editorHtml.generated.js.map +1 -1
- package/lib/module/hooks/useThemeColor.js +42 -42
- package/lib/module/hooks/useThemeColor.js.map +1 -1
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/lib/raised-surface.js +22 -0
- package/lib/module/lib/raised-surface.js.map +1 -0
- package/lib/typescript/components/Badge.d.ts +14 -4
- package/lib/typescript/components/Badge.d.ts.map +1 -1
- package/lib/typescript/components/Chip.d.ts +2 -2
- package/lib/typescript/components/Choice.d.ts.map +1 -1
- package/lib/typescript/components/SegmentControl.d.ts +37 -0
- package/lib/typescript/components/SegmentControl.d.ts.map +1 -0
- package/lib/typescript/components/Tag.d.ts +2 -2
- package/lib/typescript/editor/editorHtml.generated.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +3 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/lib/raised-surface.d.ts +3 -0
- package/lib/typescript/lib/raised-surface.d.ts.map +1 -0
- package/package.json +1 -1
- package/setup/tokens.native.css +49 -22
- package/src/components/Badge.tsx +207 -61
- package/src/components/Choice.tsx +66 -45
- package/src/components/SegmentControl.tsx +265 -0
- package/src/editor/editorHtml.generated.ts +1 -1
- package/src/hooks/useThemeColor.ts +42 -42
- package/src/index.ts +21 -1
- package/src/lib/raised-surface.tsx +21 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Children,
|
|
3
|
+
createContext,
|
|
4
|
+
useContext,
|
|
5
|
+
useMemo,
|
|
6
|
+
useState,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
} from "react";
|
|
9
|
+
import { Pressable, View, type Insets } from "react-native";
|
|
10
|
+
import * as TabsPrimitive from "@rn-primitives/tabs";
|
|
11
|
+
import { cva } from "class-variance-authority";
|
|
12
|
+
|
|
13
|
+
import { Badge, type BadgeProps } from "@/components/Badge";
|
|
14
|
+
import { Text } from "@/components/Text";
|
|
15
|
+
import { RaisedSurface } from "@/lib/raised-surface";
|
|
16
|
+
import { cn } from "@/lib/utils";
|
|
17
|
+
|
|
18
|
+
type SegmentSize = "sm" | "md" | "lg";
|
|
19
|
+
|
|
20
|
+
const SegmentControlContext = createContext<{ size: SegmentSize }>({
|
|
21
|
+
size: "md",
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// `min-h` instead of the web's `max-h`: a larger system font grows the segment instead of clipping it.
|
|
25
|
+
const segmentControlTriggerVariants = cva(
|
|
26
|
+
"shrink flex-row items-center justify-center",
|
|
27
|
+
{
|
|
28
|
+
variants: {
|
|
29
|
+
size: {
|
|
30
|
+
sm: "min-h-7 gap-2 px-3 py-1.5",
|
|
31
|
+
md: "min-h-9 gap-2.5 px-3.5 py-2",
|
|
32
|
+
lg: "min-h-11 gap-3 px-4 py-2.5",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
defaultVariants: { size: "md" },
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// Up to the package's 44pt touch target (`MIN_TOUCH_TARGET`), vertically only: segments sit 2px apart.
|
|
40
|
+
const TRIGGER_HIT_SLOP: Record<SegmentSize, Insets | undefined> = {
|
|
41
|
+
sm: { top: 8, bottom: 8 },
|
|
42
|
+
md: { top: 4, bottom: 4 },
|
|
43
|
+
lg: undefined,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const segmentControlTextVariants = cva("text-center", {
|
|
47
|
+
variants: {
|
|
48
|
+
size: {
|
|
49
|
+
sm: "text-xs leading-xs",
|
|
50
|
+
md: "text-sm leading-sm",
|
|
51
|
+
lg: "text-md leading-md",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
defaultVariants: { size: "md" },
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
interface SegmentControlProps
|
|
58
|
+
extends Omit<TabsPrimitive.RootProps, "value" | "onValueChange"> {
|
|
59
|
+
value?: string;
|
|
60
|
+
defaultValue?: string;
|
|
61
|
+
onValueChange?: (value: string) => void;
|
|
62
|
+
size?: SegmentSize;
|
|
63
|
+
className?: string;
|
|
64
|
+
children?: ReactNode;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function SegmentControl({
|
|
68
|
+
value: valueProp,
|
|
69
|
+
defaultValue = "",
|
|
70
|
+
onValueChange,
|
|
71
|
+
size = "md",
|
|
72
|
+
className,
|
|
73
|
+
children,
|
|
74
|
+
...props
|
|
75
|
+
}: SegmentControlProps) {
|
|
76
|
+
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
|
|
77
|
+
const value = valueProp ?? uncontrolledValue;
|
|
78
|
+
const context = useMemo(() => ({ size }), [size]);
|
|
79
|
+
|
|
80
|
+
const handleValueChange = (next: string) => {
|
|
81
|
+
if (next === value) return;
|
|
82
|
+
if (valueProp === undefined) setUncontrolledValue(next);
|
|
83
|
+
onValueChange?.(next);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<SegmentControlContext.Provider value={context}>
|
|
88
|
+
<TabsPrimitive.Root
|
|
89
|
+
asChild
|
|
90
|
+
value={value}
|
|
91
|
+
onValueChange={handleValueChange}
|
|
92
|
+
{...props}
|
|
93
|
+
>
|
|
94
|
+
<View className={cn("gap-2", className)}>{children}</View>
|
|
95
|
+
</TabsPrimitive.Root>
|
|
96
|
+
</SegmentControlContext.Provider>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface SegmentControlListProps extends TabsPrimitive.ListProps {
|
|
101
|
+
className?: string;
|
|
102
|
+
children?: ReactNode;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function SegmentControlList({
|
|
106
|
+
className,
|
|
107
|
+
children,
|
|
108
|
+
...props
|
|
109
|
+
}: SegmentControlListProps) {
|
|
110
|
+
return (
|
|
111
|
+
<TabsPrimitive.List asChild {...props}>
|
|
112
|
+
<View
|
|
113
|
+
className={cn(
|
|
114
|
+
"flex-row items-center gap-0.5 self-start rounded-md bg-gray-background-soft p-0.5",
|
|
115
|
+
className
|
|
116
|
+
)}
|
|
117
|
+
>
|
|
118
|
+
{children}
|
|
119
|
+
</View>
|
|
120
|
+
</TabsPrimitive.List>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface SegmentControlTriggerProps extends TabsPrimitive.TriggerProps {
|
|
125
|
+
className?: string;
|
|
126
|
+
children?: ReactNode;
|
|
127
|
+
icon?: ReactNode;
|
|
128
|
+
badge?: number | string;
|
|
129
|
+
badgeVariant?: BadgeProps["variant"];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function shouldShowBadge(
|
|
133
|
+
badge: number | string | undefined,
|
|
134
|
+
disabled: boolean
|
|
135
|
+
): badge is number | string {
|
|
136
|
+
return (
|
|
137
|
+
badge != null && badge !== 0 && badge !== "" && badge !== "0" && !disabled
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Strings and numbers, as in `Aulas {count}`, join into one label; anything else renders as is.
|
|
142
|
+
function toLabel(children: ReactNode): string | null {
|
|
143
|
+
const parts = Children.toArray(children);
|
|
144
|
+
return parts.length > 0 &&
|
|
145
|
+
parts.every((part) => typeof part === "string" || typeof part === "number")
|
|
146
|
+
? parts.join("")
|
|
147
|
+
: null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function SegmentControlTrigger({
|
|
151
|
+
className,
|
|
152
|
+
children,
|
|
153
|
+
icon,
|
|
154
|
+
badge,
|
|
155
|
+
badgeVariant,
|
|
156
|
+
value,
|
|
157
|
+
disabled,
|
|
158
|
+
accessibilityLabel,
|
|
159
|
+
...props
|
|
160
|
+
}: SegmentControlTriggerProps) {
|
|
161
|
+
const { size } = useContext(SegmentControlContext);
|
|
162
|
+
const { value: activeValue } = TabsPrimitive.useRootContext();
|
|
163
|
+
const isActive = activeValue === value;
|
|
164
|
+
const isDisabled = !!disabled;
|
|
165
|
+
const showBadge = shouldShowBadge(badge, isDisabled);
|
|
166
|
+
const label = toLabel(children);
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<TabsPrimitive.Trigger asChild value={value} disabled={disabled} {...props}>
|
|
170
|
+
<Pressable
|
|
171
|
+
accessibilityLabel={
|
|
172
|
+
accessibilityLabel ??
|
|
173
|
+
(label != null
|
|
174
|
+
? showBadge
|
|
175
|
+
? `${label}, ${badge}`
|
|
176
|
+
: label
|
|
177
|
+
: undefined)
|
|
178
|
+
}
|
|
179
|
+
hitSlop={TRIGGER_HIT_SLOP[size]}
|
|
180
|
+
className={cn(segmentControlTriggerVariants({ size }), className)}
|
|
181
|
+
>
|
|
182
|
+
{({ pressed }) => (
|
|
183
|
+
<>
|
|
184
|
+
{isActive ? <RaisedSurface /> : null}
|
|
185
|
+
{icon ? <View className="shrink-0">{icon}</View> : null}
|
|
186
|
+
{label != null ? (
|
|
187
|
+
<View className="min-w-0 shrink">
|
|
188
|
+
{/* Holds the semibold width, so the row doesn't shift when the active segment changes. */}
|
|
189
|
+
<Text
|
|
190
|
+
numberOfLines={1}
|
|
191
|
+
accessibilityElementsHidden
|
|
192
|
+
importantForAccessibility="no-hide-descendants"
|
|
193
|
+
className={cn(
|
|
194
|
+
segmentControlTextVariants({ size }),
|
|
195
|
+
"font-semibold opacity-0"
|
|
196
|
+
)}
|
|
197
|
+
>
|
|
198
|
+
{label}
|
|
199
|
+
</Text>
|
|
200
|
+
<Text
|
|
201
|
+
numberOfLines={1}
|
|
202
|
+
className={cn(
|
|
203
|
+
segmentControlTextVariants({ size }),
|
|
204
|
+
"absolute inset-0",
|
|
205
|
+
isActive ? "font-semibold" : "font-medium",
|
|
206
|
+
isDisabled
|
|
207
|
+
? "text-gray-foreground-muted"
|
|
208
|
+
: isActive
|
|
209
|
+
? pressed
|
|
210
|
+
? "text-primary-foreground-emphasis"
|
|
211
|
+
: "text-primary-foreground"
|
|
212
|
+
: pressed
|
|
213
|
+
? "text-gray-foreground-emphasis"
|
|
214
|
+
: "text-gray-foreground"
|
|
215
|
+
)}
|
|
216
|
+
>
|
|
217
|
+
{label}
|
|
218
|
+
</Text>
|
|
219
|
+
</View>
|
|
220
|
+
) : (
|
|
221
|
+
children
|
|
222
|
+
)}
|
|
223
|
+
{showBadge ? (
|
|
224
|
+
<Badge size={size} variant={badgeVariant}>
|
|
225
|
+
{String(badge)}
|
|
226
|
+
</Badge>
|
|
227
|
+
) : null}
|
|
228
|
+
</>
|
|
229
|
+
)}
|
|
230
|
+
</Pressable>
|
|
231
|
+
</TabsPrimitive.Trigger>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
interface SegmentControlContentProps extends TabsPrimitive.ContentProps {
|
|
236
|
+
className?: string;
|
|
237
|
+
children?: ReactNode;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function SegmentControlContent({
|
|
241
|
+
className,
|
|
242
|
+
children,
|
|
243
|
+
...props
|
|
244
|
+
}: SegmentControlContentProps) {
|
|
245
|
+
return (
|
|
246
|
+
<TabsPrimitive.Content asChild {...props}>
|
|
247
|
+
<View className={cn("flex-1", className)}>{children}</View>
|
|
248
|
+
</TabsPrimitive.Content>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export {
|
|
253
|
+
SegmentControl,
|
|
254
|
+
SegmentControlContent,
|
|
255
|
+
SegmentControlList,
|
|
256
|
+
SegmentControlTrigger,
|
|
257
|
+
segmentControlTriggerVariants,
|
|
258
|
+
};
|
|
259
|
+
export type {
|
|
260
|
+
SegmentControlContentProps,
|
|
261
|
+
SegmentControlListProps,
|
|
262
|
+
SegmentControlProps,
|
|
263
|
+
SegmentControlTriggerProps,
|
|
264
|
+
SegmentSize,
|
|
265
|
+
};
|