@innovui/react-native 0.0.0 → 0.2.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 (29) hide show
  1. package/LICENSE +21 -0
  2. package/dist/components/_shared/anchored-overlay.js +37 -12
  3. package/dist/components/_shared/anchored-overlay.js.map +1 -1
  4. package/dist/components/_shared/sliding-indicator.js +98 -0
  5. package/dist/components/_shared/sliding-indicator.js.map +1 -0
  6. package/dist/components/_shared/use-transition.js +53 -0
  7. package/dist/components/_shared/use-transition.js.map +1 -0
  8. package/dist/components/drawer/drawer.d.ts +11 -1
  9. package/dist/components/drawer/drawer.js +37 -22
  10. package/dist/components/drawer/drawer.js.map +1 -1
  11. package/dist/components/drawer/use-swipe-to-close.js +143 -0
  12. package/dist/components/drawer/use-swipe-to-close.js.map +1 -0
  13. package/dist/components/input/input.d.ts +8 -2
  14. package/dist/components/input/input.js +10 -2
  15. package/dist/components/input/input.js.map +1 -1
  16. package/dist/components/modal/modal.js +59 -39
  17. package/dist/components/modal/modal.js.map +1 -1
  18. package/dist/components/segmented-control/segmented-control.d.ts +2 -2
  19. package/dist/components/segmented-control/segmented-control.js +20 -7
  20. package/dist/components/segmented-control/segmented-control.js.map +1 -1
  21. package/dist/components/tabs/tabs.d.ts +2 -2
  22. package/dist/components/tabs/tabs.js +32 -9
  23. package/dist/components/tabs/tabs.js.map +1 -1
  24. package/dist/components/toast/toast.js +66 -5
  25. package/dist/components/toast/toast.js.map +1 -1
  26. package/dist/components/tooltip/tooltip.d.ts +7 -3
  27. package/dist/components/tooltip/tooltip.js +10 -4
  28. package/dist/components/tooltip/tooltip.js.map +1 -1
  29. package/package.json +9 -9
