@innovui/react-native 0.0.0-stage → 0.1.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 (204) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +57 -2
  3. package/dist/components/_shared/anchored-overlay.js +74 -0
  4. package/dist/components/_shared/anchored-overlay.js.map +1 -0
  5. package/dist/components/_shared/backdrop.js +25 -0
  6. package/dist/components/_shared/backdrop.js.map +1 -0
  7. package/dist/components/_shared/overlay-anchor.js +31 -0
  8. package/dist/components/_shared/overlay-anchor.js.map +1 -0
  9. package/dist/components/_shared/overlay-state.js +31 -0
  10. package/dist/components/_shared/overlay-state.js.map +1 -0
  11. package/dist/components/_shared/status.d.ts +5 -0
  12. package/dist/components/_shared/status.js +98 -0
  13. package/dist/components/_shared/status.js.map +1 -0
  14. package/dist/components/_shared/use-token.js +15 -0
  15. package/dist/components/_shared/use-token.js.map +1 -0
  16. package/dist/components/accordion/accordion.d.ts +43 -0
  17. package/dist/components/accordion/accordion.js +90 -0
  18. package/dist/components/accordion/accordion.js.map +1 -0
  19. package/dist/components/accordion/index.d.ts +2 -0
  20. package/dist/components/alert/alert.d.ts +25 -0
  21. package/dist/components/alert/alert.js +61 -0
  22. package/dist/components/alert/alert.js.map +1 -0
  23. package/dist/components/alert/index.d.ts +2 -0
  24. package/dist/components/avatar/avatar.d.ts +17 -0
  25. package/dist/components/avatar/avatar.js +51 -0
  26. package/dist/components/avatar/avatar.js.map +1 -0
  27. package/dist/components/avatar/index.d.ts +2 -0
  28. package/dist/components/badge/badge.d.ts +16 -0
  29. package/dist/components/badge/badge.js +57 -0
  30. package/dist/components/badge/badge.js.map +1 -0
  31. package/dist/components/badge/index.d.ts +2 -0
  32. package/dist/components/banner/banner.d.ts +23 -0
  33. package/dist/components/banner/banner.js +50 -0
  34. package/dist/components/banner/banner.js.map +1 -0
  35. package/dist/components/banner/index.d.ts +2 -0
  36. package/dist/components/breadcrumb/breadcrumb.d.ts +22 -0
  37. package/dist/components/breadcrumb/breadcrumb.js +54 -0
  38. package/dist/components/breadcrumb/breadcrumb.js.map +1 -0
  39. package/dist/components/breadcrumb/index.d.ts +2 -0
  40. package/dist/components/button/button.d.ts +21 -0
  41. package/dist/components/button/button.js +89 -0
  42. package/dist/components/button/button.js.map +1 -0
  43. package/dist/components/button/index.d.ts +2 -0
  44. package/dist/components/card/card.d.ts +17 -0
  45. package/dist/components/card/card.js +28 -0
  46. package/dist/components/card/card.js.map +1 -0
  47. package/dist/components/card/index.d.ts +2 -0
  48. package/dist/components/checkbox/checkbox.d.ts +18 -0
  49. package/dist/components/checkbox/checkbox.js +64 -0
  50. package/dist/components/checkbox/checkbox.js.map +1 -0
  51. package/dist/components/checkbox/index.d.ts +2 -0
  52. package/dist/components/combobox/combobox.d.ts +40 -0
  53. package/dist/components/combobox/combobox.js +101 -0
  54. package/dist/components/combobox/combobox.js.map +1 -0
  55. package/dist/components/combobox/index.d.ts +2 -0
  56. package/dist/components/container/container.d.ts +17 -0
  57. package/dist/components/container/container.js +42 -0
  58. package/dist/components/container/container.js.map +1 -0
  59. package/dist/components/container/index.d.ts +2 -0
  60. package/dist/components/date-picker/calendar.d.ts +38 -0
  61. package/dist/components/date-picker/calendar.js +137 -0
  62. package/dist/components/date-picker/calendar.js.map +1 -0
  63. package/dist/components/date-picker/date-picker.d.ts +24 -0
  64. package/dist/components/date-picker/date-picker.js +52 -0
  65. package/dist/components/date-picker/date-picker.js.map +1 -0
  66. package/dist/components/date-picker/date-presets.d.ts +15 -0
  67. package/dist/components/date-picker/date-presets.js +52 -0
  68. package/dist/components/date-picker/date-presets.js.map +1 -0
  69. package/dist/components/date-picker/date-range-picker.d.ts +24 -0
  70. package/dist/components/date-picker/date-range-picker.js +78 -0
  71. package/dist/components/date-picker/date-range-picker.js.map +1 -0
  72. package/dist/components/date-picker/date-sheet.js +33 -0
  73. package/dist/components/date-picker/date-sheet.js.map +1 -0
  74. package/dist/components/date-picker/date-trigger.js +14 -0
  75. package/dist/components/date-picker/date-trigger.js.map +1 -0
  76. package/dist/components/date-picker/dates.d.ts +8 -0
  77. package/dist/components/date-picker/dates.js +40 -0
  78. package/dist/components/date-picker/dates.js.map +1 -0
  79. package/dist/components/date-picker/index.d.ts +6 -0
  80. package/dist/components/divider/divider.d.ts +11 -0
  81. package/dist/components/divider/divider.js +16 -0
  82. package/dist/components/divider/divider.js.map +1 -0
  83. package/dist/components/divider/index.d.ts +2 -0
  84. package/dist/components/drawer/drawer.d.ts +34 -0
  85. package/dist/components/drawer/drawer.js +144 -0
  86. package/dist/components/drawer/drawer.js.map +1 -0
  87. package/dist/components/drawer/index.d.ts +2 -0
  88. package/dist/components/field/field.d.ts +31 -0
  89. package/dist/components/field/field.js +52 -0
  90. package/dist/components/field/field.js.map +1 -0
  91. package/dist/components/field/index.d.ts +2 -0
  92. package/dist/components/grid/grid.d.ts +35 -0
  93. package/dist/components/grid/grid.js +64 -0
  94. package/dist/components/grid/grid.js.map +1 -0
  95. package/dist/components/grid/index.d.ts +2 -0
  96. package/dist/components/icon-button/icon-button.d.ts +23 -0
  97. package/dist/components/icon-button/icon-button.js +48 -0
  98. package/dist/components/icon-button/icon-button.js.map +1 -0
  99. package/dist/components/icon-button/index.d.ts +2 -0
  100. package/dist/components/input/index.d.ts +2 -0
  101. package/dist/components/input/input.d.ts +22 -0
  102. package/dist/components/input/input.js +86 -0
  103. package/dist/components/input/input.js.map +1 -0
  104. package/dist/components/link/index.d.ts +2 -0
  105. package/dist/components/link/link.d.ts +13 -0
  106. package/dist/components/link/link.js +20 -0
  107. package/dist/components/link/link.js.map +1 -0
  108. package/dist/components/list/index.d.ts +2 -0
  109. package/dist/components/list/list.d.ts +26 -0
  110. package/dist/components/list/list.js +45 -0
  111. package/dist/components/list/list.js.map +1 -0
  112. package/dist/components/menu/index.d.ts +2 -0
  113. package/dist/components/menu/menu.d.ts +47 -0
  114. package/dist/components/menu/menu.js +81 -0
  115. package/dist/components/menu/menu.js.map +1 -0
  116. package/dist/components/modal/index.d.ts +2 -0
  117. package/dist/components/modal/modal.d.ts +41 -0
  118. package/dist/components/modal/modal.js +92 -0
  119. package/dist/components/modal/modal.js.map +1 -0
  120. package/dist/components/pagination/index.d.ts +2 -0
  121. package/dist/components/pagination/pagination.d.ts +21 -0
  122. package/dist/components/pagination/pagination.js +99 -0
  123. package/dist/components/pagination/pagination.js.map +1 -0
  124. package/dist/components/popover/index.d.ts +2 -0
  125. package/dist/components/popover/popover.d.ts +29 -0
  126. package/dist/components/popover/popover.js +54 -0
  127. package/dist/components/popover/popover.js.map +1 -0
  128. package/dist/components/radio/index.d.ts +2 -0
  129. package/dist/components/radio/radio.d.ts +16 -0
  130. package/dist/components/radio/radio.js +37 -0
  131. package/dist/components/radio/radio.js.map +1 -0
  132. package/dist/components/segmented-control/index.d.ts +2 -0
  133. package/dist/components/segmented-control/segmented-control.d.ts +23 -0
  134. package/dist/components/segmented-control/segmented-control.js +53 -0
  135. package/dist/components/segmented-control/segmented-control.js.map +1 -0
  136. package/dist/components/select/field-trigger.d.ts +6 -0
  137. package/dist/components/select/field-trigger.js +90 -0
  138. package/dist/components/select/field-trigger.js.map +1 -0
  139. package/dist/components/select/index.d.ts +2 -0
  140. package/dist/components/select/select.d.ts +35 -0
  141. package/dist/components/select/select.js +98 -0
  142. package/dist/components/select/select.js.map +1 -0
  143. package/dist/components/skeleton/index.d.ts +2 -0
  144. package/dist/components/skeleton/skeleton.d.ts +17 -0
  145. package/dist/components/skeleton/skeleton.js +53 -0
  146. package/dist/components/skeleton/skeleton.js.map +1 -0
  147. package/dist/components/slider/index.d.ts +2 -0
  148. package/dist/components/slider/slider.d.ts +26 -0
  149. package/dist/components/slider/slider.js +102 -0
  150. package/dist/components/slider/slider.js.map +1 -0
  151. package/dist/components/spinner/index.d.ts +2 -0
  152. package/dist/components/spinner/spinner.d.ts +13 -0
  153. package/dist/components/spinner/spinner.js +47 -0
  154. package/dist/components/spinner/spinner.js.map +1 -0
  155. package/dist/components/stack/index.d.ts +2 -0
  156. package/dist/components/stack/stack.d.ts +17 -0
  157. package/dist/components/stack/stack.js +60 -0
  158. package/dist/components/stack/stack.js.map +1 -0
  159. package/dist/components/stepper/index.d.ts +2 -0
  160. package/dist/components/stepper/stepper.d.ts +20 -0
  161. package/dist/components/stepper/stepper.js +61 -0
  162. package/dist/components/stepper/stepper.js.map +1 -0
  163. package/dist/components/switch/index.d.ts +2 -0
  164. package/dist/components/switch/switch.d.ts +15 -0
  165. package/dist/components/switch/switch.js +51 -0
  166. package/dist/components/switch/switch.js.map +1 -0
  167. package/dist/components/table/index.d.ts +2 -0
  168. package/dist/components/table/table.d.ts +42 -0
  169. package/dist/components/table/table.js +83 -0
  170. package/dist/components/table/table.js.map +1 -0
  171. package/dist/components/tabs/index.d.ts +2 -0
  172. package/dist/components/tabs/tabs.d.ts +35 -0
  173. package/dist/components/tabs/tabs.js +82 -0
  174. package/dist/components/tabs/tabs.js.map +1 -0
  175. package/dist/components/tag/index.d.ts +2 -0
  176. package/dist/components/tag/tag.d.ts +18 -0
  177. package/dist/components/tag/tag.js +37 -0
  178. package/dist/components/tag/tag.js.map +1 -0
  179. package/dist/components/textarea/index.d.ts +2 -0
  180. package/dist/components/textarea/textarea.d.ts +16 -0
  181. package/dist/components/textarea/textarea.js +46 -0
  182. package/dist/components/textarea/textarea.js.map +1 -0
  183. package/dist/components/toast/index.d.ts +2 -0
  184. package/dist/components/toast/toast.d.ts +41 -0
  185. package/dist/components/toast/toast.js +167 -0
  186. package/dist/components/toast/toast.js.map +1 -0
  187. package/dist/components/tooltip/index.d.ts +2 -0
  188. package/dist/components/tooltip/tooltip.d.ts +28 -0
  189. package/dist/components/tooltip/tooltip.js +62 -0
  190. package/dist/components/tooltip/tooltip.js.map +1 -0
  191. package/dist/components/upload/index.d.ts +2 -0
  192. package/dist/components/upload/upload.d.ts +65 -0
  193. package/dist/components/upload/upload.js +130 -0
  194. package/dist/components/upload/upload.js.map +1 -0
  195. package/dist/index.d.ts +87 -0
  196. package/dist/index.js +46 -0
  197. package/dist/tokens/generated.d.ts +371 -0
  198. package/dist/tokens/generated.js +373 -0
  199. package/dist/tokens/generated.js.map +1 -0
  200. package/dist/utils/cn.d.ts +6 -0
  201. package/dist/utils/cn.js +11 -0
  202. package/dist/utils/cn.js.map +1 -0
  203. package/package.json +61 -3
  204. package/theme.css +191 -0
