@medway-ui/native 1.6.1 → 1.7.1

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 (224) hide show
  1. package/README.md +27 -2
  2. package/lib/commonjs/components/Alert.js +3 -1
  3. package/lib/commonjs/components/Alert.js.map +1 -1
  4. package/lib/commonjs/components/AlertDialog.js +5 -2
  5. package/lib/commonjs/components/AlertDialog.js.map +1 -1
  6. package/lib/commonjs/components/Badge.js +37 -2
  7. package/lib/commonjs/components/Badge.js.map +1 -1
  8. package/lib/commonjs/components/Button.js +32 -1
  9. package/lib/commonjs/components/Button.js.map +1 -1
  10. package/lib/commonjs/components/Carousel.js +12 -2
  11. package/lib/commonjs/components/Carousel.js.map +1 -1
  12. package/lib/commonjs/components/Checkbox.js +2 -0
  13. package/lib/commonjs/components/Checkbox.js.map +1 -1
  14. package/lib/commonjs/components/Chip.js +208 -0
  15. package/lib/commonjs/components/Chip.js.map +1 -0
  16. package/lib/commonjs/components/Choice.js +116 -0
  17. package/lib/commonjs/components/Choice.js.map +1 -0
  18. package/lib/commonjs/components/Dialog.js +27 -16
  19. package/lib/commonjs/components/Dialog.js.map +1 -1
  20. package/lib/commonjs/components/DropdownMenu.js +73 -57
  21. package/lib/commonjs/components/DropdownMenu.js.map +1 -1
  22. package/lib/commonjs/components/EmptyState.js +9 -2
  23. package/lib/commonjs/components/EmptyState.js.map +1 -1
  24. package/lib/commonjs/components/Feed.js +7 -3
  25. package/lib/commonjs/components/Feed.js.map +1 -1
  26. package/lib/commonjs/components/Feedback.js +77 -0
  27. package/lib/commonjs/components/Feedback.js.map +1 -0
  28. package/lib/commonjs/components/Filter.js +93 -0
  29. package/lib/commonjs/components/Filter.js.map +1 -0
  30. package/lib/commonjs/components/IndexLevel.js +4 -3
  31. package/lib/commonjs/components/IndexLevel.js.map +1 -1
  32. package/lib/commonjs/components/Input.js +149 -45
  33. package/lib/commonjs/components/Input.js.map +1 -1
  34. package/lib/commonjs/components/InvitePopover.js +4 -1
  35. package/lib/commonjs/components/InvitePopover.js.map +1 -1
  36. package/lib/commonjs/components/MemberRow.js +3 -4
  37. package/lib/commonjs/components/MemberRow.js.map +1 -1
  38. package/lib/commonjs/components/PostForm.js +18 -8
  39. package/lib/commonjs/components/PostForm.js.map +1 -1
  40. package/lib/commonjs/components/PrizeCollection.js +1 -1
  41. package/lib/commonjs/components/RadioGroup.js +7 -0
  42. package/lib/commonjs/components/RadioGroup.js.map +1 -1
  43. package/lib/commonjs/components/Select.js +1 -1
  44. package/lib/commonjs/components/Sheet.js +4 -1
  45. package/lib/commonjs/components/Sheet.js.map +1 -1
  46. package/lib/commonjs/components/Skeleton.js +55 -4
  47. package/lib/commonjs/components/Skeleton.js.map +1 -1
  48. package/lib/commonjs/components/Switch.js +2 -0
  49. package/lib/commonjs/components/Switch.js.map +1 -1
  50. package/lib/commonjs/components/Tabs.js +2 -0
  51. package/lib/commonjs/components/Tabs.js.map +1 -1
  52. package/lib/commonjs/components/Toast.js +355 -79
  53. package/lib/commonjs/components/Toast.js.map +1 -1
  54. package/lib/commonjs/editor/NoteEditor.js +41 -14
  55. package/lib/commonjs/editor/NoteEditor.js.map +1 -1
  56. package/lib/commonjs/editor/editorHtml.generated.js +1 -1
  57. package/lib/commonjs/editor/editorHtml.generated.js.map +1 -1
  58. package/lib/commonjs/hooks/useThemeColor.js +693 -65
  59. package/lib/commonjs/hooks/useThemeColor.js.map +1 -1
  60. package/lib/commonjs/hooks/useTrackedText.js +21 -0
  61. package/lib/commonjs/hooks/useTrackedText.js.map +1 -0
  62. package/lib/commonjs/index.js +46 -0
  63. package/lib/commonjs/index.js.map +1 -1
  64. package/lib/commonjs/lib/optional-deps.js +20 -0
  65. package/lib/commonjs/lib/optional-deps.js.map +1 -0
  66. package/lib/commonjs/lib/touch-target.js +19 -0
  67. package/lib/commonjs/lib/touch-target.js.map +1 -0
  68. package/lib/module/components/Alert.js +3 -1
  69. package/lib/module/components/Alert.js.map +1 -1
  70. package/lib/module/components/AlertDialog.js +5 -2
  71. package/lib/module/components/AlertDialog.js.map +1 -1
  72. package/lib/module/components/Badge.js +36 -2
  73. package/lib/module/components/Badge.js.map +1 -1
  74. package/lib/module/components/Button.js +32 -1
  75. package/lib/module/components/Button.js.map +1 -1
  76. package/lib/module/components/Carousel.js +12 -2
  77. package/lib/module/components/Carousel.js.map +1 -1
  78. package/lib/module/components/Checkbox.js +2 -0
  79. package/lib/module/components/Checkbox.js.map +1 -1
  80. package/lib/module/components/Chip.js +204 -0
  81. package/lib/module/components/Chip.js.map +1 -0
  82. package/lib/module/components/Choice.js +112 -0
  83. package/lib/module/components/Choice.js.map +1 -0
  84. package/lib/module/components/Dialog.js +28 -17
  85. package/lib/module/components/Dialog.js.map +1 -1
  86. package/lib/module/components/DropdownMenu.js +74 -58
  87. package/lib/module/components/DropdownMenu.js.map +1 -1
  88. package/lib/module/components/EmptyState.js +9 -2
  89. package/lib/module/components/EmptyState.js.map +1 -1
  90. package/lib/module/components/Feed.js +7 -3
  91. package/lib/module/components/Feed.js.map +1 -1
  92. package/lib/module/components/Feedback.js +74 -0
  93. package/lib/module/components/Feedback.js.map +1 -0
  94. package/lib/module/components/Filter.js +89 -0
  95. package/lib/module/components/Filter.js.map +1 -0
  96. package/lib/module/components/IndexLevel.js +4 -3
  97. package/lib/module/components/IndexLevel.js.map +1 -1
  98. package/lib/module/components/Input.js +149 -45
  99. package/lib/module/components/Input.js.map +1 -1
  100. package/lib/module/components/InvitePopover.js +4 -1
  101. package/lib/module/components/InvitePopover.js.map +1 -1
  102. package/lib/module/components/MemberRow.js +3 -4
  103. package/lib/module/components/MemberRow.js.map +1 -1
  104. package/lib/module/components/PostForm.js +18 -8
  105. package/lib/module/components/PostForm.js.map +1 -1
  106. package/lib/module/components/PrizeCollection.js +1 -1
  107. package/lib/module/components/RadioGroup.js +7 -0
  108. package/lib/module/components/RadioGroup.js.map +1 -1
  109. package/lib/module/components/Select.js +1 -1
  110. package/lib/module/components/Sheet.js +4 -1
  111. package/lib/module/components/Sheet.js.map +1 -1
  112. package/lib/module/components/Skeleton.js +56 -5
  113. package/lib/module/components/Skeleton.js.map +1 -1
  114. package/lib/module/components/Switch.js +2 -0
  115. package/lib/module/components/Switch.js.map +1 -1
  116. package/lib/module/components/Tabs.js +2 -0
  117. package/lib/module/components/Tabs.js.map +1 -1
  118. package/lib/module/components/Toast.js +360 -84
  119. package/lib/module/components/Toast.js.map +1 -1
  120. package/lib/module/editor/NoteEditor.js +41 -14
  121. package/lib/module/editor/NoteEditor.js.map +1 -1
  122. package/lib/module/editor/editorHtml.generated.js +1 -1
  123. package/lib/module/editor/editorHtml.generated.js.map +1 -1
  124. package/lib/module/hooks/useThemeColor.js +693 -65
  125. package/lib/module/hooks/useThemeColor.js.map +1 -1
  126. package/lib/module/hooks/useTrackedText.js +18 -0
  127. package/lib/module/hooks/useTrackedText.js.map +1 -0
  128. package/lib/module/index.js +8 -0
  129. package/lib/module/index.js.map +1 -1
  130. package/lib/module/lib/optional-deps.js +16 -0
  131. package/lib/module/lib/optional-deps.js.map +1 -0
  132. package/lib/module/lib/touch-target.js +15 -0
  133. package/lib/module/lib/touch-target.js.map +1 -0
  134. package/lib/typescript/components/Alert.d.ts.map +1 -1
  135. package/lib/typescript/components/AlertDialog.d.ts.map +1 -1
  136. package/lib/typescript/components/Badge.d.ts +1 -1
  137. package/lib/typescript/components/Badge.d.ts.map +1 -1
  138. package/lib/typescript/components/Button.d.ts.map +1 -1
  139. package/lib/typescript/components/Carousel.d.ts +1 -1
  140. package/lib/typescript/components/Carousel.d.ts.map +1 -1
  141. package/lib/typescript/components/Checkbox.d.ts.map +1 -1
  142. package/lib/typescript/components/Chip.d.ts +30 -0
  143. package/lib/typescript/components/Chip.d.ts.map +1 -0
  144. package/lib/typescript/components/Choice.d.ts +17 -0
  145. package/lib/typescript/components/Choice.d.ts.map +1 -0
  146. package/lib/typescript/components/Dialog.d.ts.map +1 -1
  147. package/lib/typescript/components/DropdownMenu.d.ts +0 -7
  148. package/lib/typescript/components/DropdownMenu.d.ts.map +1 -1
  149. package/lib/typescript/components/EmptyState.d.ts +3 -2
  150. package/lib/typescript/components/EmptyState.d.ts.map +1 -1
  151. package/lib/typescript/components/Feed.d.ts.map +1 -1
  152. package/lib/typescript/components/Feedback.d.ts +17 -0
  153. package/lib/typescript/components/Feedback.d.ts.map +1 -0
  154. package/lib/typescript/components/Filter.d.ts +17 -0
  155. package/lib/typescript/components/Filter.d.ts.map +1 -0
  156. package/lib/typescript/components/IndexLevel.d.ts.map +1 -1
  157. package/lib/typescript/components/Input.d.ts +3 -3
  158. package/lib/typescript/components/Input.d.ts.map +1 -1
  159. package/lib/typescript/components/InvitePopover.d.ts.map +1 -1
  160. package/lib/typescript/components/MemberRow.d.ts.map +1 -1
  161. package/lib/typescript/components/PostForm.d.ts.map +1 -1
  162. package/lib/typescript/components/RadioGroup.d.ts.map +1 -1
  163. package/lib/typescript/components/Sheet.d.ts.map +1 -1
  164. package/lib/typescript/components/Skeleton.d.ts +1 -1
  165. package/lib/typescript/components/Skeleton.d.ts.map +1 -1
  166. package/lib/typescript/components/Switch.d.ts.map +1 -1
  167. package/lib/typescript/components/Tabs.d.ts.map +1 -1
  168. package/lib/typescript/components/Tag.d.ts +2 -2
  169. package/lib/typescript/components/ThemedIcon.d.ts +1 -1
  170. package/lib/typescript/components/ThemedIcon.d.ts.map +1 -1
  171. package/lib/typescript/components/Toast.d.ts +24 -13
  172. package/lib/typescript/components/Toast.d.ts.map +1 -1
  173. package/lib/typescript/editor/NoteEditor.d.ts.map +1 -1
  174. package/lib/typescript/editor/editorHtml.generated.d.ts.map +1 -1
  175. package/lib/typescript/hooks/useThemeColor.d.ts +0 -10
  176. package/lib/typescript/hooks/useThemeColor.d.ts.map +1 -1
  177. package/lib/typescript/hooks/useTrackedText.d.ts +6 -0
  178. package/lib/typescript/hooks/useTrackedText.d.ts.map +1 -0
  179. package/lib/typescript/index.d.ts +9 -1
  180. package/lib/typescript/index.d.ts.map +1 -1
  181. package/lib/typescript/lib/optional-deps.d.ts +23 -0
  182. package/lib/typescript/lib/optional-deps.d.ts.map +1 -0
  183. package/lib/typescript/lib/touch-target.d.ts +5 -0
  184. package/lib/typescript/lib/touch-target.d.ts.map +1 -0
  185. package/package.json +6 -1
  186. package/setup/createMedwayUiNavigator.tsx +4 -7
  187. package/setup/metro.js +12 -3
  188. package/setup/tokens.native.css +268 -258
  189. package/src/components/Alert.tsx +3 -1
  190. package/src/components/AlertDialog.tsx +8 -3
  191. package/src/components/Badge.tsx +51 -3
  192. package/src/components/Button.tsx +39 -1
  193. package/src/components/Carousel.tsx +17 -3
  194. package/src/components/Checkbox.tsx +2 -0
  195. package/src/components/Chip.tsx +227 -0
  196. package/src/components/Choice.tsx +139 -0
  197. package/src/components/Dialog.tsx +39 -23
  198. package/src/components/DropdownMenu.tsx +99 -87
  199. package/src/components/EmptyState.tsx +18 -3
  200. package/src/components/Feed.tsx +4 -3
  201. package/src/components/Feedback.tsx +111 -0
  202. package/src/components/Filter.tsx +106 -0
  203. package/src/components/IndexLevel.tsx +8 -10
  204. package/src/components/Input.tsx +142 -54
  205. package/src/components/InvitePopover.tsx +5 -1
  206. package/src/components/MemberRow.tsx +3 -5
  207. package/src/components/PostForm.tsx +16 -9
  208. package/src/components/PrizeCollection.tsx +1 -1
  209. package/src/components/RadioGroup.tsx +8 -0
  210. package/src/components/Select.tsx +1 -1
  211. package/src/components/Sheet.tsx +4 -1
  212. package/src/components/Skeleton.tsx +47 -6
  213. package/src/components/Switch.tsx +2 -0
  214. package/src/components/Tabs.tsx +2 -0
  215. package/src/components/ThemedIcon.tsx +1 -1
  216. package/src/components/Toast.tsx +489 -87
  217. package/src/editor/NoteEditor.tsx +50 -11
  218. package/src/editor/editorHtml.generated.ts +1 -1
  219. package/src/hooks/useThemeColor.ts +757 -64
  220. package/src/hooks/useTrackedText.ts +20 -0
  221. package/src/index.ts +36 -1
  222. package/src/lib/optional-deps.ts +27 -0
  223. package/src/lib/touch-target.ts +12 -0
  224. package/setup/index.tsx +0 -42