@@ -1 +1 @@
1
- {"version":3,"file":"tabs.js","names":[],"sources":["../../../src/components/tabs/tabs.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { createContext, type ReactNode, useContext, useMemo, useState } from 'react'\nimport {\n Pressable,\n type PressableProps,\n ScrollView,\n type ScrollViewProps,\n Text,\n View,\n type ViewProps,\n} from 'react-native'\nimport { cn } from '../../utils/cn'\n\ntype TabsContextValue = { value: string | undefined; setValue: (value: string) => void }\n\nconst TabsContext = createContext<TabsContextValue>({ value: undefined, setValue: () => {} })\n\nexport type TabsProps = ViewProps & {\n className?: string\n /** Aba ativa (controlado). */\n value?: string\n /** Aba ativa inicial (não controlado). */\n defaultValue?: string\n onValueChange?: (value: string) => void\n}\n\n/** Raiz das abas: controla qual `Tab` está ativa (`value`/`defaultValue`/`onValueChange`). */\nexport function Tabs({ className, value, defaultValue, onValueChange, ...props }: TabsProps) {\n const [internal, setInternal] = useState(defaultValue)\n const current = value !== undefined ? value : internal\n\n const context = useMemo(\n () => ({\n value: current,\n setValue: (next: string) => {\n if (value === undefined) setInternal(next)\n onValueChange?.(next)\n },\n }),\n [current, value, onValueChange],\n )\n\n return (\n <TabsContext.Provider value={context}>\n <View className={className} {...props} />\n </TabsContext.Provider>\n )\n}\n\nexport type TabListProps = Omit<ScrollViewProps, 'horizontal' | 'style'> & {\n className?: string\n}\n\n/** Lista de gatilhos. Rola na horizontal quando as abas não cabem na tela. */\nexport function TabList({ className, ...props }: TabListProps) {\n return (\n <ScrollView\n horizontal\n showsHorizontalScrollIndicator={false}\n accessibilityRole=\"tablist\"\n className={cn('grow-0', className)}\n contentContainerClassName=\"items-start\"\n {...props}\n />\n )\n}\n\nexport type TabProps = Omit<PressableProps, 'children' | 'style'> & {\n className?: string\n /** Valor que liga a aba ao `TabPanel` de mesmo `value`. */\n value: string\n /** Ícone à esquerda do rótulo (ex.: um ícone SVG de 24px). */\n icon?: ReactNode\n children?: ReactNode\n}\n\nexport function Tab({\n className,\n value,\n icon,\n children,\n disabled,\n onPress,\n accessibilityState,\n ...props\n}: TabProps) {\n const { value: active, setValue } = useContext(TabsContext)\n const selected = active === value\n\n return (\n <Pressable\n accessibilityRole=\"tab\"\n accessibilityState={{ ...accessibilityState, selected, disabled: Boolean(disabled) }}\n disabled={disabled}\n onPress={(event) => {\n setValue(value)\n onPress?.(event)\n }}\n className={cn('items-start gap-[6px] px-3 pt-2', className)}\n {...props}\n >\n <View className=\"flex-row items-center gap-1\">\n {icon}\n {typeof children === 'string' ? (\n <Text\n className={cn(\n 'text-sm text-text-secondary',\n selected && 'font-medium text-text-brand',\n disabled && 'text-text-disabled',\n )}\n >\n {children}\n </Text>\n ) : (\n children\n )}\n </View>\n <View\n className={cn('h-0.5 self-stretch bg-transparent', selected && 'bg-interactive-primary')}\n />\n </Pressable>\n )\n}\n\nexport type TabPanelProps = ViewProps & {\n className?: string\n value: string\n}\n\n/** Painel da aba: só é montado enquanto a aba de mesmo `value` está ativa (igual ao web). */\nexport function TabPanel({ className, value, ...props }: TabPanelProps) {\n const { value: active } = useContext(TabsContext)\n if (active !== value) return null\n return <View className={className} {...props} />\n}\n"],"mappings":";;;;;AAgBA,MAAM,cAAc,cAAgC;CAAE,OAAO,KAAA;CAAW,gBAAgB,CAAC;AAAE,CAAC;;AAY5F,SAAgB,KAAK,EAAE,WAAW,OAAO,cAAc,eAAe,GAAG,SAAoB;CAC3F,MAAM,CAAC,UAAU,eAAe,SAAS,YAAY;CACrD,MAAM,UAAU,UAAU,KAAA,IAAY,QAAQ;CAE9C,MAAM,UAAU,eACP;EACL,OAAO;EACP,WAAW,SAAiB;GAC1B,IAAI,UAAU,KAAA,GAAW,YAAY,IAAI;GACzC,gBAAgB,IAAI;EACtB;CACF,IACA;EAAC;EAAS;EAAO;CAAa,CAChC;CAEA,OACE,oBAAC,YAAY,UAAb;EAAsB,OAAO;EAC3B,UAAA,oBAAC,MAAD;GAAiB;GAAW,GAAI;EAAQ,CAAA;CACpB,CAAA;AAE1B;;AAOA,SAAgB,QAAQ,EAAE,WAAW,GAAG,SAAuB;CAC7D,OACE,oBAAC,YAAD;EACE,YAAA;EACA,gCAAgC;EAChC,mBAAkB;EAClB,WAAW,GAAG,UAAU,SAAS;EACjC,2BAA0B;EAC1B,GAAI;CACL,CAAA;AAEL;AAWA,SAAgB,IAAI,EAClB,WACA,OACA,MACA,UACA,UACA,SACA,oBACA,GAAG,SACQ;CACX,MAAM,EAAE,OAAO,QAAQ,aAAa,WAAW,WAAW;CAC1D,MAAM,WAAW,WAAW;CAE5B,OACE,qBAAC,WAAD;EACE,mBAAkB;EAClB,oBAAoB;GAAE,GAAG;GAAoB;GAAU,UAAU,QAAQ,QAAQ;EAAE;EACzE;EACV,UAAU,UAAU;GAClB,SAAS,KAAK;GACd,UAAU,KAAK;EACjB;EACA,WAAW,GAAG,mCAAmC,SAAS;EAC1D,GAAI;EATN,UAAA,CAWE,qBAAC,MAAD;GAAM,WAAU;GAAhB,UAAA,CACG,MACA,OAAO,aAAa,WACnB,oBAAC,MAAD;IACE,WAAW,GACT,+BACA,YAAY,+BACZ,YAAY,oBACd;IAEC;GACG,CAAA,IAEN,QAEE;EACN,CAAA,GAAA,oBAAC,MAAD,EACE,WAAW,GAAG,qCAAqC,YAAY,wBAAwB,EACxF,CAAA,CACQ;;AAEf;;AAQA,SAAgB,SAAS,EAAE,WAAW,OAAO,GAAG,SAAwB;CACtE,MAAM,EAAE,OAAO,WAAW,WAAW,WAAW;CAChD,IAAI,WAAW,OAAO,OAAO;CAC7B,OAAO,oBAAC,MAAD;EAAiB;EAAW,GAAI;CAAQ,CAAA;AACjD"}
1
+ {"version":3,"file":"tabs.js","names":[],"sources":["../../../src/components/tabs/tabs.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { createContext, type ReactNode, useContext, useMemo, useState } from 'react'\nimport {\n Pressable,\n type PressableProps,\n ScrollView,\n type ScrollViewProps,\n Text,\n View,\n type ViewProps,\n} from 'react-native'\nimport { cn } from '../../utils/cn'\nimport { type Rect, SlidingIndicator, useItemLayouts } from '../_shared/sliding-indicator'\n\ntype TabsContextValue = {\n value: string | undefined\n setValue: (value: string) => void\n /** Posição de cada aba no contêiner da lista, para o indicador deslizante. */\n layouts: Record<string, Rect>\n setLayout: (value: string, rect: Rect) => void\n}\n\nconst TabsContext = createContext<TabsContextValue>({\n value: undefined,\n setValue: () => {},\n layouts: {},\n setLayout: () => {},\n})\n\nexport type TabsProps = ViewProps & {\n className?: string\n /** Aba ativa (controlado). */\n value?: string\n /** Aba ativa inicial (não controlado). */\n defaultValue?: string\n onValueChange?: (value: string) => void\n}\n\n/** Raiz das abas: controla qual `Tab` está ativa (`value`/`defaultValue`/`onValueChange`). */\nexport function Tabs({ className, value, defaultValue, onValueChange, ...props }: TabsProps) {\n const [internal, setInternal] = useState(defaultValue)\n const current = value !== undefined ? value : internal\n\n const { layouts, setLayout } = useItemLayouts()\n const context = useMemo(\n () => ({\n value: current,\n setValue: (next: string) => {\n if (value === undefined) setInternal(next)\n onValueChange?.(next)\n },\n layouts,\n setLayout,\n }),\n [current, value, onValueChange, layouts, setLayout],\n )\n\n return (\n <TabsContext.Provider value={context}>\n <View className={className} {...props} />\n </TabsContext.Provider>\n )\n}\n\nexport type TabListProps = Omit<ScrollViewProps, 'horizontal' | 'style'> & {\n className?: string\n}\n\n/** Lista de gatilhos. Rola na horizontal quando as abas não cabem na tela. */\nexport function TabList({ className, children, ...props }: TabListProps) {\n const { value, layouts } = useContext(TabsContext)\n const active = value === undefined ? undefined : layouts[value]\n return (\n <ScrollView\n horizontal\n showsHorizontalScrollIndicator={false}\n accessibilityRole=\"tablist\"\n className={cn('grow-0', className)}\n contentContainerClassName=\"items-start\"\n {...props}\n >\n {/* a linha de 2px é do indicador: fica na base da aba ativa e desliza para a próxima */}\n <SlidingIndicator\n target={active && { ...active, y: active.y + active.height - 2, height: 2 }}\n className=\"bg-interactive-primary\"\n />\n {children}\n </ScrollView>\n )\n}\n\nexport type TabProps = Omit<PressableProps, 'children' | 'style'> & {\n className?: string\n /** Valor que liga a aba ao `TabPanel` de mesmo `value`. */\n value: string\n /** Ícone à esquerda do rótulo (ex.: um ícone SVG de 24px). */\n icon?: ReactNode\n children?: ReactNode\n}\n\nexport function Tab({\n className,\n value,\n icon,\n children,\n disabled,\n onPress,\n onLayout,\n accessibilityState,\n ...props\n}: TabProps) {\n const { value: active, setValue, layouts, setLayout } = useContext(TabsContext)\n const selected = active === value\n // antes de o indicador medir a aba ativa, ela ainda se desenha sozinha\n const slid = active !== undefined && layouts[active] !== undefined\n\n return (\n <Pressable\n accessibilityRole=\"tab\"\n accessibilityState={{ ...accessibilityState, selected, disabled: Boolean(disabled) }}\n disabled={disabled}\n onPress={(event) => {\n setValue(value)\n onPress?.(event)\n }}\n onLayout={(event) => {\n setLayout(value, event.nativeEvent.layout)\n onLayout?.(event)\n }}\n className={cn('items-start gap-[6px] px-3 pt-2', className)}\n {...props}\n >\n <View className=\"flex-row items-center gap-1\">\n {icon}\n {typeof children === 'string' ? (\n <Text\n className={cn(\n 'text-sm text-text-secondary',\n selected && 'font-medium text-text-brand',\n disabled && 'text-text-disabled',\n )}\n >\n {children}\n </Text>\n ) : (\n children\n )}\n </View>\n <View\n className={cn(\n 'h-0.5 self-stretch bg-transparent',\n selected && !slid && 'bg-interactive-primary',\n )}\n />\n </Pressable>\n )\n}\n\nexport type TabPanelProps = ViewProps & {\n className?: string\n value: string\n}\n\n/** Painel da aba: só é montado enquanto a aba de mesmo `value` está ativa (igual ao web). */\nexport function TabPanel({ className, value, ...props }: TabPanelProps) {\n const { value: active } = useContext(TabsContext)\n if (active !== value) return null\n return <View className={className} {...props} />\n}\n"],"mappings":";;;;;;AAuBA,MAAM,cAAc,cAAgC;CAClD,OAAO,KAAA;CACP,gBAAgB,CAAC;CACjB,SAAS,CAAC;CACV,iBAAiB,CAAC;AACpB,CAAC;;AAYD,SAAgB,KAAK,EAAE,WAAW,OAAO,cAAc,eAAe,GAAG,SAAoB;CAC3F,MAAM,CAAC,UAAU,eAAe,SAAS,YAAY;CACrD,MAAM,UAAU,UAAU,KAAA,IAAY,QAAQ;CAE9C,MAAM,EAAE,SAAS,cAAc,eAAe;CAC9C,MAAM,UAAU,eACP;EACL,OAAO;EACP,WAAW,SAAiB;GAC1B,IAAI,UAAU,KAAA,GAAW,YAAY,IAAI;GACzC,gBAAgB,IAAI;EACtB;EACA;EACA;CACF,IACA;EAAC;EAAS;EAAO;EAAe;EAAS;CAAS,CACpD;CAEA,OACE,oBAAC,YAAY,UAAb;EAAsB,OAAO;EAC3B,UAAA,oBAAC,MAAD;GAAiB;GAAW,GAAI;EAAQ,CAAA;CACpB,CAAA;AAE1B;;AAOA,SAAgB,QAAQ,EAAE,WAAW,UAAU,GAAG,SAAuB;CACvE,MAAM,EAAE,OAAO,YAAY,WAAW,WAAW;CACjD,MAAM,SAAS,UAAU,KAAA,IAAY,KAAA,IAAY,QAAQ;CACzD,OACE,qBAAC,YAAD;EACE,YAAA;EACA,gCAAgC;EAChC,mBAAkB;EAClB,WAAW,GAAG,UAAU,SAAS;EACjC,2BAA0B;EAC1B,GAAI;EANN,UAAA,CASE,oBAAC,kBAAD;GACE,QAAQ,UAAU;IAAE,GAAG;IAAQ,GAAG,OAAO,IAAI,OAAO,SAAS;IAAG,QAAQ;GAAE;GAC1E,WAAU;EACX,CAAA,GACA,QACS;;AAEhB;AAWA,SAAgB,IAAI,EAClB,WACA,OACA,MACA,UACA,UACA,SACA,UACA,oBACA,GAAG,SACQ;CACX,MAAM,EAAE,OAAO,QAAQ,UAAU,SAAS,cAAc,WAAW,WAAW;CAC9E,MAAM,WAAW,WAAW;CAE5B,MAAM,OAAO,WAAW,KAAA,KAAa,QAAQ,YAAY,KAAA;CAEzD,OACE,qBAAC,WAAD;EACE,mBAAkB;EAClB,oBAAoB;GAAE,GAAG;GAAoB;GAAU,UAAU,QAAQ,QAAQ;EAAE;EACzE;EACV,UAAU,UAAU;GAClB,SAAS,KAAK;GACd,UAAU,KAAK;EACjB;EACA,WAAW,UAAU;GACnB,UAAU,OAAO,MAAM,YAAY,MAAM;GACzC,WAAW,KAAK;EAClB;EACA,WAAW,GAAG,mCAAmC,SAAS;EAC1D,GAAI;EAbN,UAAA,CAeE,qBAAC,MAAD;GAAM,WAAU;GAAhB,UAAA,CACG,MACA,OAAO,aAAa,WACnB,oBAAC,MAAD;IACE,WAAW,GACT,+BACA,YAAY,+BACZ,YAAY,oBACd;IAEC;GACG,CAAA,IAEN,QAEE;EACN,CAAA,GAAA,oBAAC,MAAD,EACE,WAAW,GACT,qCACA,YAAY,CAAC,QAAQ,wBACvB,EACD,CAAA,CACQ;;AAEf;;AAQA,SAAgB,SAAS,EAAE,WAAW,OAAO,GAAG,SAAwB;CACtE,MAAM,EAAE,OAAO,WAAW,WAAW,WAAW;CAChD,IAAI,WAAW,OAAO,OAAO;CAC7B,OAAO,oBAAC,MAAD;EAAiB;EAAW,GAAI;CAAQ,CAAA;AACjD"}
@@ -1,11 +1,16 @@
1
1
  import { CloseIcon, StatusIcon, statusRole, statusText, statusTextToken } from "../_shared/status.js";