@@ -0,0 +1,46 @@
1
+ import { cn } from "../../utils/cn.js";
2
+ import { useToken } from "../_shared/use-token.js";
3
+ import { TextInput } from "react-native";
4
+ import { jsx } from "react/jsx-runtime";
5
+ import { cva } from "class-variance-authority";
6
+ //#region src/components/textarea/textarea.tsx
7
+ const textareaVariants = cva("min-h-30 w-full rounded-md border border-field-border bg-field-bg text-field-text focus:border-field-focus-ring", {
8
+ variants: {
9
+ size: {
10
+ lg: "p-4 text-base",
11
+ md: "p-3 text-base",
12
+ sm: "p-2 text-sm"
13
+ },
14
+ error: {
15
+ true: "border-field-border-error focus:border-field-border-error",
16
+ false: ""
17
+ }
18
+ },
19
+ defaultVariants: {
20
+ size: "md",
21
+ error: false
22
+ }
23
+ });
24
+ function Textarea({ className, size, error = false, disabled = false, readOnly = false, accessibilityState, ...props }) {
25
+ const placeholderColor = useToken("--color-field-text-placeholder");
26
+ return /* @__PURE__ */ jsx(TextInput, {
27
+ multiline: true,
28
+ textAlignVertical: "top",
29
+ editable: !disabled && !readOnly,
30
+ readOnly,
31
+ placeholderTextColor: placeholderColor,
32
+ accessibilityState: {
33
+ ...accessibilityState,
34
+ disabled
35
+ },
36
+ className: cn(textareaVariants({
37
+ size,
38
+ error
39
+ }), disabled && "border-field-border-disabled bg-field-bg-disabled text-field-text-disabled focus:border-field-border-disabled", readOnly && !disabled && "border-transparent bg-field-bg-disabled focus:border-transparent", className),
40
+ ...props
41
+ });
42
+ }
43
+ //#endregion
44
+ export { Textarea };
45
+
46
+ //# sourceMappingURL=textarea.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"textarea.js","names":[],"sources":["../../../src/components/textarea/textarea.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { TextInput, type TextInputProps } from 'react-native'\nimport { cn } from '../../utils/cn'\nimport { useToken } from '../_shared/use-token'\n\nconst textareaVariants = cva(\n 'min-h-30 w-full rounded-md border border-field-border bg-field-bg text-field-text focus:border-field-focus-ring',\n {\n variants: {\n size: {\n lg: 'p-4 text-base',\n md: 'p-3 text-base',\n sm: 'p-2 text-sm',\n },\n error: {\n true: 'border-field-border-error focus:border-field-border-error',\n false: '',\n },\n },\n defaultVariants: { size: 'md', error: false },\n },\n)\n\nexport type TextareaProps = Omit<TextInputProps, 'style' | 'editable' | 'multiline'> &\n Omit<VariantProps<typeof textareaVariants>, 'error'> & {\n className?: string\n /** Borda e foco em vermelho. */\n error?: boolean\n disabled?: boolean\n }\n\nexport function Textarea({\n className,\n size,\n error = false,\n disabled = false,\n readOnly = false,\n accessibilityState,\n ...props\n}: TextareaProps) {\n const placeholderColor = useToken('--color-field-text-placeholder')\n\n return (\n <TextInput\n multiline\n textAlignVertical=\"top\"\n editable={!disabled && !readOnly}\n readOnly={readOnly}\n placeholderTextColor={placeholderColor}\n accessibilityState={{ ...accessibilityState, disabled }}\n className={cn(\n textareaVariants({ size, error }),\n disabled &&\n 'border-field-border-disabled bg-field-bg-disabled text-field-text-disabled focus:border-field-border-disabled',\n readOnly && !disabled && 'border-transparent bg-field-bg-disabled focus:border-transparent',\n className,\n )}\n {...props}\n />\n )\n}\n"],"mappings":";;;;;;AAOA,MAAM,mBAAmB,IACvB,mHACA;CACE,UAAU;EACR,MAAM;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EACN;EACA,OAAO;GACL,MAAM;GACN,OAAO;EACT;CACF;CACA,iBAAiB;EAAE,MAAM;EAAM,OAAO;CAAM;AAC9C,CACF;AAUA,SAAgB,SAAS,EACvB,WACA,MACA,QAAQ,OACR,WAAW,OACX,WAAW,OACX,oBACA,GAAG,SACa;CAChB,MAAM,mBAAmB,SAAS,gCAAgC;CAElE,OACE,oBAAC,WAAD;EACE,WAAA;EACA,mBAAkB;EAClB,UAAU,CAAC,YAAY,CAAC;EACd;EACV,sBAAsB;EACtB,oBAAoB;GAAE,GAAG;GAAoB;EAAS;EACtD,WAAW,GACT,iBAAiB;GAAE;GAAM;EAAM,CAAC,GAChC,YACE,iHACF,YAAY,CAAC,YAAY,oEACzB,SACF;EACA,GAAI;CACL,CAAA;AAEL"}
@@ -0,0 +1,2 @@
1
+ import { ToastOptions, ToastProvider, ToastProviderProps, useToast } from "./toast.js";
2
+ export { type ToastOptions, ToastProvider, type ToastProviderProps, useToast };
@@ -0,0 +1,41 @@
1
+ import { Status } from "../_shared/status.js";
2
+ import { ReactNode } from "react";
3
+ //#region src/components/toast/toast.d.ts
4
+ export type ToastOptions = {
5
+ status?: Status;
6
+ /** Resumo (opcional). */
7
+ title?: string;
8
+ description: string;
9
+ /** Ação contextual (ex.: "Desfazer"). Tocar nela também fecha o toast. */
10
+ action?: {
11
+ label: string;
12
+ onClick: () => void;
13
+ };
14
+ /** Tempo até fechar sozinho, em ms. `0` não fecha sozinho. Padrão: 5000. */
15
+ duration?: number;
16
+ /** Mostra o ícone do status. Padrão: `true`. */
17
+ showIcon?: boolean;
18
+ /** Mostra o botão de fechar (×). Sem ele, o toast ainda fecha pelo `duration`. Padrão: `true`. */
19
+ dismissible?: boolean;
20
+ };
21
+ type ToastContextValue = {
22
+ /** Exibe um toast e devolve o id, para fechá-lo com `dismiss`. */
23
+ toast: (options: ToastOptions) => number;
24
+ dismiss: (id: number) => void;
25
+ };
26
+ export type ToastProviderProps = {
27
+ children: ReactNode;
28
+ /** Nome acessível da região de notificações. */
29
+ label?: string;
30
+ /** Nome acessível do botão de fechar de cada toast. */
31
+ dismissLabel?: string;
32
+ testID?: string;
33
+ };
34
+ /**
35
+ * Envolva o app uma vez; qualquer componente abaixo dispara toasts com `useToast()`. A pilha fica
36
+ * no topo da tela, acima do conteúdo do app, mas abaixo de Modal/Drawer (que são janelas nativas).
37
+ */
38
+ export declare function ToastProvider({ children, label, dismissLabel, testID }: ToastProviderProps): import("react").JSX.Element;
39
+ export declare function useToast(): ToastContextValue;
40
+ //#endregion
41
+ //# sourceMappingURL=toast.d.ts.map
@@ -0,0 +1,167 @@
1
+ import { CloseIcon, StatusIcon, statusRole, statusText, statusTextToken } from "../_shared/status.js";
2
+ import { useToken } from "../_shared/use-token.js";
3
+ import { Button } from "../button/button.js";
4
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
5
+ import { Animated, Platform, Pressable, StatusBar, Text, View } from "react-native";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ //#region src/components/toast/toast.tsx
8
+ const ToastContext = createContext(null);
9
+ const topInset = Platform.OS === "ios" ? 56 : (StatusBar.currentHeight ?? 24) + 8;
10
+ /**
11
+ * Envolva o app uma vez; qualquer componente abaixo dispara toasts com `useToast()`. A pilha fica
12
+ * no topo da tela, acima do conteúdo do app, mas abaixo de Modal/Drawer (que são janelas nativas).
13
+ */
14
+ function ToastProvider({ children, label = "Notificações", dismissLabel = "Fechar", testID }) {
15
+ const [items, setItems] = useState([]);
16
+ const nextId = useRef(0);
17
+ const dismiss = useCallback((id) => {
18
+ setItems((current) => current.map((item) => item.id === id ? {
19
+ ...item,
20
+ closing: true
21
+ } : item));
22
+ }, []);
23
+ const remove = useCallback((id) => {
24
+ setItems((current) => current.filter((item) => item.id !== id));
25
+ }, []);
26
+ const toast = useCallback((options) => {
27
+ const id = nextId.current++;
28
+ setItems((current) => [...current, {
29
+ ...options,
30
+ id,
31
+ closing: false
32
+ }]);
33
+ return id;
34
+ }, []);
35
+ const value = useMemo(() => ({
36
+ toast,
37
+ dismiss
38
+ }), [toast, dismiss]);
39
+ return /* @__PURE__ */ jsxs(ToastContext.Provider, {
40
+ value,
41
+ children: [children, /* @__PURE__ */ jsx(View, {
42
+ pointerEvents: "box-none",
43
+ accessibilityLabel: label,
44
+ testID,
45
+ style: {
46
+ position: "absolute",
47
+ top: 0,
48
+ left: 0,
49
+ right: 0,
50
+ zIndex: 9999,
51
+ elevation: 9999
52
+ },
53
+ children: /* @__PURE__ */ jsx(View, {
54
+ pointerEvents: "box-none",
55
+ className: "w-full max-w-[360px] gap-2 self-center px-4",
56
+ style: { paddingTop: topInset },
57
+ children: items.map((item) => /* @__PURE__ */ jsx(ToastView, {
58
+ item,
59
+ dismissLabel,
60
+ onDismiss: dismiss,
61
+ onRemove: remove
62
+ }, item.id))
63
+ })
64
+ })]
65
+ });
66
+ }
67
+ function ToastView({ item, dismissLabel, onDismiss, onRemove }) {
68
+ const { id, status = "info", title, description, action, duration = 5e3, showIcon = true, dismissible = true, closing } = item;
69
+ const progress = useRef(new Animated.Value(0)).current;
70
+ const iconColor = useToken(statusTextToken[status]);
71
+ const closeColor = useToken("--color-text-primary");
72
+ useEffect(() => {
73
+ if (closing) return;
74
+ const animation = Animated.timing(progress, {
75
+ toValue: 1,
76
+ duration: 200,
77
+ useNativeDriver: true
78
+ });
79
+ animation.start();
80
+ return () => animation.stop();
81
+ }, [closing, progress]);
82
+ useEffect(() => {
83
+ if (!closing) return;
84
+ const animation = Animated.timing(progress, {
85
+ toValue: 0,
86
+ duration: 150,
87
+ useNativeDriver: true
88
+ });
89
+ animation.start(({ finished }) => finished && onRemove(id));
90
+ return () => animation.stop();
91
+ }, [
92
+ closing,
93
+ id,
94
+ onRemove,
95
+ progress
96
+ ]);
97
+ useEffect(() => {
98
+ if (closing || duration <= 0) return;
99
+ const timer = setTimeout(() => onDismiss(id), duration);
100
+ return () => clearTimeout(timer);
101
+ }, [
102
+ closing,
103
+ duration,
104
+ id,
105
+ onDismiss
106
+ ]);
107
+ const translateY = progress.interpolate({
108
+ inputRange: [0, 1],
109
+ outputRange: [-16, 0]
110
+ });
111
+ return /* @__PURE__ */ jsx(Animated.View, {
112
+ style: {
113
+ opacity: progress,
114
+ transform: [{ translateY }]
115
+ },
116
+ children: /* @__PURE__ */ jsxs(View, {
117
+ accessibilityRole: statusRole(status),
118
+ accessibilityLiveRegion: statusRole(status) ? "assertive" : "polite",
119
+ className: "flex-row items-start gap-3 rounded-md border border-border-default bg-surface-elevated py-3 pr-3 pl-4 shadow-md",
120
+ style: { elevation: 6 },
121
+ children: [
122
+ showIcon && /* @__PURE__ */ jsx(StatusIcon, {
123
+ status,
124
+ color: iconColor
125
+ }),
126
+ /* @__PURE__ */ jsxs(View, {
127
+ className: "min-w-0 flex-1 items-start gap-1",
128
+ children: [
129
+ title ? /* @__PURE__ */ jsx(Text, {
130
+ className: `text-sm font-medium leading-5 ${statusText[status]}`,
131
+ children: title
132
+ }) : null,
133
+ /* @__PURE__ */ jsx(Text, {
134
+ className: "text-sm leading-5 text-text-primary",
135
+ children: description
136
+ }),
137
+ action && /* @__PURE__ */ jsx(Button, {
138
+ variant: "ghost",
139
+ onPress: () => {
140
+ action.onClick();
141
+ onDismiss(id);
142
+ },
143
+ children: action.label
144
+ })
145
+ ]
146
+ }),
147
+ dismissible && /* @__PURE__ */ jsx(Pressable, {
148
+ accessibilityRole: "button",
149
+ accessibilityLabel: dismissLabel,
150
+ hitSlop: 8,
151
+ onPress: () => onDismiss(id),
152
+ className: "size-8 items-center justify-center rounded-full active:bg-button-secondary-bg-hover",
153
+ children: /* @__PURE__ */ jsx(CloseIcon, { color: closeColor })
154
+ })
155
+ ]
156
+ })
157
+ });
158
+ }
159
+ function useToast() {
160
+ const context = useContext(ToastContext);
161
+ if (!context) throw new Error("useToast precisa estar dentro de <ToastProvider>.");
162
+ return context;
163
+ }
164
+ //#endregion
165
+ export { ToastProvider, useToast };
166
+
167
+ //# sourceMappingURL=toast.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toast.js","names":[],"sources":["../../../src/components/toast/toast.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport {\n createContext,\n type ReactNode,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport { Animated, Platform, Pressable, StatusBar, Text, View } from 'react-native'\nimport {\n CloseIcon,\n type Status,\n StatusIcon,\n statusRole,\n statusText,\n statusTextToken,\n} from '../_shared/status'\nimport { useToken } from '../_shared/use-token'\nimport { Button } from '../button/button'\n\nexport type ToastOptions = {\n status?: Status\n /** Resumo (opcional). */\n title?: string\n description: string\n /** Ação contextual (ex.: \"Desfazer\"). Tocar nela também fecha o toast. */\n action?: { label: string; onClick: () => void }\n /** Tempo até fechar sozinho, em ms. `0` não fecha sozinho. Padrão: 5000. */\n duration?: number\n /** Mostra o ícone do status. Padrão: `true`. */\n showIcon?: boolean\n /** Mostra o botão de fechar (×). Sem ele, o toast ainda fecha pelo `duration`. Padrão: `true`. */\n dismissible?: boolean\n}\n\ntype ToastItem = ToastOptions & { id: number; closing: boolean }\n\ntype ToastContextValue = {\n /** Exibe um toast e devolve o id, para fechá-lo com `dismiss`. */\n toast: (options: ToastOptions) => number\n dismiss: (id: number) => void\n}\n\nconst ToastContext = createContext<ToastContextValue | null>(null)\n\nexport type ToastProviderProps = {\n children: ReactNode\n /** Nome acessível da região de notificações. */\n label?: string\n /** Nome acessível do botão de fechar de cada toast. */\n dismissLabel?: string\n testID?: string\n}\n\n// Sem react-native-safe-area-context: margem fixa para o notch no iOS e a barra de status no Android.\nconst topInset = Platform.OS === 'ios' ? 56 : (StatusBar.currentHeight ?? 24) + 8\n\n/**\n * Envolva o app uma vez; qualquer componente abaixo dispara toasts com `useToast()`. A pilha fica\n * no topo da tela, acima do conteúdo do app, mas abaixo de Modal/Drawer (que são janelas nativas).\n */\nexport function ToastProvider({\n children,\n label = 'Notificações',\n dismissLabel = 'Fechar',\n testID,\n}: ToastProviderProps) {\n const [items, setItems] = useState<ToastItem[]>([])\n const nextId = useRef(0)\n\n // Marca como \"closing\"; o próprio toast anima a saída e então chama `remove`.\n const dismiss = useCallback((id: number) => {\n setItems((current) =>\n current.map((item) => (item.id === id ? { ...item, closing: true } : item)),\n )\n }, [])\n const remove = useCallback((id: number) => {\n setItems((current) => current.filter((item) => item.id !== id))\n }, [])\n const toast = useCallback((options: ToastOptions) => {\n const id = nextId.current++\n setItems((current) => [...current, { ...options, id, closing: false }])\n return id\n }, [])\n const value = useMemo(() => ({ toast, dismiss }), [toast, dismiss])\n\n return (\n <ToastContext.Provider value={value}>\n {children}\n <View\n pointerEvents=\"box-none\"\n accessibilityLabel={label}\n testID={testID}\n style={{ position: 'absolute', top: 0, left: 0, right: 0, zIndex: 9999, elevation: 9999 }}\n >\n <View\n pointerEvents=\"box-none\"\n className=\"w-full max-w-[360px] gap-2 self-center px-4\"\n style={{ paddingTop: topInset }}\n >\n {items.map((item) => (\n <ToastView\n key={item.id}\n item={item}\n dismissLabel={dismissLabel}\n onDismiss={dismiss}\n onRemove={remove}\n />\n ))}\n </View>\n </View>\n </ToastContext.Provider>\n )\n}\n\nfunction ToastView({\n item,\n dismissLabel,\n onDismiss,\n onRemove,\n}: {\n item: ToastItem\n dismissLabel: string\n onDismiss: (id: number) => void\n onRemove: (id: number) => void\n}) {\n const {\n id,\n status = 'info',\n title,\n description,\n action,\n duration = 5000,\n showIcon = true,\n dismissible = true,\n closing,\n } = item\n const progress = useRef(new Animated.Value(0)).current\n const iconColor = useToken(statusTextToken[status])\n const closeColor = useToken('--color-text-primary')\n\n useEffect(() => {\n if (closing) return\n const animation = Animated.timing(progress, {\n toValue: 1,\n duration: 200,\n useNativeDriver: true,\n })\n animation.start()\n return () => animation.stop()\n }, [closing, progress])\n\n useEffect(() => {\n if (!closing) return\n const animation = Animated.timing(progress, {\n toValue: 0,\n duration: 150,\n useNativeDriver: true,\n })\n animation.start(({ finished }) => finished && onRemove(id))\n return () => animation.stop()\n }, [closing, id, onRemove, progress])\n\n useEffect(() => {\n if (closing || duration <= 0) return\n const timer = setTimeout(() => onDismiss(id), duration)\n return () => clearTimeout(timer)\n }, [closing, duration, id, onDismiss])\n\n const translateY = progress.interpolate({ inputRange: [0, 1], outputRange: [-16, 0] })\n\n return (\n <Animated.View style={{ opacity: progress, transform: [{ translateY }] }}>\n <View\n accessibilityRole={statusRole(status)}\n // Erro e aviso interrompem o leitor de tela; o resto é anunciado com calma.\n accessibilityLiveRegion={statusRole(status) ? 'assertive' : 'polite'}\n className=\"flex-row items-start gap-3 rounded-md border border-border-default bg-surface-elevated py-3 pr-3 pl-4 shadow-md\"\n style={{ elevation: 6 }}\n >\n {showIcon && <StatusIcon status={status} color={iconColor} />}\n <View className=\"min-w-0 flex-1 items-start gap-1\">\n {title ? (\n <Text className={`text-sm font-medium leading-5 ${statusText[status]}`}>{title}</Text>\n ) : null}\n <Text className=\"text-sm leading-5 text-text-primary\">{description}</Text>\n {action && (\n <Button\n variant=\"ghost\"\n onPress={() => {\n action.onClick()\n onDismiss(id)\n }}\n >\n {action.label}\n </Button>\n )}\n </View>\n {dismissible && (\n <Pressable\n accessibilityRole=\"button\"\n accessibilityLabel={dismissLabel}\n hitSlop={8}\n onPress={() => onDismiss(id)}\n className=\"size-8 items-center justify-center rounded-full active:bg-button-secondary-bg-hover\"\n >\n <CloseIcon color={closeColor} />\n </Pressable>\n )}\n </View>\n </Animated.View>\n )\n}\n\nexport function useToast(): ToastContextValue {\n const context = useContext(ToastContext)\n if (!context) throw new Error('useToast precisa estar dentro de <ToastProvider>.')\n return context\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { Tooltip, TooltipProps } from "./tooltip.js";
2
+ export { Tooltip, type TooltipProps };
@@ -0,0 +1,28 @@
1
+ import { ReactElement, ReactNode } from "react";
2
+ //#region src/components/tooltip/tooltip.d.ts
3
+ export type TooltipProps = {
4
+ open?: boolean;
5
+ defaultOpen?: boolean;
6
+ onOpenChange?: (open: boolean) => void;
7
+ /** O elemento que dispara o tooltip (ex.: um `<Button>`); recebe `onLongPress`. */
8
+ children: ReactElement;
9
+ /** O texto principal. */
10
+ content: ReactNode;
11
+ /** Título em destaque acima do texto (opcional). */
12
+ title?: ReactNode;
13
+ /** Lado do gatilho em que o tooltip aparece; inverte se não couber. Padrão: `top`. */
14
+ side?: 'top' | 'bottom';
15
+ /** Fecha sozinho depois deste tempo, em ms. `0` mantém aberto até tocar fora. Padrão: 3000. */
16
+ duration?: number;
17
+ className?: string;
18
+ testID?: string;
19
+ };
20
+ /**
21
+ * Dica contextual. No toque não há hover: abre ao pressionar e segurar o gatilho (`onLongPress`),
22
+ * e fecha após `duration` ou ao tocar fora. Como o long press não é óbvio para leitores de tela,
23
+ * o `content` (se for texto) vira o `accessibilityHint` do gatilho. Diferenças do web: sem
24
+ * `delayDuration`, sem seta (`showArrow`) e sem lados `left`/`right`.
25
+ */
26
+ export declare function Tooltip({ open, defaultOpen, onOpenChange, children, content, title, side, duration, className, testID }: TooltipProps): import("react").JSX.Element;
27
+ //#endregion
28
+ //# sourceMappingURL=tooltip.d.ts.map
@@ -0,0 +1,62 @@
1
+ import { cn } from "../../utils/cn.js";
2
+ import { AnchoredOverlay } from "../_shared/anchored-overlay.js";
3
+ import { useOpenState } from "../_shared/overlay-state.js";
4
+ import { AnchorTrigger, useControlledAnchor } from "../_shared/overlay-anchor.js";
5
+ import { useEffect } from "react";
6
+ import { Text, View } from "react-native";
7
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
8
+ //#region src/components/tooltip/tooltip.tsx
9
+ /**
10
+ * Dica contextual. No toque não há hover: abre ao pressionar e segurar o gatilho (`onLongPress`),
11
+ * e fecha após `duration` ou ao tocar fora. Como o long press não é óbvio para leitores de tela,
12
+ * o `content` (se for texto) vira o `accessibilityHint` do gatilho. Diferenças do web: sem
13
+ * `delayDuration`, sem seta (`showArrow`) e sem lados `left`/`right`.
14
+ */
15
+ function Tooltip({ open, defaultOpen, onOpenChange, children, content, title, side = "top", duration = 3e3, className, testID }) {
16
+ const [isOpen, setOpen] = useOpenState({
17
+ open,
18
+ defaultOpen,
19
+ onOpenChange
20
+ });
21
+ const { ref, rect } = useControlledAnchor(isOpen);
22
+ const close = () => setOpen(false);
23
+ useEffect(() => {
24
+ if (!isOpen || duration <= 0) return;
25
+ const timer = setTimeout(() => setOpen(false), duration);
26
+ return () => clearTimeout(timer);
27
+ }, [
28
+ isOpen,
29
+ duration,
30
+ setOpen
31
+ ]);
32
+ const hint = typeof content === "string" ? content : void 0;
33
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(AnchorTrigger, {
34
+ anchorRef: ref,
35
+ event: "onLongPress",
36
+ onTrigger: () => setOpen(true),
37
+ extra: hint ? { accessibilityHint: hint } : void 0,
38
+ children
39
+ }), /* @__PURE__ */ jsx(AnchoredOverlay, {
40
+ visible: isOpen,
41
+ rect,
42
+ onRequestClose: close,
43
+ placement: side,
44
+ estimatedHeight: 80,
45
+ testID,
46
+ className: cn("max-w-[240px] gap-1 rounded-sm bg-bg-inverse px-3 py-2", className),
47
+ children: /* @__PURE__ */ jsxs(View, {
48
+ accessibilityRole: "text",
49
+ children: [title && /* @__PURE__ */ jsx(Text, {
50
+ className: "text-[13px] font-medium leading-4 text-text-inverse",
51
+ children: title
52
+ }), /* @__PURE__ */ jsx(Text, {
53
+ className: "text-xs leading-4 text-text-inverse-secondary",
54
+ children: content
55
+ })]
56
+ })
57
+ })] });
58
+ }
59
+ //#endregion
60
+ export { Tooltip };
61
+
62
+ //# sourceMappingURL=tooltip.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tooltip.js","names":[],"sources":["../../../src/components/tooltip/tooltip.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { type ReactElement, type ReactNode, useEffect } from 'react'\nimport { Text, View } from 'react-native'\nimport { cn } from '../../utils/cn'\nimport { AnchoredOverlay } from '../_shared/anchored-overlay'\nimport { AnchorTrigger, useControlledAnchor } from '../_shared/overlay-anchor'\nimport { useOpenState } from '../_shared/overlay-state'\n\nexport type TooltipProps = {\n open?: boolean\n defaultOpen?: boolean\n onOpenChange?: (open: boolean) => void\n /** O elemento que dispara o tooltip (ex.: um `<Button>`); recebe `onLongPress`. */\n children: ReactElement\n /** O texto principal. */\n content: ReactNode\n /** Título em destaque acima do texto (opcional). */\n title?: ReactNode\n /** Lado do gatilho em que o tooltip aparece; inverte se não couber. Padrão: `top`. */\n side?: 'top' | 'bottom'\n /** Fecha sozinho depois deste tempo, em ms. `0` mantém aberto até tocar fora. Padrão: 3000. */\n duration?: number\n className?: string\n testID?: string\n}\n\n/**\n * Dica contextual. No toque não há hover: abre ao pressionar e segurar o gatilho (`onLongPress`),\n * e fecha após `duration` ou ao tocar fora. Como o long press não é óbvio para leitores de tela,\n * o `content` (se for texto) vira o `accessibilityHint` do gatilho. Diferenças do web: sem\n * `delayDuration`, sem seta (`showArrow`) e sem lados `left`/`right`.\n */\nexport function Tooltip({\n open,\n defaultOpen,\n onOpenChange,\n children,\n content,\n title,\n side = 'top',\n duration = 3000,\n className,\n testID,\n}: TooltipProps) {\n const [isOpen, setOpen] = useOpenState({ open, defaultOpen, onOpenChange })\n const { ref, rect } = useControlledAnchor(isOpen)\n const close = () => setOpen(false)\n\n useEffect(() => {\n if (!isOpen || duration <= 0) return\n const timer = setTimeout(() => setOpen(false), duration)\n return () => clearTimeout(timer)\n }, [isOpen, duration, setOpen])\n\n const hint = typeof content === 'string' ? content : undefined\n\n return (\n <>\n <AnchorTrigger\n anchorRef={ref}\n event=\"onLongPress\"\n onTrigger={() => setOpen(true)}\n extra={hint ? { accessibilityHint: hint } : undefined}\n >\n {children}\n </AnchorTrigger>\n <AnchoredOverlay\n visible={isOpen}\n rect={rect}\n onRequestClose={close}\n placement={side}\n estimatedHeight={80}\n testID={testID}\n className={cn('max-w-[240px] gap-1 rounded-sm bg-bg-inverse px-3 py-2', className)}\n >\n <View accessibilityRole=\"text\">\n {title && (\n <Text className=\"text-[13px] font-medium leading-4 text-text-inverse\">{title}</Text>\n )}\n <Text className=\"text-xs leading-4 text-text-inverse-secondary\">{content}</Text>\n </View>\n </AnchoredOverlay>\n </>\n )\n}\n"],"mappings":";;;;;;;;;;;;;;AAiCA,SAAgB,QAAQ,EACtB,MACA,aACA,cACA,UACA,SACA,OACA,OAAO,OACP,WAAW,KACX,WACA,UACe;CACf,MAAM,CAAC,QAAQ,WAAW,aAAa;EAAE;EAAM;EAAa;CAAa,CAAC;CAC1E,MAAM,EAAE,KAAK,SAAS,oBAAoB,MAAM;CAChD,MAAM,cAAc,QAAQ,KAAK;CAEjC,gBAAgB;EACd,IAAI,CAAC,UAAU,YAAY,GAAG;EAC9B,MAAM,QAAQ,iBAAiB,QAAQ,KAAK,GAAG,QAAQ;EACvD,aAAa,aAAa,KAAK;CACjC,GAAG;EAAC;EAAQ;EAAU;CAAO,CAAC;CAE9B,MAAM,OAAO,OAAO,YAAY,WAAW,UAAU,KAAA;CAErD,OACE,qBAAA,YAAA,EAAA,UAAA,CACE,oBAAC,eAAD;EACE,WAAW;EACX,OAAM;EACN,iBAAiB,QAAQ,IAAI;EAC7B,OAAO,OAAO,EAAE,mBAAmB,KAAK,IAAI,KAAA;EAE3C;CACY,CAAA,GACf,oBAAC,iBAAD;EACE,SAAS;EACH;EACN,gBAAgB;EAChB,WAAW;EACX,iBAAiB;EACT;EACR,WAAW,GAAG,0DAA0D,SAAS;EAEjF,UAAA,qBAAC,MAAD;GAAM,mBAAkB;GAAxB,UAAA,CACG,SACC,oBAAC,MAAD;IAAM,WAAU;IAAuD,UAAA;GAAY,CAAA,GAErF,oBAAC,MAAD;IAAM,WAAU;IAAiD,UAAA;GAAc,CAAA,CAC3E;;CACS,CAAA,CACjB,EAAA,CAAA;AAEN"}
@@ -0,0 +1,2 @@
1
+ import { Upload, UploadFile, UploadFileStatus, UploadProps } from "./upload.js";
2
+ export { Upload, type UploadFile, type UploadFileStatus, type UploadProps };
@@ -0,0 +1,65 @@
1
+ import { ReactNode } from "react";
2
+ //#region src/components/upload/upload.d.ts
3
+ export type UploadFileStatus = 'uploading' | 'done' | 'error';
4
+ export type UploadFile = {
5
+ /** Identificador estável do arquivo; volta em `onRemoveFile`. */
6
+ id: string;
7
+ name: string;
8
+ /** Tamanho em bytes; sem ele, nada é mostrado. */
9
+ size?: number;
10
+ /** `uploading` mostra a barra de progresso; `done` e `error` mostram o resultado. */
11
+ status?: UploadFileStatus;
12
+ /** Progresso de 0 a 100, só para `uploading`. Sem ele, a barra fica em 0. */
13
+ progress?: number;
14
+ /** Texto do erro, mostrado no lugar do tamanho quando `status="error"`. */
15
+ errorMessage?: string;
16
+ };
17
+ export type UploadProps = {
18
+ /**
19
+ * Chamado ao tocar na área. O componente não escolhe arquivos: abra o seletor que preferir e
20
+ * guarde o resultado no estado que alimenta `files`. Com expo-document-picker:
21
+ *
22
+ * import * as DocumentPicker from 'expo-document-picker'
23
+ *
24
+ * async function pick() {
25
+ * const result = await DocumentPicker.getDocumentAsync({ multiple: true })
26
+ * if (result.canceled) return
27
+ * setFiles((prev) => [
28
+ * ...prev,
29
+ * ...result.assets.map((a) => ({
30
+ * id: a.uri,
31
+ * name: a.name,
32
+ * size: a.size,
33
+ * status: 'uploading' as const,
34
+ * })),
35
+ * ])
36
+ * }
37
+ * <Upload onPick={pick} files={files} onRemoveFile={(id) => setFiles(...)} />
38
+ */
39
+ onPick: () => void;
40
+ /** Arquivos já escolhidos, listados abaixo da área com tamanho, status e botão de remover. */
41
+ files?: UploadFile[];
42
+ /** Chamado ao tocar no × de um arquivo. Sem ele, o botão não aparece. */
43
+ onRemoveFile?: (id: string) => void;
44
+ /** Texto principal da área. */
45
+ title?: ReactNode;
46
+ /** Texto de apoio (formatos e tamanho aceitos, por exemplo). */
47
+ description?: ReactNode;
48
+ /** Borda em vermelho, para quando os arquivos escolhidos têm problema. */
49
+ error?: boolean;
50
+ disabled?: boolean;
51
+ /** Prefixo do rótulo acessível do botão de remover ("Remover contrato.pdf"). */
52
+ removeLabel?: string;
53
+ className?: string;
54
+ accessibilityLabel?: string;
55
+ testID?: string;
56
+ };
57
+ /**
58
+ * Área para escolher arquivos tocando, mais a lista dos escolhidos. No native não há dropzone nem
59
+ * `<input type="file">`, e o seletor depende do app (expo-document-picker, image-picker...): por
60
+ * isso o componente é controlado e agnóstico. `onPick` abre o seu seletor; `files` e
61
+ * `onRemoveFile` descrevem a lista. Validar tipo e tamanho e enviar ao servidor ficam com você.
62
+ */
63
+ export declare function Upload({ onPick, files, onRemoveFile, title, description, error, disabled, removeLabel, className, accessibilityLabel, testID }: UploadProps): import("react").JSX.Element;
64
+ //#endregion
65
+ //# sourceMappingURL=upload.d.ts.map
@@ -0,0 +1,130 @@
1
+ import { cn } from "../../utils/cn.js";
2
+ import { CheckIcon, CloseIcon } from "../_shared/status.js";
3
+ import { useToken } from "../_shared/use-token.js";
4
+ import { Pressable, Text, View } from "react-native";
5
+ import Svg, { Path } from "react-native-svg";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ //#region src/components/upload/upload.tsx
8
+ /** "1,5 MB", "230 KB", "12 B". */
9
+ function formatSize(bytes) {
10
+ if (bytes < 1024) return `${bytes} B`;
11
+ const kb = bytes / 1024;
12
+ if (kb < 1024) return `${Math.round(kb)} KB`;
13
+ const mb = kb / 1024;
14
+ return `${(mb < 10 ? Math.round(mb * 10) / 10 : Math.round(mb)).toString().replace(".", ",")} MB`;
15
+ }
16
+ function UploadIcon({ color }) {
17
+ return /* @__PURE__ */ jsx(Svg, {
18
+ width: 24,
19
+ height: 24,
20
+ viewBox: "0 0 24 24",
21
+ fill: "none",
22
+ stroke: color,
23
+ strokeWidth: 1.5,
24
+ children: /* @__PURE__ */ jsx(Path, {
25
+ strokeLinecap: "round",
26
+ strokeLinejoin: "round",
27
+ d: "M12 16V4M7 9l5-5 5 5M4 16v3a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-3"
28
+ })
29
+ });
30
+ }
31
+ function FileRow({ file, onRemove, removeLabel }) {
32
+ const successColor = useToken("--color-status-success-text");
33
+ const iconColor = useToken("--color-text-secondary");
34
+ const progress = Math.min(100, Math.max(0, file.progress ?? 0));
35
+ const isError = file.status === "error";
36
+ const detail = isError && file.errorMessage ? file.errorMessage : void 0;
37
+ const size = file.size === void 0 ? void 0 : formatSize(file.size);
38
+ return /* @__PURE__ */ jsxs(View, {
39
+ className: cn("gap-2 rounded-md border bg-field-bg px-3 py-2", isError ? "border-field-border-error" : "border-field-border"),
40
+ children: [/* @__PURE__ */ jsxs(View, {
41
+ className: "flex-row items-center gap-2",
42
+ children: [
43
+ /* @__PURE__ */ jsxs(View, {
44
+ className: "flex-1",
45
+ children: [/* @__PURE__ */ jsx(Text, {
46
+ numberOfLines: 1,
47
+ className: "text-sm font-medium text-text-primary",
48
+ children: file.name
49
+ }), detail || size ? /* @__PURE__ */ jsx(Text, {
50
+ className: cn("text-xs", isError ? "text-status-error-text" : "text-text-secondary"),
51
+ children: detail ?? size
52
+ }) : null]
53
+ }),
54
+ file.status === "done" ? /* @__PURE__ */ jsx(View, {
55
+ accessible: true,
56
+ accessibilityLabel: "Concluído",
57
+ children: /* @__PURE__ */ jsx(CheckIcon, { color: successColor })
58
+ }) : null,
59
+ onRemove ? /* @__PURE__ */ jsx(Pressable, {
60
+ accessibilityRole: "button",
61
+ accessibilityLabel: `${removeLabel} ${file.name}`,
62
+ hitSlop: 8,
63
+ onPress: () => onRemove(file.id),
64
+ className: "size-8 items-center justify-center rounded-full active:bg-bg-tertiary",
65
+ children: /* @__PURE__ */ jsx(CloseIcon, { color: iconColor })
66
+ }) : null
67
+ ]
68
+ }), file.status === "uploading" ? /* @__PURE__ */ jsx(View, {
69
+ accessible: true,
70
+ accessibilityRole: "progressbar",
71
+ accessibilityLabel: file.name,
72
+ accessibilityValue: {
73
+ min: 0,
74
+ max: 100,
75
+ now: progress
76
+ },
77
+ className: "h-1 overflow-hidden rounded-full bg-bg-tertiary",
78
+ children: /* @__PURE__ */ jsx(View, {
79
+ className: "h-full rounded-full bg-interactive-primary",
80
+ style: { width: `${progress}%` }
81
+ })
82
+ }) : null]
83
+ });
84
+ }
85
+ /**
86
+ * Área para escolher arquivos tocando, mais a lista dos escolhidos. No native não há dropzone nem
87
+ * `<input type="file">`, e o seletor depende do app (expo-document-picker, image-picker...): por
88
+ * isso o componente é controlado e agnóstico. `onPick` abre o seu seletor; `files` e
89
+ * `onRemoveFile` descrevem a lista. Validar tipo e tamanho e enviar ao servidor ficam com você.
90
+ */
91
+ function Upload({ onPick, files, onRemoveFile, title = "Toque para escolher arquivos", description, error, disabled, removeLabel = "Remover", className, accessibilityLabel, testID }) {
92
+ const iconColor = useToken("--color-text-secondary");
93
+ return /* @__PURE__ */ jsxs(View, {
94
+ className: "gap-2",
95
+ children: [/* @__PURE__ */ jsxs(Pressable, {
96
+ accessibilityRole: "button",
97
+ accessibilityLabel: accessibilityLabel ?? (typeof title === "string" && typeof description === "string" ? `${title}. ${description}` : typeof title === "string" ? title : void 0),
98
+ accessibilityState: { disabled: Boolean(disabled) },
99
+ disabled,
100
+ onPress: onPick,
101
+ testID,
102
+ className: cn("items-center gap-1 rounded-md border border-dashed bg-field-bg px-6 py-8", error ? "border-field-border-error" : "border-field-border", "active:border-interactive-primary active:bg-button-secondary-bg", "disabled:border-field-border-disabled disabled:bg-field-bg-disabled", className),
103
+ children: [
104
+ /* @__PURE__ */ jsx(View, {
105
+ className: "mb-1",
106
+ children: /* @__PURE__ */ jsx(UploadIcon, { color: iconColor })
107
+ }),
108
+ /* @__PURE__ */ jsx(Text, {
109
+ className: cn("text-center text-sm font-medium text-text-primary", disabled && "text-field-text-disabled"),
110
+ children: title
111
+ }),
112
+ description ? /* @__PURE__ */ jsx(Text, {
113
+ className: cn("text-center text-xs text-text-secondary", disabled && "text-field-text-disabled"),
114
+ children: description
115
+ }) : null
116
+ ]
117
+ }), files && files.length > 0 ? /* @__PURE__ */ jsx(View, {
118
+ className: "gap-2",
119
+ children: files.map((file) => /* @__PURE__ */ jsx(FileRow, {
120
+ file,
121
+ onRemove: onRemoveFile,
122
+ removeLabel
123
+ }, file.id))
124
+ }) : null]
125
+ });
126
+ }
127
+ //#endregion
128
+ export { Upload };
129
+
130
+ //# sourceMappingURL=upload.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upload.js","names":[],"sources":["../../../src/components/upload/upload.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport type { ReactNode } from 'react'\nimport { Pressable, Text, View } from 'react-native'\nimport Svg, { Path } from 'react-native-svg'\nimport { cn } from '../../utils/cn'\nimport { CheckIcon, CloseIcon } from '../_shared/status'\nimport { useToken } from '../_shared/use-token'\n\nexport type UploadFileStatus = 'uploading' | 'done' | 'error'\n\nexport type UploadFile = {\n /** Identificador estável do arquivo; volta em `onRemoveFile`. */\n id: string\n name: string\n /** Tamanho em bytes; sem ele, nada é mostrado. */\n size?: number\n /** `uploading` mostra a barra de progresso; `done` e `error` mostram o resultado. */\n status?: UploadFileStatus\n /** Progresso de 0 a 100, só para `uploading`. Sem ele, a barra fica em 0. */\n progress?: number\n /** Texto do erro, mostrado no lugar do tamanho quando `status=\"error\"`. */\n errorMessage?: string\n}\n\nexport type UploadProps = {\n /**\n * Chamado ao tocar na área. O componente não escolhe arquivos: abra o seletor que preferir e\n * guarde o resultado no estado que alimenta `files`. Com expo-document-picker:\n *\n * import * as DocumentPicker from 'expo-document-picker'\n *\n * async function pick() {\n * const result = await DocumentPicker.getDocumentAsync({ multiple: true })\n * if (result.canceled) return\n * setFiles((prev) => [\n * ...prev,\n * ...result.assets.map((a) => ({\n * id: a.uri,\n * name: a.name,\n * size: a.size,\n * status: 'uploading' as const,\n * })),\n * ])\n * }\n * <Upload onPick={pick} files={files} onRemoveFile={(id) => setFiles(...)} />\n */\n onPick: () => void\n /** Arquivos já escolhidos, listados abaixo da área com tamanho, status e botão de remover. */\n files?: UploadFile[]\n /** Chamado ao tocar no × de um arquivo. Sem ele, o botão não aparece. */\n onRemoveFile?: (id: string) => void\n /** Texto principal da área. */\n title?: ReactNode\n /** Texto de apoio (formatos e tamanho aceitos, por exemplo). */\n description?: ReactNode\n /** Borda em vermelho, para quando os arquivos escolhidos têm problema. */\n error?: boolean\n disabled?: boolean\n /** Prefixo do rótulo acessível do botão de remover (\"Remover contrato.pdf\"). */\n removeLabel?: string\n className?: string\n accessibilityLabel?: string\n testID?: string\n}\n\n/** \"1,5 MB\", \"230 KB\", \"12 B\". */\nfunction formatSize(bytes: number) {\n if (bytes < 1024) return `${bytes} B`\n const kb = bytes / 1024\n if (kb < 1024) return `${Math.round(kb)} KB`\n const mb = kb / 1024\n return `${(mb < 10 ? Math.round(mb * 10) / 10 : Math.round(mb)).toString().replace('.', ',')} MB`\n}\n\nfunction UploadIcon({ color }: { color?: string }) {\n return (\n <Svg width={24} height={24} viewBox=\"0 0 24 24\" fill=\"none\" stroke={color} strokeWidth={1.5}>\n <Path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n d=\"M12 16V4M7 9l5-5 5 5M4 16v3a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-3\"\n />\n </Svg>\n )\n}\n\nfunction FileRow({\n file,\n onRemove,\n removeLabel,\n}: {\n file: UploadFile\n onRemove?: (id: string) => void\n removeLabel: string\n}) {\n const successColor = useToken('--color-status-success-text')\n const iconColor = useToken('--color-text-secondary')\n const progress = Math.min(100, Math.max(0, file.progress ?? 0))\n const isError = file.status === 'error'\n const detail = isError && file.errorMessage ? file.errorMessage : undefined\n const size = file.size === undefined ? undefined : formatSize(file.size)\n\n return (\n <View\n className={cn(\n 'gap-2 rounded-md border bg-field-bg px-3 py-2',\n isError ? 'border-field-border-error' : 'border-field-border',\n )}\n >\n <View className=\"flex-row items-center gap-2\">\n <View className=\"flex-1\">\n <Text numberOfLines={1} className=\"text-sm font-medium text-text-primary\">\n {file.name}\n </Text>\n {detail || size ? (\n <Text\n className={cn('text-xs', isError ? 'text-status-error-text' : 'text-text-secondary')}\n >\n {detail ?? size}\n </Text>\n ) : null}\n </View>\n {file.status === 'done' ? (\n <View accessible accessibilityLabel=\"Concluído\">\n <CheckIcon color={successColor} />\n </View>\n ) : null}\n {onRemove ? (\n <Pressable\n accessibilityRole=\"button\"\n accessibilityLabel={`${removeLabel} ${file.name}`}\n hitSlop={8}\n onPress={() => onRemove(file.id)}\n className=\"size-8 items-center justify-center rounded-full active:bg-bg-tertiary\"\n >\n <CloseIcon color={iconColor} />\n </Pressable>\n ) : null}\n </View>\n {file.status === 'uploading' ? (\n <View\n accessible\n accessibilityRole=\"progressbar\"\n accessibilityLabel={file.name}\n accessibilityValue={{ min: 0, max: 100, now: progress }}\n className=\"h-1 overflow-hidden rounded-full bg-bg-tertiary\"\n >\n <View\n className=\"h-full rounded-full bg-interactive-primary\"\n style={{ width: `${progress}%` }}\n />\n </View>\n ) : null}\n </View>\n )\n}\n\n/**\n * Área para escolher arquivos tocando, mais a lista dos escolhidos. No native não há dropzone nem\n * `<input type=\"file\">`, e o seletor depende do app (expo-document-picker, image-picker...): por\n * isso o componente é controlado e agnóstico. `onPick` abre o seu seletor; `files` e\n * `onRemoveFile` descrevem a lista. Validar tipo e tamanho e enviar ao servidor ficam com você.\n */\nexport function Upload({\n onPick,\n files,\n onRemoveFile,\n title = 'Toque para escolher arquivos',\n description,\n error,\n disabled,\n removeLabel = 'Remover',\n className,\n accessibilityLabel,\n testID,\n}: UploadProps) {\n const iconColor = useToken('--color-text-secondary')\n\n return (\n <View className=\"gap-2\">\n <Pressable\n accessibilityRole=\"button\"\n accessibilityLabel={\n accessibilityLabel ??\n (typeof title === 'string' && typeof description === 'string'\n ? `${title}. ${description}`\n : typeof title === 'string'\n ? title\n : undefined)\n }\n accessibilityState={{ disabled: Boolean(disabled) }}\n disabled={disabled}\n onPress={onPick}\n testID={testID}\n className={cn(\n 'items-center gap-1 rounded-md border border-dashed bg-field-bg px-6 py-8',\n error ? 'border-field-border-error' : 'border-field-border',\n 'active:border-interactive-primary active:bg-button-secondary-bg',\n 'disabled:border-field-border-disabled disabled:bg-field-bg-disabled',\n className,\n )}\n >\n <View className=\"mb-1\">\n <UploadIcon color={iconColor} />\n </View>\n <Text\n className={cn(\n 'text-center text-sm font-medium text-text-primary',\n disabled && 'text-field-text-disabled',\n )}\n >\n {title}\n </Text>\n {description ? (\n <Text\n className={cn(\n 'text-center text-xs text-text-secondary',\n disabled && 'text-field-text-disabled',\n )}\n >\n {description}\n </Text>\n ) : null}\n </Pressable>\n {files && files.length > 0 ? (\n <View className=\"gap-2\">\n {files.map((file) => (\n <FileRow key={file.id} file={file} onRemove={onRemoveFile} removeLabel={removeLabel} />\n ))}\n </View>\n ) : null}\n </View>\n )\n}\n"],"mappings":"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"}