@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.
- package/LICENSE +21 -0
- package/README.md +57 -2
- package/dist/components/_shared/anchored-overlay.js +74 -0
- package/dist/components/_shared/anchored-overlay.js.map +1 -0
- package/dist/components/_shared/backdrop.js +25 -0
- package/dist/components/_shared/backdrop.js.map +1 -0
- package/dist/components/_shared/overlay-anchor.js +31 -0
- package/dist/components/_shared/overlay-anchor.js.map +1 -0
- package/dist/components/_shared/overlay-state.js +31 -0
- package/dist/components/_shared/overlay-state.js.map +1 -0
- package/dist/components/_shared/status.d.ts +5 -0
- package/dist/components/_shared/status.js +98 -0
- package/dist/components/_shared/status.js.map +1 -0
- package/dist/components/_shared/use-token.js +15 -0
- package/dist/components/_shared/use-token.js.map +1 -0
- package/dist/components/accordion/accordion.d.ts +43 -0
- package/dist/components/accordion/accordion.js +90 -0
- package/dist/components/accordion/accordion.js.map +1 -0
- package/dist/components/accordion/index.d.ts +2 -0
- package/dist/components/alert/alert.d.ts +25 -0
- package/dist/components/alert/alert.js +61 -0
- package/dist/components/alert/alert.js.map +1 -0
- package/dist/components/alert/index.d.ts +2 -0
- package/dist/components/avatar/avatar.d.ts +17 -0
- package/dist/components/avatar/avatar.js +51 -0
- package/dist/components/avatar/avatar.js.map +1 -0
- package/dist/components/avatar/index.d.ts +2 -0
- package/dist/components/badge/badge.d.ts +16 -0
- package/dist/components/badge/badge.js +57 -0
- package/dist/components/badge/badge.js.map +1 -0
- package/dist/components/badge/index.d.ts +2 -0
- package/dist/components/banner/banner.d.ts +23 -0
- package/dist/components/banner/banner.js +50 -0
- package/dist/components/banner/banner.js.map +1 -0
- package/dist/components/banner/index.d.ts +2 -0
- package/dist/components/breadcrumb/breadcrumb.d.ts +22 -0
- package/dist/components/breadcrumb/breadcrumb.js +54 -0
- package/dist/components/breadcrumb/breadcrumb.js.map +1 -0
- package/dist/components/breadcrumb/index.d.ts +2 -0
- package/dist/components/button/button.d.ts +21 -0
- package/dist/components/button/button.js +89 -0
- package/dist/components/button/button.js.map +1 -0
- package/dist/components/button/index.d.ts +2 -0
- package/dist/components/card/card.d.ts +17 -0
- package/dist/components/card/card.js +28 -0
- package/dist/components/card/card.js.map +1 -0
- package/dist/components/card/index.d.ts +2 -0
- package/dist/components/checkbox/checkbox.d.ts +18 -0
- package/dist/components/checkbox/checkbox.js +64 -0
- package/dist/components/checkbox/checkbox.js.map +1 -0
- package/dist/components/checkbox/index.d.ts +2 -0
- package/dist/components/combobox/combobox.d.ts +40 -0
- package/dist/components/combobox/combobox.js +101 -0
- package/dist/components/combobox/combobox.js.map +1 -0
- package/dist/components/combobox/index.d.ts +2 -0
- package/dist/components/container/container.d.ts +17 -0
- package/dist/components/container/container.js +42 -0
- package/dist/components/container/container.js.map +1 -0
- package/dist/components/container/index.d.ts +2 -0
- package/dist/components/date-picker/calendar.d.ts +38 -0
- package/dist/components/date-picker/calendar.js +137 -0
- package/dist/components/date-picker/calendar.js.map +1 -0
- package/dist/components/date-picker/date-picker.d.ts +24 -0
- package/dist/components/date-picker/date-picker.js +52 -0
- package/dist/components/date-picker/date-picker.js.map +1 -0
- package/dist/components/date-picker/date-presets.d.ts +15 -0
- package/dist/components/date-picker/date-presets.js +52 -0
- package/dist/components/date-picker/date-presets.js.map +1 -0
- package/dist/components/date-picker/date-range-picker.d.ts +24 -0
- package/dist/components/date-picker/date-range-picker.js +78 -0
- package/dist/components/date-picker/date-range-picker.js.map +1 -0
- package/dist/components/date-picker/date-sheet.js +33 -0
- package/dist/components/date-picker/date-sheet.js.map +1 -0
- package/dist/components/date-picker/date-trigger.js +14 -0
- package/dist/components/date-picker/date-trigger.js.map +1 -0
- package/dist/components/date-picker/dates.d.ts +8 -0
- package/dist/components/date-picker/dates.js +40 -0
- package/dist/components/date-picker/dates.js.map +1 -0
- package/dist/components/date-picker/index.d.ts +6 -0
- package/dist/components/divider/divider.d.ts +11 -0
- package/dist/components/divider/divider.js +16 -0
- package/dist/components/divider/divider.js.map +1 -0
- package/dist/components/divider/index.d.ts +2 -0
- package/dist/components/drawer/drawer.d.ts +34 -0
- package/dist/components/drawer/drawer.js +144 -0
- package/dist/components/drawer/drawer.js.map +1 -0
- package/dist/components/drawer/index.d.ts +2 -0
- package/dist/components/field/field.d.ts +31 -0
- package/dist/components/field/field.js +52 -0
- package/dist/components/field/field.js.map +1 -0
- package/dist/components/field/index.d.ts +2 -0
- package/dist/components/grid/grid.d.ts +35 -0
- package/dist/components/grid/grid.js +64 -0
- package/dist/components/grid/grid.js.map +1 -0
- package/dist/components/grid/index.d.ts +2 -0
- package/dist/components/icon-button/icon-button.d.ts +23 -0
- package/dist/components/icon-button/icon-button.js +48 -0
- package/dist/components/icon-button/icon-button.js.map +1 -0
- package/dist/components/icon-button/index.d.ts +2 -0
- package/dist/components/input/index.d.ts +2 -0
- package/dist/components/input/input.d.ts +22 -0
- package/dist/components/input/input.js +86 -0
- package/dist/components/input/input.js.map +1 -0
- package/dist/components/link/index.d.ts +2 -0
- package/dist/components/link/link.d.ts +13 -0
- package/dist/components/link/link.js +20 -0
- package/dist/components/link/link.js.map +1 -0
- package/dist/components/list/index.d.ts +2 -0
- package/dist/components/list/list.d.ts +26 -0
- package/dist/components/list/list.js +45 -0
- package/dist/components/list/list.js.map +1 -0
- package/dist/components/menu/index.d.ts +2 -0
- package/dist/components/menu/menu.d.ts +47 -0
- package/dist/components/menu/menu.js +81 -0
- package/dist/components/menu/menu.js.map +1 -0
- package/dist/components/modal/index.d.ts +2 -0
- package/dist/components/modal/modal.d.ts +41 -0
- package/dist/components/modal/modal.js +92 -0
- package/dist/components/modal/modal.js.map +1 -0
- package/dist/components/pagination/index.d.ts +2 -0
- package/dist/components/pagination/pagination.d.ts +21 -0
- package/dist/components/pagination/pagination.js +99 -0
- package/dist/components/pagination/pagination.js.map +1 -0
- package/dist/components/popover/index.d.ts +2 -0
- package/dist/components/popover/popover.d.ts +29 -0
- package/dist/components/popover/popover.js +54 -0
- package/dist/components/popover/popover.js.map +1 -0
- package/dist/components/radio/index.d.ts +2 -0
- package/dist/components/radio/radio.d.ts +16 -0
- package/dist/components/radio/radio.js +37 -0
- package/dist/components/radio/radio.js.map +1 -0
- package/dist/components/segmented-control/index.d.ts +2 -0
- package/dist/components/segmented-control/segmented-control.d.ts +23 -0
- package/dist/components/segmented-control/segmented-control.js +53 -0
- package/dist/components/segmented-control/segmented-control.js.map +1 -0
- package/dist/components/select/field-trigger.d.ts +6 -0
- package/dist/components/select/field-trigger.js +90 -0
- package/dist/components/select/field-trigger.js.map +1 -0
- package/dist/components/select/index.d.ts +2 -0
- package/dist/components/select/select.d.ts +35 -0
- package/dist/components/select/select.js +98 -0
- package/dist/components/select/select.js.map +1 -0
- package/dist/components/skeleton/index.d.ts +2 -0
- package/dist/components/skeleton/skeleton.d.ts +17 -0
- package/dist/components/skeleton/skeleton.js +53 -0
- package/dist/components/skeleton/skeleton.js.map +1 -0
- package/dist/components/slider/index.d.ts +2 -0
- package/dist/components/slider/slider.d.ts +26 -0
- package/dist/components/slider/slider.js +102 -0
- package/dist/components/slider/slider.js.map +1 -0
- package/dist/components/spinner/index.d.ts +2 -0
- package/dist/components/spinner/spinner.d.ts +13 -0
- package/dist/components/spinner/spinner.js +47 -0
- package/dist/components/spinner/spinner.js.map +1 -0
- package/dist/components/stack/index.d.ts +2 -0
- package/dist/components/stack/stack.d.ts +17 -0
- package/dist/components/stack/stack.js +60 -0
- package/dist/components/stack/stack.js.map +1 -0
- package/dist/components/stepper/index.d.ts +2 -0
- package/dist/components/stepper/stepper.d.ts +20 -0
- package/dist/components/stepper/stepper.js +61 -0
- package/dist/components/stepper/stepper.js.map +1 -0
- package/dist/components/switch/index.d.ts +2 -0
- package/dist/components/switch/switch.d.ts +15 -0
- package/dist/components/switch/switch.js +51 -0
- package/dist/components/switch/switch.js.map +1 -0
- package/dist/components/table/index.d.ts +2 -0
- package/dist/components/table/table.d.ts +42 -0
- package/dist/components/table/table.js +83 -0
- package/dist/components/table/table.js.map +1 -0
- package/dist/components/tabs/index.d.ts +2 -0
- package/dist/components/tabs/tabs.d.ts +35 -0
- package/dist/components/tabs/tabs.js +82 -0
- package/dist/components/tabs/tabs.js.map +1 -0
- package/dist/components/tag/index.d.ts +2 -0
- package/dist/components/tag/tag.d.ts +18 -0
- package/dist/components/tag/tag.js +37 -0
- package/dist/components/tag/tag.js.map +1 -0
- package/dist/components/textarea/index.d.ts +2 -0
- package/dist/components/textarea/textarea.d.ts +16 -0
- package/dist/components/textarea/textarea.js +46 -0
- package/dist/components/textarea/textarea.js.map +1 -0
- package/dist/components/toast/index.d.ts +2 -0
- package/dist/components/toast/toast.d.ts +41 -0
- package/dist/components/toast/toast.js +167 -0
- package/dist/components/toast/toast.js.map +1 -0
- package/dist/components/tooltip/index.d.ts +2 -0
- package/dist/components/tooltip/tooltip.d.ts +28 -0
- package/dist/components/tooltip/tooltip.js +62 -0
- package/dist/components/tooltip/tooltip.js.map +1 -0
- package/dist/components/upload/index.d.ts +2 -0
- package/dist/components/upload/upload.d.ts +65 -0
- package/dist/components/upload/upload.js +130 -0
- package/dist/components/upload/upload.js.map +1 -0
- package/dist/index.d.ts +87 -0
- package/dist/index.js +46 -0
- package/dist/tokens/generated.d.ts +371 -0
- package/dist/tokens/generated.js +373 -0
- package/dist/tokens/generated.js.map +1 -0
- package/dist/utils/cn.d.ts +6 -0
- package/dist/utils/cn.js +11 -0
- package/dist/utils/cn.js.map +1 -0
- package/package.json +61 -3
- 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,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,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,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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"}
|