@medway-ui/native 1.7.1 → 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.
Files changed (50) hide show
  1. package/README.md +92 -89
  2. package/lib/commonjs/components/Badge.js +159 -36
  3. package/lib/commonjs/components/Badge.js.map +1 -1
  4. package/lib/commonjs/components/Choice.js +35 -37
  5. package/lib/commonjs/components/Choice.js.map +1 -1
  6. package/lib/commonjs/components/SegmentControl.js +197 -0
  7. package/lib/commonjs/components/SegmentControl.js.map +1 -0
  8. package/lib/commonjs/editor/editorHtml.generated.js +1 -1
  9. package/lib/commonjs/editor/editorHtml.generated.js.map +1 -1
  10. package/lib/commonjs/hooks/useThemeColor.js +42 -42
  11. package/lib/commonjs/hooks/useThemeColor.js.map +1 -1
  12. package/lib/commonjs/index.js +31 -0
  13. package/lib/commonjs/index.js.map +1 -1
  14. package/lib/commonjs/lib/raised-surface.js +24 -0
  15. package/lib/commonjs/lib/raised-surface.js.map +1 -0
  16. package/lib/module/components/Badge.js +159 -36
  17. package/lib/module/components/Badge.js.map +1 -1
  18. package/lib/module/components/Choice.js +35 -37
  19. package/lib/module/components/Choice.js.map +1 -1
  20. package/lib/module/components/SegmentControl.js +189 -0
  21. package/lib/module/components/SegmentControl.js.map +1 -0
  22. package/lib/module/editor/editorHtml.generated.js +1 -1
  23. package/lib/module/editor/editorHtml.generated.js.map +1 -1
  24. package/lib/module/hooks/useThemeColor.js +42 -42
  25. package/lib/module/hooks/useThemeColor.js.map +1 -1
  26. package/lib/module/index.js +2 -0
  27. package/lib/module/index.js.map +1 -1
  28. package/lib/module/lib/raised-surface.js +22 -0
  29. package/lib/module/lib/raised-surface.js.map +1 -0
  30. package/lib/typescript/components/Badge.d.ts +14 -4
  31. package/lib/typescript/components/Badge.d.ts.map +1 -1
  32. package/lib/typescript/components/Chip.d.ts +2 -2
  33. package/lib/typescript/components/Choice.d.ts.map +1 -1
  34. package/lib/typescript/components/SegmentControl.d.ts +37 -0
  35. package/lib/typescript/components/SegmentControl.d.ts.map +1 -0
  36. package/lib/typescript/components/Tag.d.ts +2 -2
  37. package/lib/typescript/editor/editorHtml.generated.d.ts.map +1 -1
  38. package/lib/typescript/index.d.ts +3 -1
  39. package/lib/typescript/index.d.ts.map +1 -1
  40. package/lib/typescript/lib/raised-surface.d.ts +3 -0
  41. package/lib/typescript/lib/raised-surface.d.ts.map +1 -0
  42. package/package.json +2 -2
  43. package/setup/tokens.native.css +49 -22
  44. package/src/components/Badge.tsx +207 -61
  45. package/src/components/Choice.tsx +66 -45
  46. package/src/components/SegmentControl.tsx +265 -0
  47. package/src/editor/editorHtml.generated.ts +1 -1
  48. package/src/hooks/useThemeColor.ts +42 -42
  49. package/src/index.ts +21 -1
  50. package/src/lib/raised-surface.tsx +21 -0
@@ -247,12 +247,12 @@
247
247
 
248
248
  /* === Gold colors === */
249
249
  --color-gold: rgba(217, 172, 104, 1);