@@ -5,6 +5,7 @@ import {
5
5
  useEffect,
6
6
  useId,
7
7
  useMemo,
8
+ useState,
8
9
  type ReactNode,
9
10
  } from "react";
10
11
  import { Dimensions, Pressable, StyleSheet, View, type PressableProps, type ViewProps } from "react-native";
@@ -12,7 +13,8 @@ import { Dimensions, Pressable, StyleSheet, View, type PressableProps, type View
12
13
  import { Text } from "@/components/Text";
13
14
  import * as DropdownMenuPrimitive from "@rn-primitives/dropdown-menu";
14
15
 
15
- import { useOverlayHost } from "@/lib/overlay-host";
16
+ import { useThemeColor } from "@/hooks/useThemeColor";
17
+ import { useOverlayHost, type HostOrigin } from "@/lib/overlay-host";
16
18
  import { cn } from "@/lib/utils";
17
19
 
18
20
  interface DropdownMenuProps extends DropdownMenuPrimitive.RootProps {
@@ -43,6 +45,14 @@ type MenuActionsContextValue = {
43
45
  close: () => void;
44
46
  };
45
47
 
48
+ type MenuAlign = NonNullable<DropdownMenuContentProps["align"]>;
49
+
50
+ type MenuPosition = { top: number; left: number };
51
+
52
+ type TriggerPosition = NonNullable<
53
+ ReturnType<typeof DropdownMenuPrimitive.useRootContext>["triggerPosition"]
54
+ >;
55
+
46
56
  const MenuActionsContext = createContext<MenuActionsContextValue | null>(null);
47
57
 
48
58
  function useMenuActions() {
@@ -90,6 +100,7 @@ const DropdownMenuTrigger = forwardRef<
90
100
  ref={ref}
91
101
  collapsable={false}
92
102
  accessibilityRole="button"
103
+ hitSlop={6}
93
104
  className={cn("items-center justify-center", className)}
94
105
  style={(state) => [
95
106
  {
@@ -107,13 +118,36 @@ const DropdownMenuTrigger = forwardRef<
107
118
  );
108
119
  });
109
120
 
110
- /**
111
- * Content renderizado no OverlayHost do MedwayUiProvider (acima da stack).
112
- * Mantém Root/Trigger do @rn-primitives/dropdown-menu para open + measure.
113
- *
114
- * `triggerPosition` vem em coordenadas de janela (pageX/pageY). O OverlayHost
115
- * fica abaixo do header da navegação, então subtraímos a origem do host.
116
- */
121
+ const MENU_WIDTH = 200;
122
+ const MENU_EDGE_GAP = 8;
123
+
124
+ // triggerPosition vem em coordenadas de janela; o OverlayHost fica abaixo do header, então subtrai a origem.
125
+ function computeMenuPosition({
126
+ triggerPosition,
127
+ hostOrigin,
128
+ align,
129
+ sideOffset,
130
+ }: {
131
+ triggerPosition: TriggerPosition;
132
+ hostOrigin: HostOrigin;
133
+ align: MenuAlign;
134
+ sideOffset: number;
135
+ }): MenuPosition {
136
+ const windowWidth = Dimensions.get("window").width;
137
+ const alignOffset: Record<MenuAlign, number> = {
138
+ start: 0,
139
+ center: triggerPosition.width / 2 - MENU_WIDTH / 2,
140
+ end: triggerPosition.width - MENU_WIDTH,
141
+ };
142
+ const rawLeft = triggerPosition.pageX + alignOffset[align] - hostOrigin.x;
143
+ const maxLeft = windowWidth - hostOrigin.x - MENU_WIDTH - MENU_EDGE_GAP;
144
+
145
+ return {
146
+ left: Math.max(MENU_EDGE_GAP, Math.min(rawLeft, maxLeft)),
147
+ top: triggerPosition.pageY + triggerPosition.height + sideOffset - hostOrigin.y,
148
+ };
149
+ }
150
+
117
151
  function DropdownMenuContent({
118
152
  className,
119
153
  align = "end",
@@ -125,103 +159,83 @@ function DropdownMenuContent({
125
159
  const overlay = useOverlayHost();
126
160
  const { close } = useMenuActions();
127
161
  const { open, triggerPosition } = DropdownMenuPrimitive.useRootContext();
162
+ const [menuPosition, setMenuPosition] = useState<MenuPosition | null>(null);
163
+ const surfaceColor = useThemeColor("surface-elevated");
164
+ const borderColor = useThemeColor("gray-stroke-muted");
165
+ const backdropColor = useThemeColor("black-48");
128
166
 
129
167
  useEffect(() => {
130
168
  if (!open || !triggerPosition) {
131
- overlay.unmount(id);
169
+ setMenuPosition(null);
132
170
  return;
133
171
  }
134
172
 
135
173
  let cancelled = false;
136
- const menuWidth = 200;
137
- const windowWidth = Dimensions.get("window").width;
138
174
 
139
175
  overlay.measureHostOrigin((hostOrigin) => {
140
- if (cancelled) {
141
- return;
142
- }
143
-
144
- let left =
145
- triggerPosition.pageX - hostOrigin.x;
146
-
147
- if (align === "end") {
148
- left =
149
- triggerPosition.pageX +
150
- triggerPosition.width -
151
- menuWidth -
152
- hostOrigin.x;
153
- } else if (align === "center") {
154
- left =
155
- triggerPosition.pageX +
156
- triggerPosition.width / 2 -
157
- menuWidth / 2 -
158
- hostOrigin.x;
159
- }
160
-
161
- left = Math.max(8, Math.min(left, windowWidth - hostOrigin.x - menuWidth - 8));
162
-
163
- const top =
164
- triggerPosition.pageY +
165
- triggerPosition.height +
166
- sideOffset -
167
- hostOrigin.y;
168
-
169
- overlay.mount(
170
- id,
171
- <MenuActionsContext.Provider value={{ close }}>
172
- <View style={styles.layer} pointerEvents="box-none">
173
- <Pressable
174
- accessibilityLabel="Fechar menu"
175
- style={[
176
- StyleSheet.absoluteFill,
177
- showOverlay ? styles.backdrop : null,
178
- ]}
179
- onPress={close}
180
- />
181
- <View
182
- className={cn(
183
- "overflow-hidden rounded-md border border-gray-stroke-muted bg-surface-elevated p-1 shadow-md",
184
- className
185
- )}
186
- // Fallback visual caso NativeWind não aplique no host de overlay
187
- style={[
188
- styles.menu,
189
- {
190
- top,
191
- left,
192
- width: menuWidth,
193
- backgroundColor: "white",
194
- borderRadius: 8,
195
- borderWidth: 1,
196
- borderColor: "#E7E9EF",
197
- padding: 4,
198
- },
199
- ]}
200
- >
201
- {children}
202
- </View>
203
- </View>
204
- </MenuActionsContext.Provider>
205
- );
176
+ if (cancelled) return;
177
+ setMenuPosition(computeMenuPosition({ triggerPosition, hostOrigin, align, sideOffset }));
206
178
  });
207
179
 
208
180
  return () => {
209
181
  cancelled = true;
210
- overlay.unmount(id);
211
182
  };
183
+ }, [align, open, overlay, sideOffset, triggerPosition]);
184
+
185
+ useEffect(() => {
186
+ if (!menuPosition) {
187
+ overlay.unmount(id);
188
+ return;
189
+ }
190
+
191
+ overlay.mount(
192
+ id,
193
+ <MenuActionsContext.Provider value={{ close }}>
194
+ <View style={styles.layer} pointerEvents="box-none">
195
+ <Pressable
196
+ accessibilityLabel="Fechar menu"
197
+ style={[
198
+ StyleSheet.absoluteFill,
199
+ showOverlay ? { backgroundColor: backdropColor } : null,
200
+ ]}
201
+ onPress={close}
202
+ />
203
+ <View
204
+ className={cn(
205
+ "overflow-hidden rounded-md border border-gray-stroke-muted bg-surface-elevated p-1 shadow-md",
206
+ className
207
+ )}
208
+ style={[
209
+ styles.menu,
210
+ {
211
+ top: menuPosition.top,
212
+ left: menuPosition.left,
213
+ width: MENU_WIDTH,
214
+ backgroundColor: surfaceColor,
215
+ borderColor,
216
+ },
217
+ ]}
218
+ >
219
+ {children}
220
+ </View>
221
+ </View>
222
+ </MenuActionsContext.Provider>
223
+ );
212
224
  }, [
213
- align,
225
+ backdropColor,
226
+ borderColor,
214
227
  children,
215
228
  className,
216
229
  close,
217
230
  id,
218
- open,
231
+ menuPosition,
219
232
  overlay,
220
233
  showOverlay,
221
- sideOffset,
222
- triggerPosition,
234
+ surfaceColor,
223
235
  ]);
224
236
 
237
+ useEffect(() => () => overlay.unmount(id), [id, overlay]);
238
+
225
239
  return null;
226
240
  }
227
241
 
@@ -284,15 +298,13 @@ const styles = StyleSheet.create({
284
298
  layer: {
285
299
  ...StyleSheet.absoluteFill,
286
300
  },
287
- backdrop: {
288
- // Alinhado ao Dialog/Sheet (`bg-black/50`); style explícito porque
289
- // NativeWind pode não aplicar classes montadas no OverlayHost.
290
- backgroundColor: "rgba(0, 0, 0, 0.5)",
291
- },
292
301
  menu: {
293
302
  position: "absolute",
294
303
  zIndex: 2,
295
304
  elevation: 8,
305
+ borderRadius: 8,
306
+ borderWidth: 1,
307
+ padding: 4,
296
308
  },
297
309
  });
298
310
 
@@ -9,16 +9,26 @@ interface EmptyStateProps extends ViewProps {
9
9
  title: string;
10
10
  description?: string;
11
11
  action?: ReactNode;
12
- variant?: "regular" | "emphasis" | "success" | "warning" | "error";
12
+ variant?: EmptyStateVariant;
13
13
  }
14
14
 
15
+ type EmptyStateVariant = "regular" | "emphasis" | "success" | "warning" | "error";
16
+
17
+ const ICON_BACKGROUND_BY_VARIANT: Record<EmptyStateVariant, string> = {
18
+ regular: "bg-gray-background-soft",
19
+ emphasis: "bg-secondary-background-soft",
20
+ success: "bg-green-background-soft",
21
+ warning: "bg-yellow-background-soft",
22
+ error: "bg-red-background-soft",
23
+ };
24
+
15
25
  function EmptyState({
16
26
  className,
17
27
  icon,
18
28
  title,
19
29
  description,
20
30
  action,
21
- variant: _variant = "regular",
31
+ variant = "regular",
22
32
  ...props
23
33
  }: EmptyStateProps) {
24
34
  return (
@@ -30,7 +40,12 @@ function EmptyState({
30
40
  {...props}
31
41
  >
32
42
  {icon ? (
33
- <View className="size-16 items-center justify-center rounded-full bg-gray-background-soft">
43
+ <View
44
+ className={cn(
45
+ "size-16 items-center justify-center rounded-full",
46
+ ICON_BACKGROUND_BY_VARIANT[variant]
47
+ )}
48
+ >
34
49
  {icon}
35
50
  </View>
36
51
  ) : null}
@@ -4,6 +4,7 @@ import { TextInput, View, type TextInputProps, type ViewProps } from "react-nati
4
4
  import { Text } from "@/components/Text";
5
5
 
6
6
  import { InputText, inputVariants } from "@/components/Input";
7
+ import { useTrackedText } from "@/hooks/useTrackedText";
7
8
  import { cn } from "@/lib/utils";
8
9
 
9
10
  interface FeedProps extends ViewProps {
@@ -53,7 +54,7 @@ function FeedComposer({
53
54
  editable = true,
54
55
  ...props
55
56
  }: FeedComposerProps) {
56
- const currentLength = (value ?? defaultValue ?? "").length;
57
+ const { text, handleChangeText } = useTrackedText(value, defaultValue, onChangeText);
57
58
 
58
59
  return (
59
60
  <View className={cn("gap-1", className)}>
@@ -67,7 +68,7 @@ function FeedComposer({
67
68
  maxLength={maxLength}
68
69
  value={value}
69
70
  defaultValue={defaultValue}
70
- onChangeText={onChangeText}
71
+ onChangeText={handleChangeText}
71
72
  placeholder={placeholder}
72
73
  editable={editable}
73
74
  className={cn(
@@ -86,7 +87,7 @@ function FeedComposer({
86
87
  />
87
88
  </View>
88
89
  <InputText className="text-right text-xs font-medium text-gray-foreground">
89
- {currentLength} / {maxLength}
90
+ {text.length} / {maxLength}
90
91
  </InputText>
91
92
  </View>
92
93
  );
@@ -0,0 +1,111 @@
1
+ import { cloneElement, isValidElement, type ReactNode } from "react";
2
+ import { View } from "react-native";
3
+
4
+ import { Text } from "@/components/Text";
5
+ import { cn } from "@/lib/utils";
6
+
7
+ type FeedbackType = "success" | "error" | "warning";
8
+ type FeedbackVariant = "filled" | "soft" | "basic";
9
+ type FeedbackSize = "sm" | "md";
10
+
11
+ interface FeedbackProps {
12
+ text: string;
13
+ type?: FeedbackType;
14
+ variant?: FeedbackVariant;
15
+ size?: FeedbackSize;
16
+ icon?: ReactNode;
17
+ showIcon?: boolean;
18
+ className?: string;
19
+ }
20
+
21
+ const SIZE_MAP: Record<
22
+ FeedbackSize,
23
+ { gap: string; padding: string; text: string; iconSize: number }
24
+ > = {
25
+ sm: {
26
+ gap: "gap-1.5",
27
+ padding: "px-2 py-1",
28
+ text: "text-xs leading-xs",
29
+ iconSize: 16,
30
+ },
31
+ md: {
32
+ gap: "gap-2",
33
+ padding: "px-2.5 py-1.5",
34
+ text: "text-sm leading-sm",
35
+ iconSize: 20,
36
+ },
37
+ };
38
+
39
+ const FILLED_BG: Record<FeedbackType, string> = {
40
+ success: "bg-feedback-success-background-fill",
41
+ error: "bg-feedback-error-background-fill",
42
+ warning: "bg-feedback-warning-background-fill",
43
+ };
44
+
45
+ const SOFT_BG: Record<FeedbackType, string> = {
46
+ success: "bg-feedback-success-background-soft",
47
+ error: "bg-feedback-error-background-soft",
48
+ warning: "bg-feedback-warning-background-soft",
49
+ };
50
+
51
+ const FILLED_TEXT_COLOR: Record<FeedbackType, string> = {
52
+ success: "text-feedback-success-foreground-onfill",
53
+ error: "text-feedback-error-foreground-onfill",
54
+ warning: "text-feedback-warning-foreground-onfill",
55
+ };
56
+
57
+ const DEFAULT_TEXT_COLOR: Record<FeedbackType, string> = {
58
+ success: "text-feedback-success-foreground",
59
+ error: "text-feedback-error-foreground",
60
+ warning: "text-feedback-warning-foreground",
61
+ };
62
+
63
+ function Feedback({
64
+ text,
65
+ type = "success",
66
+ variant = "filled",
67
+ size = "md",
68
+ icon,
69
+ showIcon = true,
70
+ className,
71
+ }: FeedbackProps) {
72
+ const sz = SIZE_MAP[size];
73
+
74
+ const isBasic = variant === "basic";
75
+ const bg = variant === "filled" ? FILLED_BG[type] : SOFT_BG[type];
76
+ const textColor =
77
+ variant === "filled" ? FILLED_TEXT_COLOR[type] : DEFAULT_TEXT_COLOR[type];
78
+
79
+ const shouldRenderIcon = showIcon && !!icon;
80
+
81
+ // Aplica o tamanho do size ao ícone, sem sobrescrever width/height explícitos.
82
+ const sizedIcon = isValidElement<{ width?: number; height?: number }>(icon)
83
+ ? cloneElement(icon, {
84
+ width: icon.props.width ?? sz.iconSize,
85
+ height: icon.props.height ?? sz.iconSize,
86
+ })
87
+ : icon;
88
+
89
+ return (
90
+ <View
91
+ className={cn(
92
+ "flex-row items-center justify-center",
93
+ shouldRenderIcon && sz.gap,
94
+ !isBasic && sz.padding,
95
+ !isBasic && "rounded-md",
96
+ !isBasic && bg,
97
+ className,
98
+ )}
99
+ >
100
+ {shouldRenderIcon ? <View className="shrink-0">{sizedIcon}</View> : null}
101
+ <Text
102
+ className={cn("text-center font-semibold", sz.text, textColor)}
103
+ >
104
+ {text}
105
+ </Text>
106
+ </View>
107
+ );
108
+ }
109
+
110
+ export { Feedback };
111
+ export type { FeedbackProps, FeedbackSize, FeedbackType, FeedbackVariant };
@@ -0,0 +1,106 @@
1
+ import type { ReactNode } from "react";
2
+ import { Pressable, View } from "react-native";
3
+ import * as MedwayIcons from "@medway-ui/icons/native";
4
+
5
+ import { Text } from "@/components/Text";
6
+ import { ThemedIcon } from "@/components/ThemedIcon";
7
+ import { useThemeColor } from "@/hooks/useThemeColor";
8
+ import { cn } from "@/lib/utils";
9
+
10
+ type FilterSize = "sm" | "md" | "lg";
11
+
12
+ interface FilterProps {
13
+ label: string;
14
+ criteria?: string;
15
+ showCriteria?: boolean;
16
+ selected?: boolean;
17
+ disabled?: boolean;
18
+ size?: FilterSize;
19
+ icon?: ReactNode;
20
+ onPress?: () => void;
21
+ className?: string;
22
+ }
23
+
24
+ const SIZE_MAP: Record<
25
+ FilterSize,
26
+ { container: string; text: string; icon: number }
27
+ > = {
28
+ sm: { container: "gap-2 px-3 py-2", text: "text-xs", icon: 16 },
29
+ md: { container: "gap-2.5 px-3.5 py-2.5", text: "text-sm", icon: 20 },
30
+ lg: { container: "gap-3 px-4 py-3", text: "text-base", icon: 24 },
31
+ };
32
+
33
+ const SHADOW_STYLE = {
34
+ shadowOffset: { width: 0, height: 2 },
35
+ shadowOpacity: 0.08,
36
+ shadowRadius: 4,
37
+ elevation: 2,
38
+ };
39
+
40
+ function Filter({
41
+ label,
42
+ criteria,
43
+ showCriteria = true,
44
+ selected = false,
45
+ disabled = false,
46
+ size = "md",
47
+ icon,
48
+ onPress,
49
+ className,
50
+ }: FilterProps) {
51
+ const sz = SIZE_MAP[size] ?? SIZE_MAP.md;
52
+ const shadowColor = useThemeColor("gray-700");
53
+
54
+ const outerBg = disabled
55
+ ? "bg-gray-background-soft-muted"
56
+ : "bg-gray-background-soft";
57
+
58
+ const textClass = disabled
59
+ ? "font-medium text-gray-foreground-muted"
60
+ : selected
61
+ ? "font-semibold text-primary-foreground"
62
+ : "font-medium text-gray-foreground";
63
+
64
+ const dropdownColorToken = disabled
65
+ ? "gray-foreground-muted"
66
+ : selected
67
+ ? "primary-foreground"
68
+ : "gray-foreground";
69
+
70
+ const hasCriteria = showCriteria && !!criteria;
71
+
72
+ return (
73
+ <Pressable
74
+ onPress={disabled ? undefined : onPress}
75
+ disabled={disabled}
76
+ accessibilityRole="button"
77
+ accessibilityState={{ selected, disabled }}
78
+ className={cn("rounded-md", outerBg, className)}
79
+ style={{ padding: 2 }}
80
+ >
81
+ <View
82
+ className={cn(
83
+ "flex-row items-center justify-center rounded-md",
84
+ sz.container,
85
+ selected ? "bg-surface-card" : "bg-transparent",
86
+ )}
87
+ style={selected ? { ...SHADOW_STYLE, shadowColor } : undefined}
88
+ >
89
+ {icon ? <View className="shrink-0">{icon}</View> : null}
90
+ <Text className={cn("text-center", sz.text, textClass)}>
91
+ {hasCriteria ? `${label}: ${criteria}` : label}
92
+ </Text>
93
+ <View className="shrink-0">
94
+ <ThemedIcon
95
+ icon={MedwayIcons.ChevronDown as never}
96
+ size={sz.icon}
97
+ colorToken={dropdownColorToken}
98
+ />
99
+ </View>
100
+ </View>
101
+ </Pressable>
102
+ );
103
+ }
104
+
105
+ export { Filter };
106
+ export type { FilterProps };
@@ -24,6 +24,10 @@ import {
24
24
  withAlpha,
25
25
  type ThemeColorToken,
26
26
  } from "@/hooks/useThemeColor";
27
+ import {
28
+ loadLinearGradient,
29
+ type LinearGradientComponent,
30
+ } from "@/lib/optional-deps";
27
31
  import { cn } from "@/lib/utils";
28
32
 
29
33
  /**
@@ -195,13 +199,7 @@ type RingDeps = {
195
199
  maskElement: ReactNode;
196
200
  children?: ReactNode;
197
201
  }>;
198
- LinearGradient: ComponentType<{
199
- colors: string[];
200
- locations?: number[];
201
- start?: { x: number; y: number };
202
- end?: { x: number; y: number };
203
- style?: object;
204
- }>;
202
+ LinearGradient: LinearGradientComponent;
205
203
  };
206
204
 
207
205
  /** Lazy-load optional peers so the static chip works without them installed. */
@@ -209,11 +207,11 @@ function loadRingDeps(): RingDeps | null {
209
207
  try {
210
208
  // eslint-disable-next-line @typescript-eslint/no-require-imports
211
209
  const masked = require("@react-native-masked-view/masked-view");
212
- // eslint-disable-next-line @typescript-eslint/no-require-imports
213
- const gradient = require("expo-linear-gradient");
210
+ const LinearGradient = loadLinearGradient();
211
+ if (!LinearGradient) return null;
214
212
  return {
215
213
  MaskedView: masked.default ?? masked,
216
- LinearGradient: gradient.LinearGradient,
214
+ LinearGradient,
217
215
  };
218
216
  } catch {
219
217
  return null;