@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.
- package/LICENSE +21 -0
- package/dist/components/_shared/anchored-overlay.js +37 -12
- package/dist/components/_shared/anchored-overlay.js.map +1 -1
- package/dist/components/_shared/sliding-indicator.js +98 -0
- package/dist/components/_shared/sliding-indicator.js.map +1 -0
- package/dist/components/_shared/use-transition.js +53 -0
- package/dist/components/_shared/use-transition.js.map +1 -0
- package/dist/components/drawer/drawer.d.ts +11 -1
- package/dist/components/drawer/drawer.js +37 -22
- package/dist/components/drawer/drawer.js.map +1 -1
- package/dist/components/drawer/use-swipe-to-close.js +143 -0
- package/dist/components/drawer/use-swipe-to-close.js.map +1 -0
- package/dist/components/input/input.d.ts +8 -2
- package/dist/components/input/input.js +10 -2
- package/dist/components/input/input.js.map +1 -1
- package/dist/components/modal/modal.js +59 -39
- package/dist/components/modal/modal.js.map +1 -1
- package/dist/components/segmented-control/segmented-control.d.ts +2 -2
- package/dist/components/segmented-control/segmented-control.js +20 -7
- package/dist/components/segmented-control/segmented-control.js.map +1 -1
- package/dist/components/tabs/tabs.d.ts +2 -2
- package/dist/components/tabs/tabs.js +32 -9
- package/dist/components/tabs/tabs.js.map +1 -1
- package/dist/components/toast/toast.js +66 -5
- package/dist/components/toast/toast.js.map +1 -1
- package/dist/components/tooltip/tooltip.d.ts +7 -3
- package/dist/components/tooltip/tooltip.js +10 -4
- package/dist/components/tooltip/tooltip.js.map +1 -1
- package/package.json +9 -9
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { useCallback, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { Animated, PanResponder } from "react-native";
|
|
3
|
+
//#region src/components/drawer/use-swipe-to-close.ts
|
|
4
|
+
/** Eixo do arraste e sinal do movimento do dedo que fecha (em direção à borda do `side`). */
|
|
5
|
+
const closing = {
|
|
6
|
+
right: {
|
|
7
|
+
axis: "x",
|
|
8
|
+
sign: 1
|
|
9
|
+
},
|
|
10
|
+
left: {
|
|
11
|
+
axis: "x",
|
|
12
|
+
sign: -1
|
|
13
|
+
},
|
|
14
|
+
bottom: {
|
|
15
|
+
axis: "y",
|
|
16
|
+
sign: 1
|
|
17
|
+
},
|
|
18
|
+
top: {
|
|
19
|
+
axis: "y",
|
|
20
|
+
sign: -1
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
/** Fração do painel que precisa sair, ou a velocidade (px/ms) do gesto, para soltar e fechar. */
|
|
24
|
+
const CLOSE_DISTANCE = .4;
|
|
25
|
+
const CLOSE_VELOCITY = .5;
|
|
26
|
+
/** Movimento mínimo (px) antes de o painel tomar o gesto. */
|
|
27
|
+
const SLOP = 6;
|
|
28
|
+
/**
|
|
29
|
+
* Fecha o painel arrastando-o para a borda de onde ele veio (mesmo comportamento do web).
|
|
30
|
+
*
|
|
31
|
+
* `PanResponder` na fase de captura, para tomar o gesto do `ScrollView` do corpo, mas só quando o
|
|
32
|
+
* dedo vai no sentido de fechar, o gesto é mais ao longo do eixo do que de lado e o corpo não rola
|
|
33
|
+
* mais naquele sentido (no `bottom`, rolagem no topo; no `top`, no fim). Como a captura é refeita a
|
|
34
|
+
* cada movimento, um gesto que começou rolando passa ao painel quando o conteúdo chega à borda; o
|
|
35
|
+
* arraste conta daí (o grant zera `dx`/`dy`), sem salto. O `Slider` não perde o toque: ele não aceita ser
|
|
36
|
+
* tomado (`onResponderTerminationRequest` retorna `false`).
|
|
37
|
+
*
|
|
38
|
+
* Devolve `drag` (px arrastados, ≥ 0) e `dragFade` (1 → 0 conforme o painel sai, para o backdrop),
|
|
39
|
+
* os `panHandlers` do painel, o `onLayout` que mede o painel e as props do `ScrollView` do corpo.
|
|
40
|
+
*/
|
|
41
|
+
function useSwipeToClose({ side, enabled, fallbackSize, onClose, isOpen }) {
|
|
42
|
+
const drag = useRef(new Animated.Value(0)).current;
|
|
43
|
+
const [panelSize, setPanelSize] = useState(fallbackSize);
|
|
44
|
+
const size = useRef(fallbackSize);
|
|
45
|
+
const scroll = useRef({
|
|
46
|
+
offset: 0,
|
|
47
|
+
content: 0,
|
|
48
|
+
layout: 0
|
|
49
|
+
});
|
|
50
|
+
const state = useRef({ offset: 0 });
|
|
51
|
+
const latest = useRef({
|
|
52
|
+
side,
|
|
53
|
+
enabled,
|
|
54
|
+
onClose,
|
|
55
|
+
isOpen
|
|
56
|
+
});
|
|
57
|
+
latest.current = {
|
|
58
|
+
side,
|
|
59
|
+
enabled,
|
|
60
|
+
onClose,
|
|
61
|
+
isOpen
|
|
62
|
+
};
|
|
63
|
+
const reset = useCallback(() => {
|
|
64
|
+
state.current.offset = 0;
|
|
65
|
+
Animated.timing(drag, {
|
|
66
|
+
toValue: 0,
|
|
67
|
+
duration: 200,
|
|
68
|
+
useNativeDriver: true
|
|
69
|
+
}).start();
|
|
70
|
+
}, [drag]);
|
|
71
|
+
const panResponder = useMemo(() => {
|
|
72
|
+
const delta = (dx, dy) => {
|
|
73
|
+
const { axis, sign } = closing[latest.current.side];
|
|
74
|
+
return {
|
|
75
|
+
main: (axis === "x" ? dx : dy) * sign,
|
|
76
|
+
cross: axis === "x" ? dy : dx
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
const bodyScrolls = () => {
|
|
80
|
+
const { axis } = closing[latest.current.side];
|
|
81
|
+
if (axis === "x") return false;
|
|
82
|
+
const { offset, content, layout } = scroll.current;
|
|
83
|
+
return latest.current.side === "bottom" ? offset > 1 : offset + layout < content - 1;
|
|
84
|
+
};
|
|
85
|
+
return PanResponder.create({
|
|
86
|
+
onMoveShouldSetPanResponderCapture: (_, g) => {
|
|
87
|
+
if (!latest.current.enabled) return false;
|
|
88
|
+
const { main, cross } = delta(g.dx, g.dy);
|
|
89
|
+
if (main <= SLOP || Math.abs(main) <= Math.abs(cross) || bodyScrolls()) return false;
|
|
90
|
+
return true;
|
|
91
|
+
},
|
|
92
|
+
onPanResponderTerminationRequest: () => false,
|
|
93
|
+
onPanResponderMove: (_, g) => {
|
|
94
|
+
const { main } = delta(g.dx, g.dy);
|
|
95
|
+
state.current.offset = Math.max(0, main);
|
|
96
|
+
drag.setValue(state.current.offset);
|
|
97
|
+
},
|
|
98
|
+
onPanResponderRelease: (_, g) => {
|
|
99
|
+
const { axis, sign } = closing[latest.current.side];
|
|
100
|
+
const velocity = (axis === "x" ? g.vx : g.vy) * sign;
|
|
101
|
+
if (state.current.offset > size.current * .4 || velocity > .5) {
|
|
102
|
+
latest.current.onClose();
|
|
103
|
+
requestAnimationFrame(() => latest.current.isOpen() && reset());
|
|
104
|
+
} else reset();
|
|
105
|
+
},
|
|
106
|
+
onPanResponderTerminate: reset
|
|
107
|
+
});
|
|
108
|
+
}, [drag, reset]);
|
|
109
|
+
return {
|
|
110
|
+
drag,
|
|
111
|
+
dragFade: drag.interpolate({
|
|
112
|
+
inputRange: [0, Math.max(panelSize, 1)],
|
|
113
|
+
outputRange: [1, 0],
|
|
114
|
+
extrapolate: "clamp"
|
|
115
|
+
}),
|
|
116
|
+
panHandlers: panResponder.panHandlers,
|
|
117
|
+
onPanelLayout: (e) => {
|
|
118
|
+
const { width, height } = e.nativeEvent.layout;
|
|
119
|
+
const next = (closing[latest.current.side].axis === "x" ? width : height) || size.current;
|
|
120
|
+
size.current = next;
|
|
121
|
+
setPanelSize(next);
|
|
122
|
+
},
|
|
123
|
+
scrollProps: {
|
|
124
|
+
scrollEventThrottle: 16,
|
|
125
|
+
onScroll: (e) => {
|
|
126
|
+
scroll.current.offset = e.nativeEvent.contentOffset.y;
|
|
127
|
+
scroll.current.content = e.nativeEvent.contentSize.height;
|
|
128
|
+
scroll.current.layout = e.nativeEvent.layoutMeasurement.height;
|
|
129
|
+
},
|
|
130
|
+
onContentSizeChange: (_w, h) => {
|
|
131
|
+
scroll.current.content = h;
|
|
132
|
+
},
|
|
133
|
+
onLayout: (e) => {
|
|
134
|
+
scroll.current.layout = e.nativeEvent.layout.height;
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
resetDrag: useCallback(() => drag.setValue(0), [drag])
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
//#endregion
|
|
141
|
+
export { CLOSE_DISTANCE, CLOSE_VELOCITY, useSwipeToClose };
|
|
142
|
+
|
|
143
|
+
//# sourceMappingURL=use-swipe-to-close.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-swipe-to-close.js","names":[],"sources":["../../../src/components/drawer/use-swipe-to-close.ts"],"sourcesContent":["import { useCallback, useMemo, useRef, useState } from 'react'\nimport {\n Animated,\n type LayoutChangeEvent,\n type NativeScrollEvent,\n type NativeSyntheticEvent,\n PanResponder,\n} from 'react-native'\n\nexport type Side = 'top' | 'right' | 'bottom' | 'left'\n\n/** Eixo do arraste e sinal do movimento do dedo que fecha (em direção à borda do `side`). */\nconst closing = {\n right: { axis: 'x', sign: 1 },\n left: { axis: 'x', sign: -1 },\n bottom: { axis: 'y', sign: 1 },\n top: { axis: 'y', sign: -1 },\n} as const\n\n/** Fração do painel que precisa sair, ou a velocidade (px/ms) do gesto, para soltar e fechar. */\nexport const CLOSE_DISTANCE = 0.4\nexport const CLOSE_VELOCITY = 0.5\n/** Movimento mínimo (px) antes de o painel tomar o gesto. */\nconst SLOP = 6\n\ntype Options = {\n side: Side\n enabled: boolean\n /** Tamanho de reserva (px) do painel no eixo do arraste, até o `onLayout` medir. */\n fallbackSize: number\n onClose: () => void\n /** O painel ainda está aberto? (controlado, o `onClose` pode ser ignorado: aí o painel volta). */\n isOpen: () => boolean\n}\n\n/**\n * Fecha o painel arrastando-o para a borda de onde ele veio (mesmo comportamento do web).\n *\n * `PanResponder` na fase de captura, para tomar o gesto do `ScrollView` do corpo, mas só quando o\n * dedo vai no sentido de fechar, o gesto é mais ao longo do eixo do que de lado e o corpo não rola\n * mais naquele sentido (no `bottom`, rolagem no topo; no `top`, no fim). Como a captura é refeita a\n * cada movimento, um gesto que começou rolando passa ao painel quando o conteúdo chega à borda; o\n * arraste conta daí (o grant zera `dx`/`dy`), sem salto. O `Slider` não perde o toque: ele não aceita ser\n * tomado (`onResponderTerminationRequest` retorna `false`).\n *\n * Devolve `drag` (px arrastados, ≥ 0) e `dragFade` (1 → 0 conforme o painel sai, para o backdrop),\n * os `panHandlers` do painel, o `onLayout` que mede o painel e as props do `ScrollView` do corpo.\n */\nexport function useSwipeToClose({ side, enabled, fallbackSize, onClose, isOpen }: Options) {\n const drag = useRef(new Animated.Value(0)).current\n const [panelSize, setPanelSize] = useState(fallbackSize)\n const size = useRef(fallbackSize)\n const scroll = useRef({ offset: 0, content: 0, layout: 0 })\n const state = useRef({ offset: 0 })\n // `PanResponder.create` roda uma vez: o que muda a cada render vai por aqui.\n const latest = useRef({ side, enabled, onClose, isOpen })\n latest.current = { side, enabled, onClose, isOpen }\n\n const reset = useCallback(() => {\n state.current.offset = 0\n Animated.timing(drag, { toValue: 0, duration: 200, useNativeDriver: true }).start()\n }, [drag])\n\n const panResponder = useMemo(() => {\n const delta = (dx: number, dy: number) => {\n const { axis, sign } = closing[latest.current.side]\n return { main: (axis === 'x' ? dx : dy) * sign, cross: axis === 'x' ? dy : dx }\n }\n // o corpo ainda rola no sentido de fechar? Então o dedo é da rolagem, não do painel\n const bodyScrolls = () => {\n const { axis } = closing[latest.current.side]\n if (axis === 'x') return false // o corpo rola na vertical\n const { offset, content, layout } = scroll.current\n return latest.current.side === 'bottom' ? offset > 1 : offset + layout < content - 1\n }\n\n return PanResponder.create({\n onMoveShouldSetPanResponderCapture: (_, g) => {\n if (!latest.current.enabled) return false\n const { main, cross } = delta(g.dx, g.dy)\n if (main <= SLOP || Math.abs(main) <= Math.abs(cross) || bodyScrolls()) return false\n return true\n },\n onPanResponderTerminationRequest: () => false,\n onPanResponderMove: (_, g) => {\n const { main } = delta(g.dx, g.dy)\n // o `PanResponder` zera `dx`/`dy` no grant: o arraste conta de onde o painel assumiu\n state.current.offset = Math.max(0, main)\n drag.setValue(state.current.offset)\n },\n onPanResponderRelease: (_, g) => {\n const { axis, sign } = closing[latest.current.side]\n const velocity = (axis === 'x' ? g.vx : g.vy) * sign\n if (state.current.offset > size.current * CLOSE_DISTANCE || velocity > CLOSE_VELOCITY) {\n // a saída continua de onde o painel foi solto; `drag` zera quando o Modal desmonta\n latest.current.onClose()\n requestAnimationFrame(() => latest.current.isOpen() && reset())\n } else reset()\n },\n onPanResponderTerminate: reset,\n })\n }, [drag, reset])\n\n return {\n drag,\n // o backdrop clareia à medida que o painel sai\n dragFade: drag.interpolate({\n inputRange: [0, Math.max(panelSize, 1)],\n outputRange: [1, 0],\n extrapolate: 'clamp',\n }),\n panHandlers: panResponder.panHandlers,\n onPanelLayout: (e: LayoutChangeEvent) => {\n const { width, height } = e.nativeEvent.layout\n const next = (closing[latest.current.side].axis === 'x' ? width : height) || size.current\n size.current = next\n setPanelSize(next)\n },\n scrollProps: {\n scrollEventThrottle: 16,\n onScroll: (e: NativeSyntheticEvent<NativeScrollEvent>) => {\n scroll.current.offset = e.nativeEvent.contentOffset.y\n scroll.current.content = e.nativeEvent.contentSize.height\n scroll.current.layout = e.nativeEvent.layoutMeasurement.height\n },\n onContentSizeChange: (_w: number, h: number) => {\n scroll.current.content = h\n },\n onLayout: (e: LayoutChangeEvent) => {\n scroll.current.layout = e.nativeEvent.layout.height\n },\n },\n resetDrag: useCallback(() => drag.setValue(0), [drag]),\n }\n}\n"],"mappings":"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"}
|
|
@@ -12,9 +12,15 @@ export type InputProps = Omit<TextInputProps, 'style' | 'editable'> & Omit<Varia
|
|
|
12
12
|
/** Borda e foco em vermelho. */
|
|
13
13
|
error?: boolean;
|
|
14
14
|
disabled?: boolean;
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Ícone antes do texto (o consumidor define a cor; o slot tem 24px). Pode ser interativo
|
|
17
|
+
* (um `Pressable` recebe o próprio toque); se não tratar o toque, ele foca o campo.
|
|
18
|
+
*/
|
|
16
19
|
leadingIcon?: ReactNode;
|
|
17
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Ícone depois do texto (o consumidor define a cor; o slot tem 24px). Pode ser interativo
|
|
22
|
+
* (ex.: um `Pressable` que mostra/oculta a senha); se não tratar o toque, ele foca o campo.
|
|
23
|
+
*/
|
|
18
24
|
trailingIcon?: ReactNode;
|
|
19
25
|
};
|
|
20
26
|
export declare function Input({ className, size, error, disabled, readOnly, leadingIcon, trailingIcon, accessibilityState, ...props }: InputProps): import("react").JSX.Element;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cn } from "../../utils/cn.js";
|
|
2
2
|
import { useToken } from "../_shared/use-token.js";
|
|
3
|
+
import { useRef } from "react";
|
|
3
4
|
import { TextInput, View } from "react-native";
|
|
4
5
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
6
|
import { cva } from "class-variance-authority";
|
|
@@ -48,7 +49,14 @@ const iconSlot = {
|
|
|
48
49
|
function Input({ className, size, error = false, disabled = false, readOnly = false, leadingIcon, trailingIcon, accessibilityState, ...props }) {
|
|
49
50
|
const placeholderColor = useToken("--color-field-text-placeholder");
|
|
50
51
|
const slot = iconSlot[size ?? "md"];
|
|
52
|
+
const ref = useRef(null);
|
|
53
|
+
const iconTouch = {
|
|
54
|
+
pointerEvents: "box-none",
|
|
55
|
+
onStartShouldSetResponder: () => !disabled,
|
|
56
|
+
onResponderRelease: () => ref.current?.focus()
|
|
57
|
+
};
|
|
51
58
|
const input = /* @__PURE__ */ jsx(TextInput, {
|
|
59
|
+
ref,
|
|
52
60
|
editable: !disabled && !readOnly,
|
|
53
61
|
readOnly,
|
|
54
62
|
placeholderTextColor: placeholderColor,
|
|
@@ -67,13 +75,13 @@ function Input({ className, size, error = false, disabled = false, readOnly = fa
|
|
|
67
75
|
className: "justify-center",
|
|
68
76
|
children: [
|
|
69
77
|
leadingIcon ? /* @__PURE__ */ jsx(View, {
|
|
70
|
-
|
|
78
|
+
...iconTouch,
|
|
71
79
|
className: cn("absolute inset-y-0 z-10 items-center justify-center", slot.icon, slot.left),
|
|
72
80
|
children: leadingIcon
|
|
73
81
|
}) : null,
|
|
74
82
|
input,
|
|
75
83
|
trailingIcon ? /* @__PURE__ */ jsx(View, {
|
|
76
|
-
|
|
84
|
+
...iconTouch,
|
|
77
85
|
className: cn("absolute inset-y-0 z-10 items-center justify-center", slot.icon, slot.right),
|
|
78
86
|
children: trailingIcon
|
|
79
87
|
}) : null
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"input.js","names":[],"sources":["../../../src/components/input/input.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport type
|
|
1
|
+
{"version":3,"file":"input.js","names":[],"sources":["../../../src/components/input/input.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { type ReactNode, useRef } from 'react'\nimport { TextInput, type TextInputProps, View } from 'react-native'\nimport { cn } from '../../utils/cn'\nimport { useToken } from '../_shared/use-token'\n\n/** Estilo do campo, compartilhado com o Textarea (não faz parte da API pública). */\nexport const inputVariants = cva(\n 'w-full rounded-md border border-field-border bg-field-bg py-0 text-field-text focus:border-field-focus-ring',\n {\n variants: {\n size: {\n lg: 'h-14 px-4 text-base',\n md: 'h-12 px-3 text-base',\n sm: 'h-10 px-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\n// Posição do ícone e recuo do texto por tamanho (Figma: padding + ícone 24/20/24 + gap 8/8/4).\nconst iconSlot = {\n lg: { icon: 'w-5', left: 'left-4', right: 'right-4', padL: 'pl-11', padR: 'pr-11' },\n md: { icon: 'w-6', left: 'left-3', right: 'right-3', padL: 'pl-11', padR: 'pr-11' },\n sm: { icon: 'w-6', left: 'left-2', right: 'right-2', padL: 'pl-9', padR: 'pr-9' },\n}\n\nexport type InputProps = Omit<TextInputProps, 'style' | 'editable'> &\n Omit<VariantProps<typeof inputVariants>, 'error'> & {\n className?: string\n /** Borda e foco em vermelho. */\n error?: boolean\n disabled?: boolean\n /**\n * Ícone antes do texto (o consumidor define a cor; o slot tem 24px). Pode ser interativo\n * (um `Pressable` recebe o próprio toque); se não tratar o toque, ele foca o campo.\n */\n leadingIcon?: ReactNode\n /**\n * Ícone depois do texto (o consumidor define a cor; o slot tem 24px). Pode ser interativo\n * (ex.: um `Pressable` que mostra/oculta a senha); se não tratar o toque, ele foca o campo.\n */\n trailingIcon?: ReactNode\n }\n\nexport function Input({\n className,\n size,\n error = false,\n disabled = false,\n readOnly = false,\n leadingIcon,\n trailingIcon,\n accessibilityState,\n ...props\n}: InputProps) {\n const placeholderColor = useToken('--color-field-text-placeholder')\n const slot = iconSlot[size ?? 'md']\n const ref = useRef<TextInput>(null)\n // `box-none`: o slot não intercepta o toque (a área vazia cai no campo) mas os filhos recebem o\n // deles, então um `Pressable` no ícone funciona. Um ícone que não trata o toque sobe para o slot,\n // que foca o campo (o `pointerEvents=\"none\"` de antes dava isso, mas bloqueava o `Pressable`).\n const iconTouch = {\n pointerEvents: 'box-none' as const,\n onStartShouldSetResponder: () => !disabled,\n onResponderRelease: () => ref.current?.focus(),\n }\n\n const input = (\n <TextInput\n ref={ref}\n editable={!disabled && !readOnly}\n readOnly={readOnly}\n placeholderTextColor={placeholderColor}\n accessibilityState={{ ...accessibilityState, disabled }}\n className={cn(\n inputVariants({ 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 leadingIcon && slot.padL,\n trailingIcon && slot.padR,\n className,\n )}\n {...props}\n />\n )\n\n if (!leadingIcon && !trailingIcon) return input\n\n return (\n <View className=\"justify-center\">\n {leadingIcon ? (\n <View\n {...iconTouch}\n className={cn(\n 'absolute inset-y-0 z-10 items-center justify-center',\n slot.icon,\n slot.left,\n )}\n >\n {leadingIcon}\n </View>\n ) : null}\n {input}\n {trailingIcon ? (\n <View\n {...iconTouch}\n className={cn(\n 'absolute inset-y-0 z-10 items-center justify-center',\n slot.icon,\n slot.right,\n )}\n >\n {trailingIcon}\n </View>\n ) : null}\n </View>\n )\n}\n"],"mappings":";;;;;;;;AASA,MAAa,gBAAgB,IAC3B,+GACA;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;AAGA,MAAM,WAAW;CACf,IAAI;EAAE,MAAM;EAAO,MAAM;EAAU,OAAO;EAAW,MAAM;EAAS,MAAM;CAAQ;CAClF,IAAI;EAAE,MAAM;EAAO,MAAM;EAAU,OAAO;EAAW,MAAM;EAAS,MAAM;CAAQ;CAClF,IAAI;EAAE,MAAM;EAAO,MAAM;EAAU,OAAO;EAAW,MAAM;EAAQ,MAAM;CAAO;AAClF;AAoBA,SAAgB,MAAM,EACpB,WACA,MACA,QAAQ,OACR,WAAW,OACX,WAAW,OACX,aACA,cACA,oBACA,GAAG,SACU;CACb,MAAM,mBAAmB,SAAS,gCAAgC;CAClE,MAAM,OAAO,SAAS,QAAQ;CAC9B,MAAM,MAAM,OAAkB,IAAI;CAIlC,MAAM,YAAY;EAChB,eAAe;EACf,iCAAiC,CAAC;EAClC,0BAA0B,IAAI,SAAS,MAAM;CAC/C;CAEA,MAAM,QACJ,oBAAC,WAAD;EACO;EACL,UAAU,CAAC,YAAY,CAAC;EACd;EACV,sBAAsB;EACtB,oBAAoB;GAAE,GAAG;GAAoB;EAAS;EACtD,WAAW,GACT,cAAc;GAAE;GAAM;EAAM,CAAC,GAC7B,YACE,iHACF,YAAY,CAAC,YAAY,oEACzB,eAAe,KAAK,MACpB,gBAAgB,KAAK,MACrB,SACF;EACA,GAAI;CACL,CAAA;CAGH,IAAI,CAAC,eAAe,CAAC,cAAc,OAAO;CAE1C,OACE,qBAAC,MAAD;EAAM,WAAU;EAAhB,UAAA;GACG,cACC,oBAAC,MAAD;IACE,GAAI;IACJ,WAAW,GACT,uDACA,KAAK,MACL,KAAK,IACP;IAEC,UAAA;GACG,CAAA,IACJ;GACH;GACA,eACC,oBAAC,MAAD;IACE,GAAI;IACJ,WAAW,GACT,uDACA,KAAK,MACL,KAAK,KACP;IAEC,UAAA;GACG,CAAA,IACJ;EACA;;AAEV"}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { cn } from "../../utils/cn.js";
|
|
2
2
|
import { CloseIcon } from "../_shared/status.js";
|
|
3
3
|
import { useToken } from "../_shared/use-token.js";
|
|
4
|
+
import { useTransition } from "../_shared/use-transition.js";
|
|
4
5
|
import { Backdrop } from "../_shared/backdrop.js";
|
|
5
6
|
import { injectHandler, useOpenState } from "../_shared/overlay-state.js";
|
|
6
7
|
import { createContext, useContext } from "react";
|
|
7
|
-
import { Modal, Pressable, ScrollView, Text, View } from "react-native";
|
|
8
|
+
import { Animated, Modal, Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
|
|
8
9
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
9
10
|
import { cva } from "class-variance-authority";
|
|
10
11
|
//#region src/components/modal/modal.tsx
|
|
@@ -30,52 +31,71 @@ function Modal$1({ open, defaultOpen, onOpenChange, title, children, footer, tri
|
|
|
30
31
|
});
|
|
31
32
|
const close = () => setOpen(false);
|
|
32
33
|
const iconColor = useToken("--color-text-primary");
|
|
34
|
+
const { mounted, progress } = useTransition(isOpen);
|
|
35
|
+
const scale = progress.interpolate({
|
|
36
|
+
inputRange: [0, 1],
|
|
37
|
+
outputRange: [.95, 1]
|
|
38
|
+
});
|
|
33
39
|
return /* @__PURE__ */ jsxs(Fragment$1, { children: [injectHandler(trigger, "onPress", () => setOpen(true)), /* @__PURE__ */ jsx(Modal, {
|
|
34
40
|
transparent: true,
|
|
35
41
|
statusBarTranslucent: true,
|
|
36
|
-
animationType: "
|
|
37
|
-
visible:
|
|
42
|
+
animationType: "none",
|
|
43
|
+
visible: mounted,
|
|
38
44
|
onRequestClose: close,
|
|
39
45
|
children: /* @__PURE__ */ jsx(CloseContext.Provider, {
|
|
40
46
|
value: close,
|
|
41
47
|
children: /* @__PURE__ */ jsxs(View, {
|
|
42
|
-
className: "flex-1
|
|
48
|
+
className: "flex-1",
|
|
43
49
|
accessibilityViewIsModal: true,
|
|
44
|
-
children: [/* @__PURE__ */ jsx(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
50
|
+
children: [/* @__PURE__ */ jsx(Animated.View, {
|
|
51
|
+
pointerEvents: "box-none",
|
|
52
|
+
style: [StyleSheet.absoluteFill, { opacity: progress }],
|
|
53
|
+
children: /* @__PURE__ */ jsx(Backdrop, {
|
|
54
|
+
onPress: close,
|
|
55
|
+
testID: testID ? `${testID}-backdrop` : void 0
|
|
56
|
+
})
|
|
57
|
+
}), /* @__PURE__ */ jsx(Animated.View, {
|
|
58
|
+
pointerEvents: "box-none",
|
|
59
|
+
style: [StyleSheet.absoluteFill, {
|
|
60
|
+
alignItems: "center",
|
|
61
|
+
justifyContent: "center",
|
|
62
|
+
padding: 16,
|
|
63
|
+
opacity: progress,
|
|
64
|
+
transform: [{ scale }]
|
|
65
|
+
}],
|
|
66
|
+
children: /* @__PURE__ */ jsxs(View, {
|
|
67
|
+
testID,
|
|
68
|
+
className: cn(modalVariants({ size }), className),
|
|
69
|
+
children: [
|
|
70
|
+
/* @__PURE__ */ jsxs(View, {
|
|
71
|
+
className: cn("flex-row items-center gap-4 px-6 py-4", showHeaderDivider && "border-b border-border-default"),
|
|
72
|
+
children: [/* @__PURE__ */ jsx(Text, {
|
|
73
|
+
accessibilityRole: "header",
|
|
74
|
+
className: "flex-1 text-lg font-semibold leading-6 text-text-primary",
|
|
75
|
+
children: title
|
|
76
|
+
}), showClose && /* @__PURE__ */ jsx(Pressable, {
|
|
77
|
+
accessibilityRole: "button",
|
|
78
|
+
accessibilityLabel: closeLabel,
|
|
79
|
+
hitSlop: 8,
|
|
80
|
+
onPress: close,
|
|
81
|
+
className: "size-8 items-center justify-center rounded-full active:bg-button-secondary-bg-hover",
|
|
82
|
+
children: /* @__PURE__ */ jsx(CloseIcon, { color: iconColor })
|
|
83
|
+
})]
|
|
84
|
+
}),
|
|
85
|
+
/* @__PURE__ */ jsx(ScrollView, {
|
|
86
|
+
className: "shrink",
|
|
87
|
+
contentContainerClassName: "gap-4 p-6",
|
|
88
|
+
children: typeof children === "string" ? /* @__PURE__ */ jsx(Text, {
|
|
89
|
+
className: "text-sm leading-5 text-text-secondary",
|
|
90
|
+
children
|
|
91
|
+
}) : children
|
|
92
|
+
}),
|
|
93
|
+
footer && /* @__PURE__ */ jsx(View, {
|
|
94
|
+
className: cn("flex-row items-center justify-end gap-2 px-6 py-4", showFooterDivider && "border-t border-border-default"),
|
|
95
|
+
children: footer
|
|
96
|
+
})
|
|
97
|
+
]
|
|
98
|
+
})
|
|
79
99
|
})]
|
|
80
100
|
})
|
|
81
101
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"modal.js","names":["Modal","RNModal"],"sources":["../../../src/components/modal/modal.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { createContext, type ReactNode, useContext } from 'react'\nimport {
|
|
1
|
+
{"version":3,"file":"modal.js","names":["Modal","RNModal"],"sources":["../../../src/components/modal/modal.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { createContext, type ReactNode, useContext } from 'react'\nimport {\n Animated,\n Pressable,\n Modal as RNModal,\n ScrollView,\n StyleSheet,\n Text,\n View,\n} from 'react-native'\nimport { cn } from '../../utils/cn'\nimport { Backdrop } from '../_shared/backdrop'\nimport { injectHandler, useOpenState } from '../_shared/overlay-state'\nimport { CloseIcon } from '../_shared/status'\nimport { useToken } from '../_shared/use-token'\nimport { useTransition } from '../_shared/use-transition'\n\nconst modalVariants = cva(\n 'max-h-full w-full overflow-hidden rounded-lg border border-border-default bg-surface-elevated shadow-2xl',\n {\n variants: {\n size: { sm: 'max-w-[480px]', md: 'max-w-[640px]', lg: 'max-w-[800px]' },\n },\n defaultVariants: { size: 'sm' },\n },\n)\n\nconst CloseContext = createContext<(() => void) | null>(null)\n\nexport type ModalProps = VariantProps<typeof modalVariants> & {\n open?: boolean\n defaultOpen?: boolean\n onOpenChange?: (open: boolean) => void\n /** Título do diálogo; também é o nome acessível dele. */\n title: ReactNode\n /** O corpo do diálogo. */\n children: ReactNode\n /** Ações do rodapé (primária à direita). Use `ModalClose` para fechar. Sem ele, não há rodapé. */\n footer?: ReactNode\n /** Elemento que abre o modal (ex.: um `<Button>`); recebe `onPress`. Dispensável no modo controlado. */\n trigger?: ReactNode\n /** Mostra o botão de fechar (×) no cabeçalho. */\n showClose?: boolean\n /** Linha entre o cabeçalho e o corpo. Padrão: `true`. */\n showHeaderDivider?: boolean\n /** Linha entre o corpo e o rodapé (só aparece com `footer`). Padrão: `true`. */\n showFooterDivider?: boolean\n /** Nome acessível do botão de fechar. */\n closeLabel?: string\n className?: string\n testID?: string\n}\n\n/**\n * Diálogo que bloqueia o conteúdo de trás, com backdrop. Usa o `Modal` nativo: o botão voltar do\n * Android e o toque no fundo fecham. Não há foco preso nem `Esc`; `accessibilityViewIsModal`\n * mantém o leitor de tela dentro do diálogo.\n */\nexport function Modal({\n open,\n defaultOpen,\n onOpenChange,\n title,\n children,\n footer,\n trigger,\n showClose = true,\n showHeaderDivider = true,\n showFooterDivider = true,\n closeLabel = 'Fechar',\n size,\n className,\n testID,\n}: ModalProps) {\n const [isOpen, setOpen] = useOpenState({ open, defaultOpen, onOpenChange })\n const close = () => setOpen(false)\n const iconColor = useToken('--color-text-primary')\n // Como no web: o fundo faz fade e o diálogo faz fade com zoom de 95%. O `Modal` nativo só fecha\n // depois da saída, então a animação é nossa (`animationType=\"none\"`).\n const { mounted, progress } = useTransition(isOpen)\n const scale = progress.interpolate({ inputRange: [0, 1], outputRange: [0.95, 1] })\n\n return (\n <>\n {injectHandler(trigger, 'onPress', () => setOpen(true))}\n <RNModal\n transparent\n statusBarTranslucent\n animationType=\"none\"\n visible={mounted}\n onRequestClose={close}\n >\n <CloseContext.Provider value={close}>\n <View className=\"flex-1\" accessibilityViewIsModal>\n <Animated.View\n pointerEvents=\"box-none\"\n style={[StyleSheet.absoluteFill, { opacity: progress }]}\n >\n <Backdrop onPress={close} testID={testID ? `${testID}-backdrop` : undefined} />\n </Animated.View>\n <Animated.View\n pointerEvents=\"box-none\"\n style={[\n StyleSheet.absoluteFill,\n {\n alignItems: 'center',\n justifyContent: 'center',\n padding: 16,\n opacity: progress,\n transform: [{ scale }],\n },\n ]}\n >\n <View testID={testID} className={cn(modalVariants({ size }), className)}>\n <View\n className={cn(\n 'flex-row items-center gap-4 px-6 py-4',\n showHeaderDivider && 'border-b border-border-default',\n )}\n >\n <Text\n accessibilityRole=\"header\"\n className=\"flex-1 text-lg font-semibold leading-6 text-text-primary\"\n >\n {title}\n </Text>\n {showClose && (\n <Pressable\n accessibilityRole=\"button\"\n accessibilityLabel={closeLabel}\n hitSlop={8}\n onPress={close}\n className=\"size-8 items-center justify-center rounded-full active:bg-button-secondary-bg-hover\"\n >\n <CloseIcon color={iconColor} />\n </Pressable>\n )}\n </View>\n <ScrollView className=\"shrink\" contentContainerClassName=\"gap-4 p-6\">\n {typeof children === 'string' ? (\n <Text className=\"text-sm leading-5 text-text-secondary\">{children}</Text>\n ) : (\n children\n )}\n </ScrollView>\n {footer && (\n <View\n className={cn(\n 'flex-row items-center justify-end gap-2 px-6 py-4',\n showFooterDivider && 'border-t border-border-default',\n )}\n >\n {footer}\n </View>\n )}\n </View>\n </Animated.View>\n </View>\n </CloseContext.Provider>\n </RNModal>\n </>\n )\n}\n\n/** Fecha o modal ao pressionar. Envolva um `<Button>` do rodapé (injeta `onPress`, como o `asChild`). */\nexport function ModalClose({ children }: { children: ReactNode }) {\n const close = useContext(CloseContext)\n return <>{injectHandler(children, 'onPress', () => close?.())}</>\n}\n"],"mappings":";;;;;;;;;;;AAoBA,MAAM,gBAAgB,IACpB,4GACA;CACE,UAAU,EACR,MAAM;EAAE,IAAI;EAAiB,IAAI;EAAiB,IAAI;CAAgB,EACxE;CACA,iBAAiB,EAAE,MAAM,KAAK;AAChC,CACF;AAEA,MAAM,eAAe,cAAmC,IAAI;;;;;;AA+B5D,SAAgBA,QAAM,EACpB,MACA,aACA,cACA,OACA,UACA,QACA,SACA,YAAY,MACZ,oBAAoB,MACpB,oBAAoB,MACpB,aAAa,UACb,MACA,WACA,UACa;CACb,MAAM,CAAC,QAAQ,WAAW,aAAa;EAAE;EAAM;EAAa;CAAa,CAAC;CAC1E,MAAM,cAAc,QAAQ,KAAK;CACjC,MAAM,YAAY,SAAS,sBAAsB;CAGjD,MAAM,EAAE,SAAS,aAAa,cAAc,MAAM;CAClD,MAAM,QAAQ,SAAS,YAAY;EAAE,YAAY,CAAC,GAAG,CAAC;EAAG,aAAa,CAAC,KAAM,CAAC;CAAE,CAAC;CAEjF,OACE,qBAAA,YAAA,EAAA,UAAA,CACG,cAAc,SAAS,iBAAiB,QAAQ,IAAI,CAAC,GACtD,oBAACC,OAAD;EACE,aAAA;EACA,sBAAA;EACA,eAAc;EACd,SAAS;EACT,gBAAgB;EAEhB,UAAA,oBAAC,aAAa,UAAd;GAAuB,OAAO;GAC5B,UAAA,qBAAC,MAAD;IAAM,WAAU;IAAS,0BAAA;IAAzB,UAAA,CACE,oBAAC,SAAS,MAAV;KACE,eAAc;KACd,OAAO,CAAC,WAAW,cAAc,EAAE,SAAS,SAAS,CAAC;KAEtD,UAAA,oBAAC,UAAD;MAAU,SAAS;MAAO,QAAQ,SAAS,GAAG,OAAO,aAAa,KAAA;KAAY,CAAA;IACjE,CAAA,GACf,oBAAC,SAAS,MAAV;KACE,eAAc;KACd,OAAO,CACL,WAAW,cACX;MACE,YAAY;MACZ,gBAAgB;MAChB,SAAS;MACT,SAAS;MACT,WAAW,CAAC,EAAE,MAAM,CAAC;KACvB,CACF;KAEA,UAAA,qBAAC,MAAD;MAAc;MAAQ,WAAW,GAAG,cAAc,EAAE,KAAK,CAAC,GAAG,SAAS;MAAtE,UAAA;OACE,qBAAC,MAAD;QACE,WAAW,GACT,yCACA,qBAAqB,gCACvB;QAJF,UAAA,CAME,oBAAC,MAAD;SACE,mBAAkB;SAClB,WAAU;SAET,UAAA;QACG,CAAA,GACL,aACC,oBAAC,WAAD;SACE,mBAAkB;SAClB,oBAAoB;SACpB,SAAS;SACT,SAAS;SACT,WAAU;SAEV,UAAA,oBAAC,WAAD,EAAW,OAAO,UAAY,CAAA;QACrB,CAAA,CAET;;OACN,oBAAC,YAAD;QAAY,WAAU;QAAS,2BAA0B;QACtD,UAAA,OAAO,aAAa,WACnB,oBAAC,MAAD;SAAM,WAAU;SAAyC;QAAe,CAAA,IAExE;OAEQ,CAAA;OACX,UACC,oBAAC,MAAD;QACE,WAAW,GACT,qDACA,qBAAqB,gCACvB;QAEC,UAAA;OACG,CAAA;MAEJ;;IACO,CAAA,CACX;;EACe,CAAA;CAChB,CAAA,CACT,EAAA,CAAA;AAEN;;AAGA,SAAgB,WAAW,EAAE,YAAqC;CAChE,MAAM,QAAQ,WAAW,YAAY;CACrC,OAAO,oBAAA,YAAA,EAAA,UAAG,cAAc,UAAU,iBAAiB,QAAQ,CAAC,EAAI,CAAA;AAClE"}
|
|
@@ -12,12 +12,12 @@ export type SegmentedControlProps = Omit<ViewProps, 'style'> & {
|
|
|
12
12
|
children: ReactNode;
|
|
13
13
|
};
|
|
14
14
|
/** Escolha única entre 2–5 views do mesmo conteúdo. Para navegar entre seções, use Tabs. */
|
|
15
|
-
export declare function SegmentedControl({ className, value, defaultValue, onValueChange, ...props }: SegmentedControlProps): import("react").JSX.Element;
|
|
15
|
+
export declare function SegmentedControl({ className, value, defaultValue, onValueChange, children, ...props }: SegmentedControlProps): import("react").JSX.Element;
|
|
16
16
|
export type SegmentedItemProps = Omit<PressableProps, 'style' | 'children' | 'onPress'> & {
|
|
17
17
|
className?: string;
|
|
18
18
|
value: string;
|
|
19
19
|
children: ReactNode;
|
|
20
20
|
};
|
|
21
|
-
export declare function SegmentedItem({ className, value, children, disabled, accessibilityState, ...props }: SegmentedItemProps): import("react").JSX.Element;
|
|
21
|
+
export declare function SegmentedItem({ className, value, children, disabled, accessibilityState, onLayout, ...props }: SegmentedItemProps): import("react").JSX.Element;
|
|
22
22
|
//#endregion
|
|
23
23
|
//# sourceMappingURL=segmented-control.d.ts.map
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { cn } from "../../utils/cn.js";
|
|
2
|
+
import { SlidingIndicator, useItemLayouts } from "../_shared/sliding-indicator.js";
|
|
2
3
|
import { createContext, useContext, useState } from "react";
|
|
3
4
|
import { Pressable, Text, View } from "react-native";
|
|
4
|
-
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
6
|
//#region src/components/segmented-control/segmented-control.tsx
|
|
6
7
|
const Context = createContext(null);
|
|
7
8
|
/** Escolha única entre 2–5 views do mesmo conteúdo. Para navegar entre seções, use Tabs. */
|
|
8
|
-
function SegmentedControl({ className, value, defaultValue, onValueChange, ...props }) {
|
|
9
|
+
function SegmentedControl({ className, value, defaultValue, onValueChange, children, ...props }) {
|
|
9
10
|
const [uncontrolled, setUncontrolled] = useState(defaultValue);
|
|
10
11
|
const current = value ?? uncontrolled;
|
|
12
|
+
const { layouts, setLayout } = useItemLayouts();
|
|
13
|
+
const active = current === void 0 ? void 0 : layouts[current];
|
|
11
14
|
const select = (next) => {
|
|
12
15
|
if (next === current) return;
|
|
13
16
|
setUncontrolled(next);
|
|
@@ -16,16 +19,22 @@ function SegmentedControl({ className, value, defaultValue, onValueChange, ...pr
|
|
|
16
19
|
return /* @__PURE__ */ jsx(Context.Provider, {
|
|
17
20
|
value: {
|
|
18
21
|
value: current,
|
|
19
|
-
select
|
|
22
|
+
select,
|
|
23
|
+
slid: active !== void 0,
|
|
24
|
+
setLayout
|
|
20
25
|
},
|
|
21
|
-
children: /* @__PURE__ */
|
|
26
|
+
children: /* @__PURE__ */ jsxs(View, {
|
|
22
27
|
accessibilityRole: "radiogroup",
|
|
23
28
|
className: cn("flex-row items-start gap-1 self-start rounded-md bg-bg-tertiary p-1", className),
|
|
24
|
-
...props
|
|
29
|
+
...props,
|
|
30
|
+
children: [/* @__PURE__ */ jsx(SlidingIndicator, {
|
|
31
|
+
target: active,
|
|
32
|
+
className: "rounded-md bg-surface-primary shadow-sm"
|
|
33
|
+
}), children]
|
|
25
34
|
})
|
|
26
35
|
});
|
|
27
36
|
}
|
|
28
|
-
function SegmentedItem({ className, value, children, disabled, accessibilityState, ...props }) {
|
|
37
|
+
function SegmentedItem({ className, value, children, disabled, accessibilityState, onLayout, ...props }) {
|
|
29
38
|
const group = useContext(Context);
|
|
30
39
|
if (!group) throw new Error("SegmentedItem deve ser usado dentro de SegmentedControl");
|
|
31
40
|
const checked = group.value === value;
|
|
@@ -39,7 +48,11 @@ function SegmentedItem({ className, value, children, disabled, accessibilityStat
|
|
|
39
48
|
},
|
|
40
49
|
disabled: isDisabled,
|
|
41
50
|
onPress: () => group.select(value),
|
|
42
|
-
|
|
51
|
+
onLayout: (event) => {
|
|
52
|
+
group.setLayout(value, event.nativeEvent.layout);
|
|
53
|
+
onLayout?.(event);
|
|
54
|
+
},
|
|
55
|
+
className: cn("items-center justify-center rounded-md px-4 py-2", checked ? !group.slid && "bg-surface-primary shadow-sm" : "active:bg-bg-secondary", className),
|
|
43
56
|
...props,
|
|
44
57
|
children: typeof children === "string" ? /* @__PURE__ */ jsx(Text, {
|
|
45
58
|
className: cn("text-sm leading-5 text-text-secondary", checked && "font-medium text-text-primary", isDisabled && "text-text-disabled"),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"segmented-control.js","names":[],"sources":["../../../src/components/segmented-control/segmented-control.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { createContext, type ReactNode, useContext, useState } from 'react'\nimport { Pressable, type PressableProps, Text, View, type ViewProps } from 'react-native'\nimport { cn } from '../../utils/cn'\n\ntype SegmentedContext = {
|
|
1
|
+
{"version":3,"file":"segmented-control.js","names":[],"sources":["../../../src/components/segmented-control/segmented-control.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { createContext, type ReactNode, useContext, useState } from 'react'\nimport { Pressable, type PressableProps, Text, View, type ViewProps } from 'react-native'\nimport { cn } from '../../utils/cn'\nimport { type Rect, SlidingIndicator, useItemLayouts } from '../_shared/sliding-indicator'\n\ntype SegmentedContext = {\n value: string | undefined\n select: (value: string) => void\n /** O pill já mediu o item marcado: os itens deixam de desenhar o próprio fundo. */\n slid: boolean\n setLayout: (value: string, rect: Rect) => void\n}\n\nconst Context = createContext<SegmentedContext | null>(null)\n\nexport type SegmentedControlProps = Omit<ViewProps, 'style'> & {\n className?: string\n /** Obrigatório: descreve o que o controle escolhe (ex.: \"Modo de exibição\"). */\n accessibilityLabel: string\n /** Controlado. Sem ele, o componente guarda o próprio estado (começa em `defaultValue`). */\n value?: string\n defaultValue?: string\n onValueChange?: (value: string) => void\n children: ReactNode\n}\n\n/** Escolha única entre 2–5 views do mesmo conteúdo. Para navegar entre seções, use Tabs. */\nexport function SegmentedControl({\n className,\n value,\n defaultValue,\n onValueChange,\n children,\n ...props\n}: SegmentedControlProps) {\n const [uncontrolled, setUncontrolled] = useState(defaultValue)\n const current = value ?? uncontrolled\n const { layouts, setLayout } = useItemLayouts()\n const active = current === undefined ? undefined : layouts[current]\n\n const select = (next: string) => {\n if (next === current) return\n setUncontrolled(next)\n onValueChange?.(next)\n }\n\n return (\n <Context.Provider value={{ value: current, select, slid: active !== undefined, setLayout }}>\n <View\n accessibilityRole=\"radiogroup\"\n className={cn(\n 'flex-row items-start gap-1 self-start rounded-md bg-bg-tertiary p-1',\n className,\n )}\n {...props}\n >\n {/* o fundo do item marcado é do indicador: fica sob os itens e desliza para o próximo */}\n <SlidingIndicator target={active} className=\"rounded-md bg-surface-primary shadow-sm\" />\n {children}\n </View>\n </Context.Provider>\n )\n}\n\nexport type SegmentedItemProps = Omit<PressableProps, 'style' | 'children' | 'onPress'> & {\n className?: string\n value: string\n children: ReactNode\n}\n\nexport function SegmentedItem({\n className,\n value,\n children,\n disabled,\n accessibilityState,\n onLayout,\n ...props\n}: SegmentedItemProps) {\n const group = useContext(Context)\n if (!group) throw new Error('SegmentedItem deve ser usado dentro de SegmentedControl')\n\n const checked = group.value === value\n const isDisabled = Boolean(disabled)\n\n return (\n <Pressable\n accessibilityRole=\"radio\"\n accessibilityState={{ ...accessibilityState, checked, disabled: isDisabled }}\n disabled={isDisabled}\n onPress={() => group.select(value)}\n onLayout={(event) => {\n group.setLayout(value, event.nativeEvent.layout)\n onLayout?.(event)\n }}\n className={cn(\n 'items-center justify-center rounded-md px-4 py-2',\n checked ? !group.slid && 'bg-surface-primary shadow-sm' : 'active:bg-bg-secondary',\n className,\n )}\n {...props}\n >\n {typeof children === 'string' ? (\n <Text\n className={cn(\n 'text-sm leading-5 text-text-secondary',\n checked && 'font-medium text-text-primary',\n isDisabled && 'text-text-disabled',\n )}\n >\n {children}\n </Text>\n ) : (\n children\n )}\n </Pressable>\n )\n}\n"],"mappings":";;;;;;AAeA,MAAM,UAAU,cAAuC,IAAI;;AAc3D,SAAgB,iBAAiB,EAC/B,WACA,OACA,cACA,eACA,UACA,GAAG,SACqB;CACxB,MAAM,CAAC,cAAc,mBAAmB,SAAS,YAAY;CAC7D,MAAM,UAAU,SAAS;CACzB,MAAM,EAAE,SAAS,cAAc,eAAe;CAC9C,MAAM,SAAS,YAAY,KAAA,IAAY,KAAA,IAAY,QAAQ;CAE3D,MAAM,UAAU,SAAiB;EAC/B,IAAI,SAAS,SAAS;EACtB,gBAAgB,IAAI;EACpB,gBAAgB,IAAI;CACtB;CAEA,OACE,oBAAC,QAAQ,UAAT;EAAkB,OAAO;GAAE,OAAO;GAAS;GAAQ,MAAM,WAAW,KAAA;GAAW;EAAU;EACvF,UAAA,qBAAC,MAAD;GACE,mBAAkB;GAClB,WAAW,GACT,uEACA,SACF;GACA,GAAI;GANN,UAAA,CASE,oBAAC,kBAAD;IAAkB,QAAQ;IAAQ,WAAU;GAA2C,CAAA,GACtF,QACG;;CACU,CAAA;AAEtB;AAQA,SAAgB,cAAc,EAC5B,WACA,OACA,UACA,UACA,oBACA,UACA,GAAG,SACkB;CACrB,MAAM,QAAQ,WAAW,OAAO;CAChC,IAAI,CAAC,OAAO,MAAM,IAAI,MAAM,yDAAyD;CAErF,MAAM,UAAU,MAAM,UAAU;CAChC,MAAM,aAAa,QAAQ,QAAQ;CAEnC,OACE,oBAAC,WAAD;EACE,mBAAkB;EAClB,oBAAoB;GAAE,GAAG;GAAoB;GAAS,UAAU;EAAW;EAC3E,UAAU;EACV,eAAe,MAAM,OAAO,KAAK;EACjC,WAAW,UAAU;GACnB,MAAM,UAAU,OAAO,MAAM,YAAY,MAAM;GAC/C,WAAW,KAAK;EAClB;EACA,WAAW,GACT,oDACA,UAAU,CAAC,MAAM,QAAQ,iCAAiC,0BAC1D,SACF;EACA,GAAI;EAEH,UAAA,OAAO,aAAa,WACnB,oBAAC,MAAD;GACE,WAAW,GACT,yCACA,WAAW,iCACX,cAAc,oBAChB;GAEC;EACG,CAAA,IAEN;CAEO,CAAA;AAEf"}
|
|
@@ -15,7 +15,7 @@ export type TabListProps = Omit<ScrollViewProps, 'horizontal' | 'style'> & {
|
|
|
15
15
|
className?: string;
|
|
16
16
|
};
|
|
17
17
|
/** Lista de gatilhos. Rola na horizontal quando as abas não cabem na tela. */
|
|
18
|
-
export declare function TabList({ className, ...props }: TabListProps): import("react").JSX.Element;
|
|
18
|
+
export declare function TabList({ className, children, ...props }: TabListProps): import("react").JSX.Element;
|
|
19
19
|
export type TabProps = Omit<PressableProps, 'children' | 'style'> & {
|
|
20
20
|
className?: string;
|
|
21
21
|
/** Valor que liga a aba ao `TabPanel` de mesmo `value`. */
|
|
@@ -24,7 +24,7 @@ export type TabProps = Omit<PressableProps, 'children' | 'style'> & {
|
|
|
24
24
|
icon?: ReactNode;
|
|
25
25
|
children?: ReactNode;
|
|
26
26
|
};
|
|
27
|
-
export declare function Tab({ className, value, icon, children, disabled, onPress, accessibilityState, ...props }: TabProps): import("react").JSX.Element;
|
|
27
|
+
export declare function Tab({ className, value, icon, children, disabled, onPress, onLayout, accessibilityState, ...props }: TabProps): import("react").JSX.Element;
|
|
28
28
|
export type TabPanelProps = ViewProps & {
|
|
29
29
|
className?: string;
|
|
30
30
|
value: string;
|
|
@@ -1,26 +1,34 @@
|
|
|
1
1
|
import { cn } from "../../utils/cn.js";
|
|
2
|
+
import { SlidingIndicator, useItemLayouts } from "../_shared/sliding-indicator.js";
|
|
2
3
|
import { createContext, useContext, useMemo, useState } from "react";
|
|
3
4
|
import { Pressable, ScrollView, Text, View } from "react-native";
|
|
4
5
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
6
|
//#region src/components/tabs/tabs.tsx
|
|
6
7
|
const TabsContext = createContext({
|
|
7
8
|
value: void 0,
|
|
8
|
-
setValue: () => {}
|
|
9
|
+
setValue: () => {},
|
|
10
|
+
layouts: {},
|
|
11
|
+
setLayout: () => {}
|
|
9
12
|
});
|
|
10
13
|
/** Raiz das abas: controla qual `Tab` está ativa (`value`/`defaultValue`/`onValueChange`). */
|
|
11
14
|
function Tabs({ className, value, defaultValue, onValueChange, ...props }) {
|
|
12
15
|
const [internal, setInternal] = useState(defaultValue);
|
|
13
16
|
const current = value !== void 0 ? value : internal;
|
|
17
|
+
const { layouts, setLayout } = useItemLayouts();
|
|
14
18
|
const context = useMemo(() => ({
|
|
15
19
|
value: current,
|
|
16
20
|
setValue: (next) => {
|
|
17
21
|
if (value === void 0) setInternal(next);
|
|
18
22
|
onValueChange?.(next);
|
|
19
|
-
}
|
|
23
|
+
},
|
|
24
|
+
layouts,
|
|
25
|
+
setLayout
|
|
20
26
|
}), [
|
|
21
27
|
current,
|
|
22
28
|
value,
|
|
23
|
-
onValueChange
|
|
29
|
+
onValueChange,
|
|
30
|
+
layouts,
|
|
31
|
+
setLayout
|
|
24
32
|
]);
|
|
25
33
|
return /* @__PURE__ */ jsx(TabsContext.Provider, {
|
|
26
34
|
value: context,
|
|
@@ -31,19 +39,30 @@ function Tabs({ className, value, defaultValue, onValueChange, ...props }) {
|
|
|
31
39
|
});
|
|
32
40
|
}
|
|
33
41
|
/** Lista de gatilhos. Rola na horizontal quando as abas não cabem na tela. */
|
|
34
|
-
function TabList({ className, ...props }) {
|
|
35
|
-
|
|
42
|
+
function TabList({ className, children, ...props }) {
|
|
43
|
+
const { value, layouts } = useContext(TabsContext);
|
|
44
|
+
const active = value === void 0 ? void 0 : layouts[value];
|
|
45
|
+
return /* @__PURE__ */ jsxs(ScrollView, {
|
|
36
46
|
horizontal: true,
|
|
37
47
|
showsHorizontalScrollIndicator: false,
|
|
38
48
|
accessibilityRole: "tablist",
|
|
39
49
|
className: cn("grow-0", className),
|
|
40
50
|
contentContainerClassName: "items-start",
|
|
41
|
-
...props
|
|
51
|
+
...props,
|
|
52
|
+
children: [/* @__PURE__ */ jsx(SlidingIndicator, {
|
|
53
|
+
target: active && {
|
|
54
|
+
...active,
|
|
55
|
+
y: active.y + active.height - 2,
|
|
56
|
+
height: 2
|
|
57
|
+
},
|
|
58
|
+
className: "bg-interactive-primary"
|
|
59
|
+
}), children]
|
|
42
60
|
});
|
|
43
61
|
}
|
|
44
|
-
function Tab({ className, value, icon, children, disabled, onPress, accessibilityState, ...props }) {
|
|
45
|
-
const { value: active, setValue } = useContext(TabsContext);
|
|
62
|
+
function Tab({ className, value, icon, children, disabled, onPress, onLayout, accessibilityState, ...props }) {
|
|
63
|
+
const { value: active, setValue, layouts, setLayout } = useContext(TabsContext);
|
|
46
64
|
const selected = active === value;
|
|
65
|
+
const slid = active !== void 0 && layouts[active] !== void 0;
|
|
47
66
|
return /* @__PURE__ */ jsxs(Pressable, {
|
|
48
67
|
accessibilityRole: "tab",
|
|
49
68
|
accessibilityState: {
|
|
@@ -56,6 +75,10 @@ function Tab({ className, value, icon, children, disabled, onPress, accessibilit
|
|
|
56
75
|
setValue(value);
|
|
57
76
|
onPress?.(event);
|
|
58
77
|
},
|
|
78
|
+
onLayout: (event) => {
|
|
79
|
+
setLayout(value, event.nativeEvent.layout);
|
|
80
|
+
onLayout?.(event);
|
|
81
|
+
},
|
|
59
82
|
className: cn("items-start gap-[6px] px-3 pt-2", className),
|
|
60
83
|
...props,
|
|
61
84
|
children: [/* @__PURE__ */ jsxs(View, {
|
|
@@ -64,7 +87,7 @@ function Tab({ className, value, icon, children, disabled, onPress, accessibilit
|
|
|
64
87
|
className: cn("text-sm text-text-secondary", selected && "font-medium text-text-brand", disabled && "text-text-disabled"),
|
|
65
88
|
children
|
|
66
89
|
}) : children]
|
|
67
|
-
}), /* @__PURE__ */ jsx(View, { className: cn("h-0.5 self-stretch bg-transparent", selected && "bg-interactive-primary") })]
|
|
90
|
+
}), /* @__PURE__ */ jsx(View, { className: cn("h-0.5 self-stretch bg-transparent", selected && !slid && "bg-interactive-primary") })]
|
|
68
91
|
});
|
|
69
92
|
}
|
|
70
93
|
/** Painel da aba: só é montado enquanto a aba de mesmo `value` está ativa (igual ao web). */
|