250
- --color-gold-darken-1: rgba(209, 154, 72, 1);
251
- --color-gold-darken-2: rgba(191, 134, 49, 1);
252
- --color-gold-darken-3: rgba(159, 111, 40, 1);
253
- --color-gold-darken-4: rgba(126, 88, 32, 1);
254
- --color-gold-darken-5: rgba(94, 66, 24, 1);
255
- --color-gold-darken-6: rgba(61, 43, 15, 1);
250
+ --color-gold-darken-1: rgba(200, 152, 81, 1);
251
+ --color-gold-darken-2: rgba(181, 132, 59, 1);
252
+ --color-gold-darken-3: rgba(151, 110, 48, 1);
253
+ --color-gold-darken-4: rgba(119, 87, 39, 1);
254
+ --color-gold-darken-5: rgba(89, 65, 29, 1);
255
+ --color-gold-darken-6: rgba(57, 42, 19, 1);
256
256
  --color-gold-lighten-1: rgba(221, 181, 121, 1);
257
257
  --color-gold-lighten-2: rgba(229, 199, 153, 1);
258
258
  --color-gold-lighten-3: rgba(233, 208, 170, 1);
@@ -366,8 +366,8 @@
366
366
  --color-secondary-0: rgba(1, 208, 180, 0);
367
367
 
368
368
  /* === Base drop-shadow colors === */
369
- --color-drop-shadow-layer-a: rgba(33, 38, 49, 0.16);
370
- --color-drop-shadow-layer-b: rgba(73, 85, 110, 0.16);
369
+ --color-drop-shadow-layer-a: rgba(33, 38, 49, 0.04);
370
+ --color-drop-shadow-layer-b: rgba(73, 85, 110, 0.08);
371
371
  --color-drop-shadow-subtle-layer-a: rgba(33, 38, 49, 0.04);
372
372
  --color-drop-shadow-subtle-layer-b: rgba(73, 85, 110, 0.08);
373
373
 
@@ -535,29 +535,29 @@
535
535
  /* === Base gold colors === */
536
536
  --color-gold-foreground: var(--color-gold-darken-4);
537
537
  --color-gold-foreground-emphasis: var(--color-gold-darken-5);
538
- --color-gold-foreground-onfill: var(--color-gold-darken-5);
539
- --color-gold-background-fill: var(--color-gold);
538
+ --color-gold-foreground-onfill: var(--color-gold-darken-6);
539
+ --color-gold-background-fill: var(--color-gold-lighten-1);
540
540
  --color-gold-background-fill-emphasis: var(--color-gold-darken-1);
541
- --color-gold-background-soft: var(--color-gold-24);
542
- --color-gold-background-soft-emphasis: var(--color-gold-16);
541
+ --color-gold-background-soft: var(--color-gold-16);
542
+ --color-gold-background-soft-emphasis: var(--color-gold-8);
543
543
 
544
544
  /* === Base silver colors === */
545
- --color-silver-foreground: var(--color-silver-darken-4);
545
+ --color-silver-foreground: var(--color-silver-darken-3);
546
546
  --color-silver-foreground-emphasis: var(--color-silver-darken-6);
547
547
  --color-silver-foreground-onfill: var(--color-white);
548
- --color-silver-background-fill: var(--color-silver);
549
- --color-silver-background-fill-emphasis: var(--color-silver-darken-1);
550
- --color-silver-background-soft: var(--color-silver-8);
551
- --color-silver-background-soft-emphasis: var(--color-silver-4);
548
+ --color-silver-background-fill: var(--color-silver-darken-1);
549
+ --color-silver-background-fill-emphasis: var(--color-silver-darken-3);
550
+ --color-silver-background-soft: var(--color-silver-16);
551
+ --color-silver-background-soft-emphasis: var(--color-silver-8);
552
552
 
553
553
  /* === Base bronze colors === */
554
- --color-bronze-foreground: var(--color-bronze-darken-3);
555
- --color-bronze-foreground-emphasis: var(--color-bronze-darken-4);
554
+ --color-bronze-foreground: var(--color-bronze-darken-2);
555
+ --color-bronze-foreground-emphasis: var(--color-bronze-darken-3);
556
556
  --color-bronze-foreground-onfill: var(--color-white);
557
557
  --color-bronze-background-fill: var(--color-bronze);
558
558
  --color-bronze-background-fill-emphasis: var(--color-bronze-darken-1);