2
2
  import { useToken } from "../_shared/use-token.js";
3
3
  import { Button } from "../button/button.js";
4
+ import { useReducedMotion } from "../_shared/sliding-indicator.js";
4
5
  import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
5
- import { Animated, Platform, Pressable, StatusBar, Text, View } from "react-native";
6
+ import { Animated, LayoutAnimation, PanResponder, Platform, Pressable, StatusBar, Text, UIManager, View } from "react-native";
6
7
  import { jsx, jsxs } from "react/jsx-runtime";
7
8
  //#region src/components/toast/toast.tsx
8
9
  const ToastContext = createContext(null);
10
+ if (Platform.OS === "android") UIManager.setLayoutAnimationEnabledExperimental?.(true);
11
+ /** Arrastar o toast para o lado além disto (px), ou mais rápido que isto (px/ms), o dispensa. */
12
+ const SWIPE_DISTANCE = 50;
13
+ const SWIPE_VELOCITY = .5;
9
14
  const topInset = Platform.OS === "ios" ? 56 : (StatusBar.currentHeight ?? 24) + 8;
10
15
  /**
11
16
  * Envolva o app uma vez; qualquer componente abaixo dispara toasts com `useToast()`. A pilha fica
@@ -20,9 +25,11 @@ function ToastProvider({ children, label = "Notificações", dismissLabel = "Fec
20
25
  closing: true
21
26
  } : item));
22
27
  }, []);
28
+ const reduced = useReducedMotion();
23
29
  const remove = useCallback((id) => {
30
+ if (!reduced) LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
24
31
  setItems((current) => current.filter((item) => item.id !== id));
25
- }, []);
32
+ }, [reduced]);
26
33
  const toast = useCallback((options) => {
27
34
  const id = nextId.current++;
28
35
  setItems((current) => [...current, {
@@ -67,6 +74,58 @@ function ToastProvider({ children, label = "Notificações", dismissLabel = "Fec
67
74
  function ToastView({ item, dismissLabel, onDismiss, onRemove }) {
68
75
  const { id, status = "info", title, description, action, duration = 5e3, showIcon = true, dismissible = true, closing } = item;
69
76
  const progress = useRef(new Animated.Value(0)).current;
77
+ const dragX = useRef(new Animated.Value(0)).current;
78
+ const [dragging, setDragging] = useState(false);
79
+ const latest = useRef({
80
+ id,
81
+ onDismiss
82
+ });
83
+ latest.current = {
84
+ id,
85
+ onDismiss
86
+ };
87
+ const panResponder = useRef(PanResponder.create({
88
+ onMoveShouldSetPanResponder: (_, g) => Math.abs(g.dx) > 6 && Math.abs(g.dx) > Math.abs(g.dy),
89
+ onPanResponderTerminationRequest: () => false,
90
+ onPanResponderGrant: () => setDragging(true),
91
+ onPanResponderMove: (_, g) => dragX.setValue(g.dx),
92
+ onPanResponderRelease: (_, g) => {
93
+ setDragging(false);
94
+ if (Math.abs(g.dx) > SWIPE_DISTANCE || Math.abs(g.vx) > SWIPE_VELOCITY) {
95
+ Animated.timing(dragX, {
96
+ toValue: (g.dx || g.vx) > 0 ? 400 : -400,
97
+ duration: 150,
98
+ useNativeDriver: true
99
+ }).start();
100
+ latest.current.onDismiss(latest.current.id);
101
+ } else Animated.timing(dragX, {
102
+ toValue: 0,
103
+ duration: 200,
104
+ useNativeDriver: true
105
+ }).start();
106
+ },
107
+ onPanResponderTerminate: () => {
108
+ setDragging(false);
109
+ Animated.timing(dragX, {
110
+ toValue: 0,
111
+ duration: 200,
112
+ useNativeDriver: true
113
+ }).start();
114
+ }
115
+ })).current;
116
+ const swipeFade = dragX.interpolate({
117
+ inputRange: [
118
+ -150,
119
+ 0,
120
+ 150
121
+ ],
122
+ outputRange: [
123
+ .2,
124
+ 1,
125
+ .2
126
+ ],
127
+ extrapolate: "clamp"
128
+ });
70
129
  const iconColor = useToken(statusTextToken[status]);
71
130
  const closeColor = useToken("--color-text-primary");
72
131
  useEffect(() => {
@@ -95,11 +154,12 @@ function ToastView({ item, dismissLabel, onDismiss, onRemove }) {
95
154
  progress
96
155
  ]);
97
156
  useEffect(() => {
98
- if (closing || duration <= 0) return;
157
+ if (closing || dragging || duration <= 0) return;
99
158
  const timer = setTimeout(() => onDismiss(id), duration);
100
159
  return () => clearTimeout(timer);
101
160
  }, [
102
161
  closing,
162
+ dragging,
103
163
  duration,
104
164
  id,
105
165
  onDismiss
@@ -109,9 +169,10 @@ function ToastView({ item, dismissLabel, onDismiss, onRemove }) {
109
169
  outputRange: [-16, 0]
110
170
  });
111
171
  return /* @__PURE__ */ jsx(Animated.View, {
172
+ ...panResponder.panHandlers,
112
173
  style: {
113
- opacity: progress,
114
- transform: [{ translateY }]
174
+ opacity: Animated.multiply(progress, swipeFade),
175
+ transform: [{ translateY }, { translateX: dragX }]
115
176
  },
116
177
  children: /* @__PURE__ */ jsxs(View, {
117
178
  accessibilityRole: statusRole(status),
@@ -1 +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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"}
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 {\n Animated,\n LayoutAnimation,\n PanResponder,\n Platform,\n Pressable,\n StatusBar,\n Text,\n UIManager,\n View,\n} from 'react-native'\nimport { useReducedMotion } from '../_shared/sliding-indicator'\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// No Android antigo o LayoutAnimation precisa ser habilitado. Na New Architecture a chamada é no-op.\nif (Platform.OS === 'android') {\n UIManager.setLayoutAnimationEnabledExperimental?.(true)\n}\n\n/** Arrastar o toast para o lado além disto (px), ou mais rápido que isto (px/ms), o dispensa. */\nconst SWIPE_DISTANCE = 50\nconst SWIPE_VELOCITY = 0.5\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 reduced = useReducedMotion()\n const remove = useCallback(\n (id: number) => {\n // os de baixo deslizam para o lugar em vez de pular\n if (!reduced) LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)\n setItems((current) => current.filter((item) => item.id !== id))\n },\n [reduced],\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 // arrastar para o lado dispensa (como o swipe do Radix no web); o auto-fechar pausa enquanto isso\n const dragX = useRef(new Animated.Value(0)).current\n const [dragging, setDragging] = useState(false)\n const latest = useRef({ id, onDismiss })\n latest.current = { id, onDismiss }\n const panResponder = useRef(\n PanResponder.create({\n onMoveShouldSetPanResponder: (_, g) => Math.abs(g.dx) > 6 && Math.abs(g.dx) > Math.abs(g.dy),\n onPanResponderTerminationRequest: () => false,\n onPanResponderGrant: () => setDragging(true),\n onPanResponderMove: (_, g) => dragX.setValue(g.dx),\n onPanResponderRelease: (_, g) => {\n setDragging(false)\n if (Math.abs(g.dx) > SWIPE_DISTANCE || Math.abs(g.vx) > SWIPE_VELOCITY) {\n // continua para o lado de onde o dedo ia, enquanto o toast some\n Animated.timing(dragX, {\n toValue: (g.dx || g.vx) > 0 ? 400 : -400,\n duration: 150,\n useNativeDriver: true,\n }).start()\n latest.current.onDismiss(latest.current.id)\n } else {\n Animated.timing(dragX, { toValue: 0, duration: 200, useNativeDriver: true }).start()\n }\n },\n onPanResponderTerminate: () => {\n setDragging(false)\n Animated.timing(dragX, { toValue: 0, duration: 200, useNativeDriver: true }).start()\n },\n }),\n ).current\n const swipeFade = dragX.interpolate({\n inputRange: [-150, 0, 150],\n outputRange: [0.2, 1, 0.2],\n extrapolate: 'clamp',\n })\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 || dragging || duration <= 0) return\n const timer = setTimeout(() => onDismiss(id), duration)\n return () => clearTimeout(timer)\n }, [closing, dragging, duration, id, onDismiss])\n\n const translateY = progress.interpolate({ inputRange: [0, 1], outputRange: [-16, 0] })\n\n return (\n <Animated.View\n {...panResponder.panHandlers}\n style={{\n opacity: Animated.multiply(progress, swipeFade),\n transform: [{ translateY }, { translateX: dragX }],\n }}\n >\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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"}
@@ -14,15 +14,19 @@ export type TooltipProps = {
14
14
  side?: 'top' | 'bottom';
15
15
  /** Fecha sozinho depois deste tempo, em ms. `0` mantém aberto até tocar fora. Padrão: 3000. */
16
16
  duration?: number;
17
+ /** Seta apontando para o gatilho. Padrão: `true`. */
18
+ showArrow?: boolean;
19
+ /** Quanto tempo segurar o gatilho antes de abrir, em ms (o `delayLongPress` do RN, 500 por padrão). */
20
+ delayDuration?: number;
17
21
  className?: string;
18
22
  testID?: string;
19
23
  };
20
24
  /**
21
25
  * Dica contextual. No toque não há hover: abre ao pressionar e segurar o gatilho (`onLongPress`),
22
26
  * 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`.
27
+ * o `content` (se for texto) vira o `accessibilityHint` do gatilho. Fica centrado no gatilho, com
28
+ * seta. Diferença do web: sem os lados `left`/`right`.
25
29
  */
26
- export declare function Tooltip({ open, defaultOpen, onOpenChange, children, content, title, side, duration, className, testID }: TooltipProps): import("react").JSX.Element;
30
+ export declare function Tooltip({ open, defaultOpen, onOpenChange, children, content, title, side, duration, showArrow, delayDuration, className, testID }: TooltipProps): import("react").JSX.Element;
27
31
  //#endregion
28
32
  //# sourceMappingURL=tooltip.d.ts.map
@@ -9,10 +9,10 @@ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
9
9
  /**
10
10
  * Dica contextual. No toque não há hover: abre ao pressionar e segurar o gatilho (`onLongPress`),
11
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`.
12
+ * o `content` (se for texto) vira o `accessibilityHint` do gatilho. Fica centrado no gatilho, com
13
+ * seta. Diferença do web: sem os lados `left`/`right`.
14
14
  */
15
- function Tooltip({ open, defaultOpen, onOpenChange, children, content, title, side = "top", duration = 3e3, className, testID }) {
15
+ function Tooltip({ open, defaultOpen, onOpenChange, children, content, title, side = "top", duration = 3e3, showArrow = true, delayDuration, className, testID }) {
16
16
  const [isOpen, setOpen] = useOpenState({
17
17
  open,
18
18
  defaultOpen,
@@ -34,7 +34,10 @@ function Tooltip({ open, defaultOpen, onOpenChange, children, content, title, si
34
34
  anchorRef: ref,
35
35
  event: "onLongPress",
36
36
  onTrigger: () => setOpen(true),
37
- extra: hint ? { accessibilityHint: hint } : void 0,
37
+ extra: {
38
+ ...hint ? { accessibilityHint: hint } : null,
39
+ ...delayDuration === void 0 ? null : { delayLongPress: delayDuration }
40
+ },
38
41
  children
39
42
  }), /* @__PURE__ */ jsx(AnchoredOverlay, {
40
43
  visible: isOpen,
@@ -42,6 +45,9 @@ function Tooltip({ open, defaultOpen, onOpenChange, children, content, title, si
42
45
  onRequestClose: close,
43
46
  placement: side,
44
47
  estimatedHeight: 80,
48
+ align: "center",
49
+ offset: showArrow ? 8 : 4,
50
+ arrowClassName: showArrow ? "bg-bg-inverse" : void 0,
45
51
  testID,
46
52
  className: cn("max-w-[240px] gap-1 rounded-sm bg-bg-inverse px-3 py-2", className),
47
53
  children: /* @__PURE__ */ jsxs(View, {
@@ -1 +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"}
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 /** Seta apontando para o gatilho. Padrão: `true`. */\n showArrow?: boolean\n /** Quanto tempo segurar o gatilho antes de abrir, em ms (o `delayLongPress` do RN, 500 por padrão). */\n delayDuration?: 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. Fica centrado no gatilho, com\n * seta. Diferença do web: sem os 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 showArrow = true,\n delayDuration,\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={{\n ...(hint ? { accessibilityHint: hint } : null),\n ...(delayDuration === undefined ? null : { delayLongPress: delayDuration }),\n }}\n >\n {children}\n </AnchorTrigger>\n <AnchoredOverlay\n visible={isOpen}\n rect={rect}\n onRequestClose={close}\n placement={side}\n estimatedHeight={80}\n align=\"center\"\n offset={showArrow ? 8 : 4}\n arrowClassName={showArrow ? 'bg-bg-inverse' : undefined}\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":";;;;;;;;;;;;;;AAqCA,SAAgB,QAAQ,EACtB,MACA,aACA,cACA,UACA,SACA,OACA,OAAO,OACP,WAAW,KACX,YAAY,MACZ,eACA,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;GACL,GAAI,OAAO,EAAE,mBAAmB,KAAK,IAAI;GACzC,GAAI,kBAAkB,KAAA,IAAY,OAAO,EAAE,gBAAgB,cAAc;EAC3E;EAEC;CACY,CAAA,GACf,oBAAC,iBAAD;EACE,SAAS;EACH;EACN,gBAAgB;EAChB,WAAW;EACX,iBAAiB;EACjB,OAAM;EACN,QAAQ,YAAY,IAAI;EACxB,gBAAgB,YAAY,kBAAkB,KAAA;EACtC;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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@innovui/react-native",
3
- "version": "0.0.0",
3
+ "version": "0.2.0",
4
4
  "description": "Componentes React Native do design system InnovUI.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -32,13 +32,6 @@
32
32
  "tailwindcss": ">=4",
33
33
  "uniwind": ">=1.13.0"
34
34
  },
35
- "scripts": {
36
- "build": "tsdown",
37
- "generate:tokens": "node scripts/generate-tokens.ts",
38
- "test": "node scripts/generate-tokens.ts --check && jest",
39
- "test:coverage": "node scripts/generate-tokens.ts --check && jest --coverage",
40
- "typecheck": "tsc --noEmit"
41
- },
42
35
  "devDependencies": {
43
36
  "@react-native/babel-preset": "0.86.3",
44
37
  "@react-native/jest-preset": "0.86.3",
@@ -60,5 +53,12 @@
60
53
  "class-variance-authority": "^0.7.1",
61
54
  "clsx": "^2.1.1",
62
55
  "tailwind-merge": "^3.7.0"
56
+ },
57
+ "scripts": {
58
+ "build": "tsdown",
59
+ "generate:tokens": "node scripts/generate-tokens.ts",
60
+ "test": "node scripts/generate-tokens.ts --check && jest",
61
+ "test:coverage": "node scripts/generate-tokens.ts --check && jest --coverage",
62
+ "typecheck": "tsc --noEmit"
63
63
  }
64
- }
64
+ }