559
- --color-bronze-background-soft: var(--color-bronze-24);
560
- --color-bronze-background-soft-emphasis: var(--color-bronze-16);
559
+ --color-bronze-background-soft: var(--color-bronze-16);
560
+ --color-bronze-background-soft-emphasis: var(--color-bronze-8);
561
561
 
562
562
  /* === Base primary colors === */
563
563
  --color-primary-foreground: var(--color-primary);
@@ -795,7 +795,7 @@
795
795
  --color-surface-elevated: var(--color-gray-800);
796
796
  --color-surface-card: var(--color-gray-850);
797
797
  --color-surface-navigation: var(--color-gray-850);
798
- --color-surface-section: var(--color-gray-900);
798
+ --color-surface-section: var(--color-gray-850);
799
799
  --color-surface-page: var(--color-gray-950);
800
800
  --color-surface-disable: var(--color-black-8);
801
801
  --color-surface-settings-on: var(--color-black-48);
@@ -903,6 +903,33 @@
903
903
  --color-prev-background-soft: var(--color-prev-24);
904
904
  --color-prev-background-soft-emphasis: var(--color-prev-48);
905
905
 
906
+ /* === Dark gold colors === */
907
+ --color-gold-foreground: var(--color-gold-lighten-1);
908
+ --color-gold-foreground-emphasis: var(--color-gold-lighten-2);
909
+ --color-gold-foreground-onfill: var(--color-gold-darken-6);
910
+ --color-gold-background-fill: var(--color-gold-darken-1);
911
+ --color-gold-background-fill-emphasis: var(--color-gold);
912
+ --color-gold-background-soft: var(--color-gold-8);
913
+ --color-gold-background-soft-emphasis: var(--color-gold-16);
914
+
915
+ /* === Dark silver colors === */
916
+ --color-silver-foreground: var(--color-silver-lighten-3);
917
+ --color-silver-foreground-emphasis: var(--color-silver-lighten-5);
918
+ --color-silver-foreground-onfill: var(--color-white);
919
+ --color-silver-background-fill: var(--color-silver-darken-1);
920
+ --color-silver-background-fill-emphasis: var(--color-silver);
921
+ --color-silver-background-soft: var(--color-silver-8);
922
+ --color-silver-background-soft-emphasis: var(--color-silver-16);
923
+
924
+ /* === Dark bronze colors === */
925
+ --color-bronze-foreground: var(--color-bronze-lighten-3);
926
+ --color-bronze-foreground-emphasis: var(--color-bronze-lighten-4);
927
+ --color-bronze-foreground-onfill: var(--color-bronze-darken-5);
928
+ --color-bronze-background-fill: var(--color-bronze-lighten-1);
929
+ --color-bronze-background-fill-emphasis: var(--color-bronze);
930
+ --color-bronze-background-soft: var(--color-bronze-8);
931
+ --color-bronze-background-soft-emphasis: var(--color-bronze-16);
932
+
906
933
  /* === Dark primary colors === */
907
934
  --color-primary-foreground: var(--color-gray-100);
908
935
  --color-primary-foreground-emphasis: var(--color-white);
@@ -1,5 +1,10 @@
1
1
  import { useEffect, type ReactNode } from "react";
2
- import { View, type LayoutChangeEvent, type ViewProps } from "react-native";
2
+ import {
3
+ View,
4
+ type LayoutChangeEvent,
5
+ type ViewProps,
6
+ type ViewStyle,
7
+ } from "react-native";
3
8
  import Animated, {
4
9
  Easing,
5
10
  cancelAnimation,
@@ -16,71 +21,180 @@ import { cn } from "@/lib/utils";
16
21
 
17
22
  type BadgeAnimation = "shine";
18
23
 
19
- interface BadgeProps extends ViewProps, VariantProps<typeof badgeVariants> {
24
+ type BadgeLayout = "tag" | "dot";
25
+
26
+ type BadgeBaseProps = ViewProps & VariantProps<typeof badgeVariants>;
27
+
28
+ interface BadgeTagProps extends BadgeBaseProps {
29
+ layout?: "tag";
20
30
  icon?: ReactNode;
21
31
  animation?: BadgeAnimation;
22
32
  children?: ReactNode;
23
33
  }
24
34
 
25
- const ICON_ONLY_SIZES: Record<"sm" | "md" | "lg" | "xl", string> = {
26
- sm: "h-4 w-4 p-0",
27
- md: "h-5 w-5 p-0",
28
- lg: "h-6 w-6 p-0",
29
- xl: "h-7 w-7 p-0",
30
- };
35
+ // A dot has no content, so the types flag what it would drop.
36
+ interface BadgeDotProps extends BadgeBaseProps {
37
+ layout: "dot";
38
+ icon?: never;
39
+ animation?: never;
40
+ children?: never;
41
+ }
31
42
 
32
- const CHAR_PADDING: Record<"sm" | "md" | "lg" | "xl", string> = {
33
- sm: "px-1",
34
- md: "px-1",
35
- lg: "px-1",
36
- xl: "px-1.5",
37
- };
43
+ type BadgeProps = BadgeTagProps | BadgeDotProps;
38
44
 
39
- const badgeVariants = cva(
40
- "flex-row items-center justify-center rounded-full px-2",
41
- {
42
- variants: {
43
- variant: {
44
- regular: "bg-secondary-background-fill",
45
- error: "bg-red-background-soft",
46
- warning: "bg-yellow-background-soft",
47
- freemium: "bg-primary-background-fill",
48
- },
49
- size: {
50
- sm: "min-h-4 min-w-4",
51
- md: "min-h-5 min-w-5",
52
- lg: "min-h-6 min-w-6",
53
- xl: "min-h-7 min-w-7",
54
- },
45
+ const badgeVariants = cva("flex-row items-center justify-center rounded-pill", {
46
+ variants: {
47
+ variant: {
48
+ primary: "bg-primary-background-fill",
49
+ "primary-soft": "bg-surface-card",
50
+ secondary: "bg-secondary-background-fill",
51
+ "secondary-soft": "bg-surface-card",
52
+ regular: "bg-secondary-background-fill",
53
+ cm: "bg-cm-background-fill",
54
+ "cm-soft": "bg-surface-card",
55
+ go: "bg-go-background-fill",
56
+ "go-soft": "bg-surface-card",
57
+ cir: "bg-cir-background-fill",
58
+ "cir-soft": "bg-surface-card",
59
+ // The design paints pediatrics with the warning tokens, all but the filled label.
60
+ ped: "bg-yellow-background-fill",
61
+ "ped-soft": "bg-surface-card",
62
+ prev: "bg-prev-background-fill",
63
+ "prev-soft": "bg-surface-card",
64
+ freemium: "bg-primary",
65
+ error: "bg-red-background-soft",
66
+ warning: "bg-yellow-background-soft",
67
+ neutral: "bg-gray-background-soft",
55
68
  },
56
- defaultVariants: {
57
- variant: "regular",
58
- size: "md",
69
+ size: {
70
+ sm: "min-h-4 min-w-4 gap-1 px-1 py-0.5",
71
+ md: "min-h-5 min-w-5 gap-1 px-1 py-0.5",
72
+ lg: "min-h-6 min-w-6 gap-1.5 px-1.5 py-1",
73
+ xl: "min-h-7 min-w-7 gap-2 px-2 py-1",
59
74
  },
60
- }
61
- );
75
+ },
76
+ compoundVariants: [
77
+ { variant: "freemium", size: "md", className: "px-1.5 py-1" },
78
+ { variant: "freemium", size: "lg", className: "px-2" },
79
+ { variant: "freemium", size: "xl", className: "px-2.5" },
80
+ ],
81
+ defaultVariants: {
82
+ variant: "secondary",
83
+ size: "md",
84
+ },
85
+ });
62
86
 
63
- const badgeTextVariants = cva("font-semibold", {
87
+ type BadgeVariant = NonNullable<VariantProps<typeof badgeVariants>["variant"]>;
88
+
89
+ // Laid over `surface-card`, as in the design, so a soft badge stays opaque on any surface.
90
+ const SOFT_TINT: Partial<Record<BadgeVariant, string>> = {
91
+ "primary-soft": "bg-primary-background-soft",
92
+ "secondary-soft": "bg-secondary-background-soft",
93
+ "cm-soft": "bg-cm-background-soft",
94
+ "go-soft": "bg-go-background-soft",
95
+ "cir-soft": "bg-cir-background-soft",
96
+ "ped-soft": "bg-yellow-background-soft",
97
+ "prev-soft": "bg-prev-background-soft",
98
+ };
99
+
100
+ const badgeTextVariants = cva("font-bold", {
64
101
  variants: {
65
102
  variant: {
103
+ primary: "text-primary-foreground-onfill",
104
+ "primary-soft": "text-primary-foreground",
105
+ secondary: "text-secondary-foreground-onfill",
106
+ "secondary-soft": "text-secondary-foreground",
66
107
  regular: "text-secondary-foreground-onfill",
108
+ cm: "text-cm-foreground-onfill",
109
+ "cm-soft": "text-cm-foreground",
110
+ go: "text-go-foreground-onfill",
111
+ "go-soft": "text-go-foreground",
112
+ cir: "text-cir-foreground-onfill",
113
+ "cir-soft": "text-cir-foreground",
114
+ ped: "text-ped-foreground-onfill",
115
+ "ped-soft": "text-yellow-foreground",
116
+ prev: "text-prev-foreground-onfill",
117
+ "prev-soft": "text-prev-foreground",
118
+ // White, not the design's onfill token: that one turns dark in dark mode, on a fill that stays navy.
119
+ freemium: "font-semibold text-white",
67
120
  error: "text-red-stroke-emphasis",
68
121
  warning: "text-yellow-foreground",
69
- freemium: "text-white",
122
+ neutral: "text-primary-foreground",
70
123
  },
71
124
  size: {
72
- sm: "text-xs",
73
- md: "text-xs",
74
- lg: "text-xs",
75
- xl: "text-sm",
125
+ sm: "text-xxs leading-xxs",
126
+ md: "text-xs leading-xs",
127
+ lg: "text-xs leading-xs",
128
+ xl: "text-sm leading-sm",
76
129
  },
77
130
  },
131
+ compoundVariants: [
132
+ { variant: "freemium", size: "md", className: "text-xxs leading-xxs" },
133
+ ],
78
134
  defaultVariants: {
79
- variant: "regular",
135
+ variant: "secondary",
80
136
  size: "md",
81
137
  },
82
138
  });
83
139
 
140
+ const badgeDotVariants = cva("rounded-circular", {
141
+ variants: {
142
+ variant: {
143
+ primary: "bg-primary-background-fill",
144
+ "primary-soft": "bg-primary-background-fill",
145
+ secondary: "bg-secondary-background-fill",
146
+ "secondary-soft": "bg-secondary-background-fill",
147
+ regular: "bg-secondary-background-fill",
148
+ cm: "bg-cm-background-fill",
149
+ "cm-soft": "bg-cm-background-fill",
150
+ go: "bg-go-background-fill",
151
+ "go-soft": "bg-go-background-fill",
152
+ cir: "bg-cir-background-fill",
153
+ "cir-soft": "bg-cir-background-fill",
154
+ ped: "bg-yellow-background-fill",
155
+ "ped-soft": "bg-yellow-background-fill",
156
+ prev: "bg-prev-background-fill",
157
+ "prev-soft": "bg-prev-background-fill",
158
+ freemium: "bg-primary",
159
+ error: "bg-red-background-fill",
160
+ warning: "bg-yellow-background-fill",
161
+ neutral: "bg-gray-background-fill",
162
+ },
163
+ size: {
164
+ sm: "size-1",
165
+ md: "size-2",
166
+ lg: "size-3",
167
+ xl: "size-4",
168
+ },
169
+ },
170
+ defaultVariants: {
171
+ variant: "secondary",
172
+ size: "md",
173
+ },
174
+ });
175
+
176
+ // The design's 10, 12, 16 and 20px as a minimum: a bigger icon grows the badge instead of
177
+ // spilling over its gap and padding.
178
+ const ICON_BOX: Record<"sm" | "md" | "lg" | "xl", string> = {
179
+ sm: "min-h-2.5 min-w-2.5",
180
+ md: "min-h-3 min-w-3",
181
+ lg: "min-h-4 min-w-4",
182
+ xl: "min-h-5 min-w-5",
183
+ };
184
+
185
+ const ICON_ONLY_PADDING: Record<"sm" | "md" | "lg" | "xl", string> = {
186
+ sm: "p-0.5",
187
+ md: "px-1 py-0.5",
188
+ lg: "p-1",
189
+ xl: "p-1",
190
+ };
191
+
192
+ // Corner to corner like the design's at any width, unlike the web's fixed 46deg.
193
+ const FREEMIUM_GRADIENT: ViewStyle = {
194
+ experimental_backgroundImage:
195
+ "radial-gradient(135.09% 111.8% at 0% 50%, rgba(24, 98, 188, 0.02) 0%, rgba(0, 32, 91, 0) 100%), linear-gradient(to top right, rgba(62, 218, 199, 0.56) 6.54%, rgba(24, 98, 188, 0.17) 85.21%)",
196
+ };
197
+
84
198
  const SHINE_DURATION_MS = 2000;
85
199
 
86
200
  function BadgeShine() {
@@ -94,7 +208,13 @@ function BadgeShine() {
94
208
  const bandStyle = useAnimatedStyle(() => ({
95
209
  width: width.value / 2,
96
210
  transform: [
97
- { translateX: interpolate(progress.value, [0, 1], [width.value, -width.value / 2]) },
211
+ {
212
+ translateX: interpolate(
213
+ progress.value,
214
+ [0, 1],
215
+ [width.value, -width.value / 2]
216
+ ),
217
+ },
98
218
  { skewX: "-20deg" },
99
219
  ],
100
220
  }));
@@ -111,10 +231,13 @@ function BadgeShine() {
111
231
  return (
112
232
  <View
113
233
  pointerEvents="none"
114
- className="absolute inset-0 overflow-hidden rounded-full"
234
+ className="absolute inset-0 overflow-hidden rounded-pill"
115
235
  onLayout={handleLayout}
116
236
  >
117
- <Animated.View className="absolute bottom-0 top-0 bg-white-24" style={bandStyle} />
237
+ <Animated.View
238
+ className="absolute bottom-0 top-0 bg-white-24"
239
+ style={bandStyle}
240
+ />
118
241
  </View>
119
242
  );
120
243
  }
@@ -125,39 +248,62 @@ function Badge({
125
248
  size,
126
249
  icon,
127
250
  animation,
251
+ layout = "tag",
252
+ style,
128
253
  children,
129
254
  ...props
130
255
  }: BadgeProps) {
131
- const childText =
132
- typeof children === "string" || typeof children === "number"
133
- ? String(children).trim()
134
- : "";
135
- const isSingleAlphaNum = /^[A-Za-z0-9]$/.test(childText);
136
- const isShortAlnumUpTo3 = /^[A-Za-z0-9]{2,3}$/.test(childText);
137
- const isDigitsWithSuffix = /^\d{1,2}\S$/.test(childText);
138
- const isIconOnly = !!icon && !children;
139
- const isShortUpTo3 = isShortAlnumUpTo3 || isDigitsWithSuffix;
256
+ if (layout === "dot") {
257
+ return (
258
+ <View
259
+ className={cn(badgeDotVariants({ variant, size }), className)}
260
+ style={style}
261
+ {...props}
262
+ />
263
+ );
264
+ }
265
+
266
+ const resolvedSize = size ?? "md";
267
+ const tint = SOFT_TINT[variant ?? "secondary"];
268
+ const hasContent =
269
+ children != null && typeof children !== "boolean" && children !== "";
270
+ const isIconOnly = !!icon && !hasContent;
140
271
 
141
272
  return (
142
273
  <View
143
274
  className={cn(
144
275
  badgeVariants({ variant, size }),
145
- (isIconOnly || isSingleAlphaNum) && ICON_ONLY_SIZES[size ?? "md"],
146
- !isIconOnly && isShortUpTo3 && CHAR_PADDING[size ?? "md"],
276
+ isIconOnly && ICON_ONLY_PADDING[resolvedSize],
147
277
  className
148
278
  )}
279
+ style={variant === "freemium" ? [FREEMIUM_GRADIENT, style] : style}
149
280
  {...props}
150
281
  >
282
+ {tint ? (
283
+ <View
284
+ pointerEvents="none"
285
+ className={cn("absolute inset-0 rounded-pill", tint)}
286
+ />
287
+ ) : null}
151
288
  {animation === "shine" ? <BadgeShine /> : null}
152
- {icon}
153
- {children != null && (
154
- <Text className={cn(badgeTextVariants({ variant, size }))}>
289
+ {icon ? (
290
+ <View
291
+ className={cn("items-center justify-center", ICON_BOX[resolvedSize])}
292
+ >
293
+ {icon}
294
+ </View>
295
+ ) : null}
296
+ {hasContent ? (
297
+ <Text
298
+ numberOfLines={1}
299
+ className={cn(badgeTextVariants({ variant, size }))}
300
+ >
155
301
  {children}
156
302
  </Text>
157
- )}
303
+ ) : null}
158
304
  </View>
159
305
  );
160
306
  }
161
307
 
162
308
  export { Badge, badgeVariants };
163
- export type { BadgeAnimation, BadgeProps };
309
+ export type { BadgeAnimation, BadgeLayout, BadgeProps };
@@ -1,10 +1,10 @@
1
1
  import type { ReactNode } from "react";
2
- import { Pressable, View } from "react-native";
2
+ import { Pressable, View, type Insets } from "react-native";
3
3
  import * as MedwayIcons from "@medway-ui/icons/native";
4
4
 
5
5
  import { Text } from "@/components/Text";
6
6
  import { ThemedIcon } from "@/components/ThemedIcon";
7
- import { useThemeColor } from "@/hooks/useThemeColor";
7
+ import { RaisedSurface } from "@/lib/raised-surface";
8
8
  import { cn } from "@/lib/utils";
9
9
 
10
10
  type ChoiceSize = "sm" | "md" | "lg";
@@ -33,13 +33,29 @@ interface ChoiceProps {
33
33
  className?: string;
34
34
  }
35
35
 
36
+ // The design's padding minus the 2px ring (`p-0.5`) around the inner card. The vertical hit slop
37
+ // takes the 32, 40 and 48pt chips to the package's 44pt touch target (`MIN_TOUCH_TARGET`).
36
38
  const SIZE_MAP: Record<
37
39
  ChoiceSize,
38
- { container: string; text: string; icon: number }
40
+ { container: string; text: string; icon: number; hitSlop?: Insets }
39
41
  > = {
40
- sm: { container: "gap-2 px-3 py-2", text: "text-xs", icon: 16 },
41
- md: { container: "gap-2.5 px-3.5 py-2.5", text: "text-sm", icon: 20 },
42
- lg: { container: "gap-3 px-4 py-3", text: "text-base", icon: 24 },
42
+ sm: {
43
+ container: "gap-2 px-2.5 py-1.5",
44
+ text: "text-xs leading-xs",
45
+ icon: 16,
46
+ hitSlop: { top: 6, bottom: 6 },
47
+ },
48
+ md: {
49
+ container: "gap-2.5 px-3 py-2",
50
+ text: "text-sm leading-sm",
51
+ icon: 20,
52
+ hitSlop: { top: 2, bottom: 2 },
53
+ },
54
+ lg: {
55
+ container: "gap-3 px-3.5 py-2.5",
56
+ text: "text-md leading-md",
57
+ icon: 24,
58
+ },
43
59
  };
44
60
 
45
61
  const THEME_SELECTED_OUTER_BG: Record<ChoiceTheme, string> = {
@@ -70,13 +86,6 @@ const THEME_CHECK_COLOR_TOKEN: Record<ChoiceTheme, string> = {
70
86
  prev: "prev-foreground",
71
87
  };
72
88
 
73
- const SHADOW_STYLE = {
74
- shadowOffset: { width: 0, height: 2 },
75
- shadowOpacity: 0.08,
76
- shadowRadius: 4,
77
- elevation: 2,
78
- };
79
-
80
89
  function Choice({
81
90
  label,
82
91
  selected = false,
@@ -88,49 +97,61 @@ function Choice({
88
97
  className,
89
98
  }: ChoiceProps) {
90
99
  const sz = SIZE_MAP[size] ?? SIZE_MAP.md;
91
- const shadowColor = useThemeColor("gray-700");
100
+ const isRaised = selected && !disabled;
92
101
 
93
102
  const outerBg = disabled
94
103
  ? "bg-gray-background-soft-muted"
95
104
  : selected
96
105
  ? THEME_SELECTED_OUTER_BG[theme]
97
- : "bg-gray-background-soft";
98
-
99
- const textClass = disabled
100
- ? "font-medium text-gray-foreground-muted"
101
- : selected
102
- ? "font-semibold text-primary-foreground"
103
- : "font-medium text-gray-foreground";
106
+ : "bg-gray-background-soft active:bg-gray-background-soft-emphasis";
104
107
 
105
108
  return (
106
109
  <Pressable
107
110
  onPress={disabled ? undefined : onPress}
108
111
  disabled={disabled}
109
- accessibilityRole="button"
110
- accessibilityState={{ selected, disabled }}
111
- className={cn("rounded-md", outerBg, className)}
112
- style={{ padding: 2 }}
112
+ hitSlop={sz.hitSlop}
113
+ accessibilityRole="togglebutton"
114
+ accessibilityLabel={label}
115
+ accessibilityState={{ checked: selected, disabled }}
116
+ className={cn("rounded-md p-0.5", outerBg, className)}
113
117
  >
114
- <View
115
- className={cn(
116
- "flex-row items-center justify-center rounded-md",
117
- sz.container,
118
- selected ? "bg-surface-card" : "bg-transparent",
119
- )}
120
- style={selected ? { ...SHADOW_STYLE, shadowColor } : undefined}
121
- >
122
- {icon ? <View className="shrink-0">{icon}</View> : null}
123
- <Text className={cn("text-center", sz.text, textClass)}>{label}</Text>
124
- {selected ? (
125
- <View className="shrink-0">
126
- <ThemedIcon
127
- icon={MedwayIcons.Check}
128
- size={sz.icon}
129
- colorToken={THEME_CHECK_COLOR_TOKEN[theme]}
130
- />
131
- </View>
132
- ) : null}
133
- </View>
118
+ {({ pressed }) => (
119
+ <View
120
+ className={cn("flex-row items-center justify-center", sz.container)}
121
+ >
122
+ {isRaised ? <RaisedSurface /> : null}
123
+ {icon ? <View className="shrink-0">{icon}</View> : null}
124
+ <Text
125
+ numberOfLines={1}
126
+ className={cn(
127
+ "shrink text-center",
128
+ sz.text,
129
+ disabled
130
+ ? "font-medium text-gray-foreground-muted"
131
+ : selected
132
+ ? "font-semibold text-primary-foreground"
133
+ : pressed
134
+ ? "font-medium text-gray-foreground-emphasis"
135
+ : "font-medium text-gray-foreground"
136
+ )}
137
+ >
138
+ {label}
139
+ </Text>
140
+ {selected ? (
141
+ <View className="shrink-0">
142
+ <ThemedIcon
143
+ icon={MedwayIcons.CheckMedium}
144
+ size={sz.icon}
145
+ colorToken={
146
+ disabled
147
+ ? "gray-foreground-muted"
148
+ : THEME_CHECK_COLOR_TOKEN[theme]
149
+ }
150
+ />
151
+ </View>
152
+ ) : null}
153
+ </View>
154
+ )}
134
155
  </Pressable>
135
156
  );
136
157
  }