@innovui/react-native 0.1.0 → 0.3.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/README.md +1 -0
- package/dist/components/_shared/anchored-overlay.js +37 -12
- package/dist/components/_shared/anchored-overlay.js.map +1 -1
- package/dist/components/_shared/media-icons.js +125 -0
- package/dist/components/_shared/media-icons.js.map +1 -0
- package/dist/components/_shared/media.js +114 -0
- package/dist/components/_shared/media.js.map +1 -0
- 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/audio-player/audio-player.d.ts +34 -0
- package/dist/components/audio-player/audio-player.js +89 -0
- package/dist/components/audio-player/audio-player.js.map +1 -0
- package/dist/components/audio-player/index.d.ts +2 -0
- package/dist/components/carousel/carousel.d.ts +34 -0
- package/dist/components/carousel/carousel.js +110 -0
- package/dist/components/carousel/carousel.js.map +1 -0
- package/dist/components/carousel/index.d.ts +2 -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/gallery/gallery.d.ts +21 -0
- package/dist/components/gallery/gallery.js +54 -0
- package/dist/components/gallery/gallery.js.map +1 -0
- package/dist/components/gallery/index.d.ts +2 -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/lightbox/lightbox.d.ts +30 -0
- package/dist/components/lightbox/lightbox.js +147 -0
- package/dist/components/lightbox/lightbox.js.map +1 -0
- 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/dist/components/video-player/index.d.ts +2 -0
- package/dist/components/video-player/video-player.d.ts +37 -0
- package/dist/components/video-player/video-player.js +178 -0
- package/dist/components/video-player/video-player.js.map +1 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.js +6 -1
- package/dist/tokens/generated.d.ts +8 -0
- package/dist/tokens/generated.js +10 -2
- package/dist/tokens/generated.js.map +1 -1
- package/package.json +1 -1
- package/theme.css +8 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { cn } from "../../utils/cn.js";
|
|
2
|
+
import { useToken } from "../_shared/use-token.js";
|
|
3
|
+
import { DEFAULT_WAVEFORM, formatTime, resamplePeaks, useSeek } from "../_shared/media.js";
|
|
4
|
+
import { PauseIcon, PlayIcon } from "../_shared/media-icons.js";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { Pressable, Text, View } from "react-native";
|
|
7
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
//#region src/components/audio-player/audio-player.tsx
|
|
9
|
+
const BAR_MIN = 6;
|
|
10
|
+
const BAR_RANGE = 28;
|
|
11
|
+
/**
|
|
12
|
+
* Casca de player de áudio: botão, onda, tempo e velocidade. Não toca nada: quem usa guarda o
|
|
13
|
+
* estado (por exemplo com `expo-audio`) e o passa por props, ligando `onPlayPause`, `onSeek` e
|
|
14
|
+
* `onRateChange` ao player de verdade.
|
|
15
|
+
*/
|
|
16
|
+
function AudioPlayer({ className, label = "Áudio", playing = false, position = 0, duration = 0, rate, speeds = [
|
|
17
|
+
1,
|
|
18
|
+
1.5,
|
|
19
|
+
2
|
|
20
|
+
], waveform, bars = 52, onPlayPause, onSeek, onRateChange, ...props }) {
|
|
21
|
+
const [chosen, setChosen] = useState(speeds[0] ?? 1);
|
|
22
|
+
const speed = rate ?? chosen;
|
|
23
|
+
const peaks = resamplePeaks(waveform ?? DEFAULT_WAVEFORM, bars);
|
|
24
|
+
const played = duration ? Math.round(position / duration * bars) : 0;
|
|
25
|
+
const iconColor = useToken("--color-media-on-surface");
|
|
26
|
+
const seek = useSeek({
|
|
27
|
+
position,
|
|
28
|
+
duration,
|
|
29
|
+
label: "Posição do áudio",
|
|
30
|
+
onSeek
|
|
31
|
+
});
|
|
32
|
+
function nextSpeed() {
|
|
33
|
+
const next = speeds[(speeds.indexOf(speed) + 1) % speeds.length] ?? 1;
|
|
34
|
+
setChosen(next);
|
|
35
|
+
onRateChange?.(next);
|
|
36
|
+
}
|
|
37
|
+
return /* @__PURE__ */ jsxs(View, {
|
|
38
|
+
accessibilityLabel: label,
|
|
39
|
+
className: cn("w-full max-w-[480px] flex-row items-center gap-3 rounded-lg border border-border-default bg-surface-elevated p-4 shadow-sm", className),
|
|
40
|
+
...props,
|
|
41
|
+
children: [
|
|
42
|
+
/* @__PURE__ */ jsx(Pressable, {
|
|
43
|
+
accessibilityRole: "button",
|
|
44
|
+
accessibilityLabel: playing ? "Pausar" : "Reproduzir",
|
|
45
|
+
onPress: onPlayPause,
|
|
46
|
+
className: "size-11 shrink-0 items-center justify-center rounded-full bg-button-primary-bg active:bg-button-primary-bg-active",
|
|
47
|
+
children: playing ? /* @__PURE__ */ jsx(PauseIcon, { color: iconColor }) : /* @__PURE__ */ jsx(PlayIcon, { color: iconColor })
|
|
48
|
+
}),
|
|
49
|
+
/* @__PURE__ */ jsxs(View, {
|
|
50
|
+
className: "min-w-0 flex-1 flex-row items-center gap-2",
|
|
51
|
+
children: [/* @__PURE__ */ jsx(View, {
|
|
52
|
+
...seek.props,
|
|
53
|
+
className: "h-[34px] min-w-0 flex-1 justify-center",
|
|
54
|
+
children: /* @__PURE__ */ jsx(View, {
|
|
55
|
+
pointerEvents: "none",
|
|
56
|
+
className: "h-full flex-row items-center gap-0.5",
|
|
57
|
+
children: peaks.map((peak, i) => /* @__PURE__ */ jsx(View, {
|
|
58
|
+
className: cn("min-w-0.5 flex-1 rounded-full", i < played ? "bg-button-primary-bg" : "bg-media-wave-rest"),
|
|
59
|
+
style: { height: BAR_MIN + peak * BAR_RANGE }
|
|
60
|
+
}, i))
|
|
61
|
+
})
|
|
62
|
+
}), /* @__PURE__ */ jsxs(Text, {
|
|
63
|
+
accessible: false,
|
|
64
|
+
className: "shrink-0 text-xs font-medium leading-4 text-text-secondary",
|
|
65
|
+
style: { fontVariant: ["tabular-nums"] },
|
|
66
|
+
children: [
|
|
67
|
+
formatTime(position),
|
|
68
|
+
" / ",
|
|
69
|
+
formatTime(duration)
|
|
70
|
+
]
|
|
71
|
+
})]
|
|
72
|
+
}),
|
|
73
|
+
/* @__PURE__ */ jsx(Pressable, {
|
|
74
|
+
accessibilityRole: "button",
|
|
75
|
+
accessibilityLabel: `Velocidade de reprodução: ${speed}x`,
|
|
76
|
+
onPress: nextSpeed,
|
|
77
|
+
className: "h-8 w-16 shrink-0 items-center justify-center rounded-full border border-border-default bg-surface-elevated active:bg-bg-tertiary",
|
|
78
|
+
children: /* @__PURE__ */ jsxs(Text, {
|
|
79
|
+
className: "text-sm font-medium leading-5 text-text-secondary",
|
|
80
|
+
children: [speed, "x"]
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
]
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
export { AudioPlayer };
|
|
88
|
+
|
|
89
|
+
//# sourceMappingURL=audio-player.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audio-player.js","names":[],"sources":["../../../src/components/audio-player/audio-player.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { useState } from 'react'\nimport { Pressable, Text, View, type ViewProps } from 'react-native'\nimport { cn } from '../../utils/cn'\nimport { DEFAULT_WAVEFORM, formatTime, resamplePeaks, useSeek } from '../_shared/media'\nimport { PauseIcon, PlayIcon } from '../_shared/media-icons'\nimport { useToken } from '../_shared/use-token'\n\n// Alturas das barras no Figma: de 6px (silêncio) a 34px (pico).\nconst BAR_MIN = 6\nconst BAR_RANGE = 28\n\nexport type AudioPlayerProps = Omit<ViewProps, 'children' | 'style'> & {\n className?: string\n /** Nome acessível do player. */\n label?: string\n /** Tocando agora: mostra o botão de pausar. */\n playing?: boolean\n /** Posição atual, em segundos. */\n position?: number\n /** Duração total, em segundos. */\n duration?: number\n /** Velocidade atual (controlado). Sem ela, o player guarda a escolhida. */\n rate?: number\n /** Velocidades do botão (cada toque passa para a próxima). Padrão: 1x, 1.5x e 2x. */\n speeds?: number[]\n /** Picos da onda, de 0 a 1. Sem eles, usa uma onda padrão. */\n waveform?: number[]\n /** Quantidade de barras da onda. Padrão: 52. */\n bars?: number\n onPlayPause?: () => void\n /** Chamado com a posição (em segundos) tocada ou arrastada na onda. */\n onSeek?: (seconds: number) => void\n /** Chamado com a nova velocidade quando o botão é tocado. */\n onRateChange?: (rate: number) => void\n}\n\n/**\n * Casca de player de áudio: botão, onda, tempo e velocidade. Não toca nada: quem usa guarda o\n * estado (por exemplo com `expo-audio`) e o passa por props, ligando `onPlayPause`, `onSeek` e\n * `onRateChange` ao player de verdade.\n */\nexport function AudioPlayer({\n className,\n label = 'Áudio',\n playing = false,\n position = 0,\n duration = 0,\n rate,\n speeds = [1, 1.5, 2],\n waveform,\n bars = 52,\n onPlayPause,\n onSeek,\n onRateChange,\n ...props\n}: AudioPlayerProps) {\n const [chosen, setChosen] = useState(speeds[0] ?? 1)\n const speed = rate ?? chosen\n const peaks = resamplePeaks(waveform ?? DEFAULT_WAVEFORM, bars)\n const played = duration ? Math.round((position / duration) * bars) : 0\n const iconColor = useToken('--color-media-on-surface')\n const seek = useSeek({ position, duration, label: 'Posição do áudio', onSeek })\n\n function nextSpeed() {\n const next = speeds[(speeds.indexOf(speed) + 1) % speeds.length] ?? 1\n setChosen(next)\n onRateChange?.(next)\n }\n\n return (\n <View\n accessibilityLabel={label}\n className={cn(\n 'w-full max-w-[480px] flex-row items-center gap-3 rounded-lg border border-border-default bg-surface-elevated p-4 shadow-sm',\n className,\n )}\n {...props}\n >\n <Pressable\n accessibilityRole=\"button\"\n accessibilityLabel={playing ? 'Pausar' : 'Reproduzir'}\n onPress={onPlayPause}\n className=\"size-11 shrink-0 items-center justify-center rounded-full bg-button-primary-bg active:bg-button-primary-bg-active\"\n >\n {playing ? <PauseIcon color={iconColor} /> : <PlayIcon color={iconColor} />}\n </Pressable>\n <View className=\"min-w-0 flex-1 flex-row items-center gap-2\">\n <View {...seek.props} className=\"h-[34px] min-w-0 flex-1 justify-center\">\n {/* Filhos ignoram toques para que `locationX` seja sempre relativo à onda. */}\n <View pointerEvents=\"none\" className=\"h-full flex-row items-center gap-0.5\">\n {peaks.map((peak, i) => (\n <View\n // biome-ignore lint/suspicious/noArrayIndexKey: as barras são posicionais e fixas\n key={i}\n className={cn(\n 'min-w-0.5 flex-1 rounded-full',\n i < played ? 'bg-button-primary-bg' : 'bg-media-wave-rest',\n )}\n style={{ height: BAR_MIN + peak * BAR_RANGE }}\n />\n ))}\n </View>\n </View>\n <Text\n accessible={false}\n className=\"shrink-0 text-xs font-medium leading-4 text-text-secondary\"\n style={{ fontVariant: ['tabular-nums'] }}\n >\n {formatTime(position)} / {formatTime(duration)}\n </Text>\n </View>\n <Pressable\n accessibilityRole=\"button\"\n accessibilityLabel={`Velocidade de reprodução: ${speed}x`}\n onPress={nextSpeed}\n className=\"h-8 w-16 shrink-0 items-center justify-center rounded-full border border-border-default bg-surface-elevated active:bg-bg-tertiary\"\n >\n <Text className=\"text-sm font-medium leading-5 text-text-secondary\">{speed}x</Text>\n </Pressable>\n </View>\n )\n}\n"],"mappings":";;;;;;;;AAUA,MAAM,UAAU;AAChB,MAAM,YAAY;;;;;;AAgClB,SAAgB,YAAY,EAC1B,WACA,QAAQ,SACR,UAAU,OACV,WAAW,GACX,WAAW,GACX,MACA,SAAS;CAAC;CAAG;CAAK;AAAC,GACnB,UACA,OAAO,IACP,aACA,QACA,cACA,GAAG,SACgB;CACnB,MAAM,CAAC,QAAQ,aAAa,SAAS,OAAO,MAAM,CAAC;CACnD,MAAM,QAAQ,QAAQ;CACtB,MAAM,QAAQ,cAAc,YAAY,kBAAkB,IAAI;CAC9D,MAAM,SAAS,WAAW,KAAK,MAAO,WAAW,WAAY,IAAI,IAAI;CACrE,MAAM,YAAY,SAAS,0BAA0B;CACrD,MAAM,OAAO,QAAQ;EAAE;EAAU;EAAU,OAAO;EAAoB;CAAO,CAAC;CAE9E,SAAS,YAAY;EACnB,MAAM,OAAO,QAAQ,OAAO,QAAQ,KAAK,IAAI,KAAK,OAAO,WAAW;EACpE,UAAU,IAAI;EACd,eAAe,IAAI;CACrB;CAEA,OACE,qBAAC,MAAD;EACE,oBAAoB;EACpB,WAAW,GACT,8HACA,SACF;EACA,GAAI;EANN,UAAA;GAQE,oBAAC,WAAD;IACE,mBAAkB;IAClB,oBAAoB,UAAU,WAAW;IACzC,SAAS;IACT,WAAU;IAET,UAAA,UAAU,oBAAC,WAAD,EAAW,OAAO,UAAY,CAAA,IAAI,oBAAC,UAAD,EAAU,OAAO,UAAY,CAAA;GACjE,CAAA;GACX,qBAAC,MAAD;IAAM,WAAU;IAAhB,UAAA,CACE,oBAAC,MAAD;KAAM,GAAI,KAAK;KAAO,WAAU;KAE9B,UAAA,oBAAC,MAAD;MAAM,eAAc;MAAO,WAAU;MAClC,UAAA,MAAM,KAAK,MAAM,MAChB,oBAAC,MAAD;OAGE,WAAW,GACT,iCACA,IAAI,SAAS,yBAAyB,oBACxC;OACA,OAAO,EAAE,QAAQ,UAAU,OAAO,UAAU;MAC7C,GANM,CAMN,CACF;KACG,CAAA;IACF,CAAA,GACN,qBAAC,MAAD;KACE,YAAY;KACZ,WAAU;KACV,OAAO,EAAE,aAAa,CAAC,cAAc,EAAE;KAHzC,UAAA;MAKG,WAAW,QAAQ;MAAE;MAAI,WAAW,QAAQ;KACzC;IACF,CAAA,CAAA;;GACN,oBAAC,WAAD;IACE,mBAAkB;IAClB,oBAAoB,6BAA6B,MAAM;IACvD,SAAS;IACT,WAAU;IAEV,UAAA,qBAAC,MAAD;KAAM,WAAU;KAAhB,UAAA,CAAqE,OAAM,GAAO;;GACzE,CAAA;EACP;;AAEV"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
import { ViewProps } from "react-native";
|
|
3
|
+
//#region src/components/carousel/carousel.d.ts
|
|
4
|
+
export type CarouselProps = Omit<ViewProps, 'children' | 'style'> & {
|
|
5
|
+
className?: string;
|
|
6
|
+
/** Nome acessível do carrossel (ex.: "Modelos de mensagem"). */
|
|
7
|
+
label: string;
|
|
8
|
+
/** Os itens: use `CarouselItem`. */
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
/** Mostra os pontos de posição. Padrão: `true`. */
|
|
11
|
+
showDots?: boolean;
|
|
12
|
+
/** Mostra as setas anterior/próximo. Padrão: `true`. */
|
|
13
|
+
showArrows?: boolean;
|
|
14
|
+
/** Largura de cada item, de 0 a 1 da largura do carrossel. Padrão: 0.78 no celular e 0.52 a partir de 768px de tela. */
|
|
15
|
+
itemRatio?: number;
|
|
16
|
+
/** Chamado com o índice do item em destaque sempre que ele muda. */
|
|
17
|
+
onIndexChange?: (index: number) => void;
|
|
18
|
+
previousLabel?: string;
|
|
19
|
+
nextLabel?: string;
|
|
20
|
+
/** Monta o nome acessível de cada ponto. Padrão: "Ir para o item N". */
|
|
21
|
+
dotLabel?: (index: number) => string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Fileira de itens que desliza com o dedo e encaixa item a item, com pontos e setas. Mostra um
|
|
25
|
+
* pedaço do próximo item. O último item "chega ao fim" mesmo que não caiba alinhado à esquerda:
|
|
26
|
+
* o ponto e a seta refletem isso.
|
|
27
|
+
*/
|
|
28
|
+
export declare function Carousel({ className, label, children, showDots, showArrows, itemRatio, onIndexChange, previousLabel, nextLabel, dotLabel, ...props }: CarouselProps): import("react").JSX.Element;
|
|
29
|
+
/** Um item do `Carousel`. A largura vem do carrossel (`itemRatio`); o item preenche toda a altura. */
|
|
30
|
+
export declare function CarouselItem({ className, ...props }: Omit<ViewProps, 'style'> & {
|
|
31
|
+
className?: string;
|
|
32
|
+
}): import("react").JSX.Element;
|
|
33
|
+
//#endregion
|
|
34
|
+
//# sourceMappingURL=carousel.d.ts.map
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { cn } from "../../utils/cn.js";
|
|
2
|
+
import { useToken } from "../_shared/use-token.js";
|
|
3
|
+
import { IconButton } from "../icon-button/icon-button.js";
|
|
4
|
+
import { NextIcon, PreviousIcon } from "../_shared/media-icons.js";
|
|
5
|
+
import { Children, useRef, useState } from "react";
|
|
6
|
+
import { Pressable, ScrollView, View, useWindowDimensions } from "react-native";
|
|
7
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
//#region src/components/carousel/carousel.tsx
|
|
9
|
+
const GAP = 16;
|
|
10
|
+
const TABLET = 768;
|
|
11
|
+
/**
|
|
12
|
+
* Fileira de itens que desliza com o dedo e encaixa item a item, com pontos e setas. Mostra um
|
|
13
|
+
* pedaço do próximo item. O último item "chega ao fim" mesmo que não caiba alinhado à esquerda:
|
|
14
|
+
* o ponto e a seta refletem isso.
|
|
15
|
+
*/
|
|
16
|
+
function Carousel({ className, label, children, showDots = true, showArrows = true, itemRatio, onIndexChange, previousLabel = "Item anterior", nextLabel = "Próximo item", dotLabel = (index) => `Ir para o item ${index + 1}`, ...props }) {
|
|
17
|
+
const items = Children.toArray(children);
|
|
18
|
+
const count = items.length;
|
|
19
|
+
const scroll = useRef(null);
|
|
20
|
+
const { width: screen } = useWindowDimensions();
|
|
21
|
+
const [width, setWidth] = useState(0);
|
|
22
|
+
const [active, setActive] = useState(0);
|
|
23
|
+
const arrowColor = useToken("--color-text-primary");
|
|
24
|
+
const arrowDisabled = useToken("--color-button-disabled-text");
|
|
25
|
+
const itemWidth = width * (itemRatio ?? (screen >= TABLET ? .52 : .78));
|
|
26
|
+
const step = itemWidth + GAP;
|
|
27
|
+
const max = Math.max(count * step - GAP - width, 0);
|
|
28
|
+
const offsets = Array.from({ length: count }, (_, i) => Math.min(i * step, max));
|
|
29
|
+
function indexAt(x) {
|
|
30
|
+
if (max > 0 && x >= max - 1) return count - 1;
|
|
31
|
+
let best = 0;
|
|
32
|
+
offsets.forEach((offset, i) => {
|
|
33
|
+
if (Math.abs(offset - x) < Math.abs((offsets[best] ?? 0) - x)) best = i;
|
|
34
|
+
});
|
|
35
|
+
return best;
|
|
36
|
+
}
|
|
37
|
+
function onScroll(event) {
|
|
38
|
+
const next = indexAt(event.nativeEvent.contentOffset.x);
|
|
39
|
+
if (next === active) return;
|
|
40
|
+
setActive(next);
|
|
41
|
+
onIndexChange?.(next);
|
|
42
|
+
}
|
|
43
|
+
function goTo(index) {
|
|
44
|
+
const target = Math.min(Math.max(index, 0), count - 1);
|
|
45
|
+
scroll.current?.scrollTo({
|
|
46
|
+
x: offsets[target] ?? 0,
|
|
47
|
+
animated: true
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return /* @__PURE__ */ jsxs(View, {
|
|
51
|
+
className: cn("gap-6", className),
|
|
52
|
+
...props,
|
|
53
|
+
children: [/* @__PURE__ */ jsx(ScrollView, {
|
|
54
|
+
ref: scroll,
|
|
55
|
+
horizontal: true,
|
|
56
|
+
accessibilityLabel: label,
|
|
57
|
+
showsHorizontalScrollIndicator: false,
|
|
58
|
+
decelerationRate: "fast",
|
|
59
|
+
snapToOffsets: offsets,
|
|
60
|
+
scrollEventThrottle: 16,
|
|
61
|
+
onLayout: (event) => setWidth(event.nativeEvent.layout.width),
|
|
62
|
+
onScroll,
|
|
63
|
+
contentContainerClassName: "gap-4",
|
|
64
|
+
children: items.map((child, i) => /* @__PURE__ */ jsx(View, {
|
|
65
|
+
accessibilityLabel: `${i + 1} de ${count}`,
|
|
66
|
+
style: { width: itemWidth },
|
|
67
|
+
children: child
|
|
68
|
+
}, i))
|
|
69
|
+
}), (showDots || showArrows) && count > 0 && /* @__PURE__ */ jsxs(View, {
|
|
70
|
+
className: "flex-row items-center justify-between",
|
|
71
|
+
children: [showDots ? /* @__PURE__ */ jsx(View, {
|
|
72
|
+
className: "flex-row items-center gap-2",
|
|
73
|
+
children: items.map((_, i) => /* @__PURE__ */ jsx(Pressable, {
|
|
74
|
+
accessibilityRole: "button",
|
|
75
|
+
accessibilityLabel: dotLabel(i),
|
|
76
|
+
accessibilityState: { selected: i === active },
|
|
77
|
+
onPress: () => goTo(i),
|
|
78
|
+
className: "h-11 items-center justify-center",
|
|
79
|
+
children: /* @__PURE__ */ jsx(View, { className: cn("h-2 rounded-full", i === active ? "w-6 bg-interactive-primary" : "w-2 bg-border-strong") })
|
|
80
|
+
}, i))
|
|
81
|
+
}) : /* @__PURE__ */ jsx(View, {}), showArrows && /* @__PURE__ */ jsxs(View, {
|
|
82
|
+
className: "flex-row gap-2",
|
|
83
|
+
children: [/* @__PURE__ */ jsx(IconButton, {
|
|
84
|
+
accessibilityLabel: previousLabel,
|
|
85
|
+
variant: "tertiary",
|
|
86
|
+
disabled: active <= 0,
|
|
87
|
+
onPress: () => goTo(active - 1),
|
|
88
|
+
children: /* @__PURE__ */ jsx(PreviousIcon, { color: active <= 0 ? arrowDisabled : arrowColor })
|
|
89
|
+
}), /* @__PURE__ */ jsx(IconButton, {
|
|
90
|
+
accessibilityLabel: nextLabel,
|
|
91
|
+
variant: "tertiary",
|
|
92
|
+
disabled: active >= count - 1,
|
|
93
|
+
onPress: () => goTo(active + 1),
|
|
94
|
+
children: /* @__PURE__ */ jsx(NextIcon, { color: active >= count - 1 ? arrowDisabled : arrowColor })
|
|
95
|
+
})]
|
|
96
|
+
})]
|
|
97
|
+
})]
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
/** Um item do `Carousel`. A largura vem do carrossel (`itemRatio`); o item preenche toda a altura. */
|
|
101
|
+
function CarouselItem({ className, ...props }) {
|
|
102
|
+
return /* @__PURE__ */ jsx(View, {
|
|
103
|
+
className: cn("flex-1", className),
|
|
104
|
+
...props
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
//#endregion
|
|
108
|
+
export { Carousel, CarouselItem };
|
|
109
|
+
|
|
110
|
+
//# sourceMappingURL=carousel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"carousel.js","names":[],"sources":["../../../src/components/carousel/carousel.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { Children, type ReactNode, useRef, useState } from 'react'\nimport {\n type LayoutChangeEvent,\n type NativeScrollEvent,\n type NativeSyntheticEvent,\n Pressable,\n ScrollView,\n useWindowDimensions,\n View,\n type ViewProps,\n} from 'react-native'\nimport { cn } from '../../utils/cn'\nimport { NextIcon, PreviousIcon } from '../_shared/media-icons'\nimport { useToken } from '../_shared/use-token'\nimport { IconButton } from '../icon-button'\n\nconst GAP = 16\nconst TABLET = 768\n\nexport type CarouselProps = Omit<ViewProps, 'children' | 'style'> & {\n className?: string\n /** Nome acessível do carrossel (ex.: \"Modelos de mensagem\"). */\n label: string\n /** Os itens: use `CarouselItem`. */\n children: ReactNode\n /** Mostra os pontos de posição. Padrão: `true`. */\n showDots?: boolean\n /** Mostra as setas anterior/próximo. Padrão: `true`. */\n showArrows?: boolean\n /** Largura de cada item, de 0 a 1 da largura do carrossel. Padrão: 0.78 no celular e 0.52 a partir de 768px de tela. */\n itemRatio?: number\n /** Chamado com o índice do item em destaque sempre que ele muda. */\n onIndexChange?: (index: number) => void\n previousLabel?: string\n nextLabel?: string\n /** Monta o nome acessível de cada ponto. Padrão: \"Ir para o item N\". */\n dotLabel?: (index: number) => string\n}\n\n/**\n * Fileira de itens que desliza com o dedo e encaixa item a item, com pontos e setas. Mostra um\n * pedaço do próximo item. O último item \"chega ao fim\" mesmo que não caiba alinhado à esquerda:\n * o ponto e a seta refletem isso.\n */\nexport function Carousel({\n className,\n label,\n children,\n showDots = true,\n showArrows = true,\n itemRatio,\n onIndexChange,\n previousLabel = 'Item anterior',\n nextLabel = 'Próximo item',\n dotLabel = (index) => `Ir para o item ${index + 1}`,\n ...props\n}: CarouselProps) {\n const items = Children.toArray(children)\n const count = items.length\n const scroll = useRef<ScrollView>(null)\n const { width: screen } = useWindowDimensions()\n const [width, setWidth] = useState(0)\n const [active, setActive] = useState(0)\n const arrowColor = useToken('--color-text-primary')\n const arrowDisabled = useToken('--color-button-disabled-text')\n\n const itemWidth = width * (itemRatio ?? (screen >= TABLET ? 0.52 : 0.78))\n const step = itemWidth + GAP\n const max = Math.max(count * step - GAP - width, 0)\n // o último ponto de encaixe é o fim da rolagem, mesmo que o último item não chegue à esquerda\n const offsets = Array.from({ length: count }, (_, i) => Math.min(i * step, max))\n\n function indexAt(x: number) {\n if (max > 0 && x >= max - 1) return count - 1\n let best = 0\n offsets.forEach((offset, i) => {\n if (Math.abs(offset - x) < Math.abs((offsets[best] ?? 0) - x)) best = i\n })\n return best\n }\n\n function onScroll(event: NativeSyntheticEvent<NativeScrollEvent>) {\n const next = indexAt(event.nativeEvent.contentOffset.x)\n if (next === active) return\n setActive(next)\n onIndexChange?.(next)\n }\n\n function goTo(index: number) {\n const target = Math.min(Math.max(index, 0), count - 1)\n scroll.current?.scrollTo({ x: offsets[target] ?? 0, animated: true })\n }\n\n return (\n <View className={cn('gap-6', className)} {...props}>\n <ScrollView\n ref={scroll}\n horizontal\n accessibilityLabel={label}\n showsHorizontalScrollIndicator={false}\n decelerationRate=\"fast\"\n snapToOffsets={offsets}\n scrollEventThrottle={16}\n onLayout={(event: LayoutChangeEvent) => setWidth(event.nativeEvent.layout.width)}\n onScroll={onScroll}\n contentContainerClassName=\"gap-4\"\n >\n {items.map((child, i) => (\n <View\n // biome-ignore lint/suspicious/noArrayIndexKey: os itens são posicionais\n key={i}\n accessibilityLabel={`${i + 1} de ${count}`}\n style={{ width: itemWidth }}\n >\n {child}\n </View>\n ))}\n </ScrollView>\n {(showDots || showArrows) && count > 0 && (\n <View className=\"flex-row items-center justify-between\">\n {showDots ? (\n <View className=\"flex-row items-center gap-2\">\n {items.map((_, i) => (\n <Pressable\n // biome-ignore lint/suspicious/noArrayIndexKey: os pontos são posicionais\n key={i}\n accessibilityRole=\"button\"\n accessibilityLabel={dotLabel(i)}\n accessibilityState={{ selected: i === active }}\n onPress={() => goTo(i)}\n className=\"h-11 items-center justify-center\"\n >\n <View\n className={cn(\n 'h-2 rounded-full',\n i === active ? 'w-6 bg-interactive-primary' : 'w-2 bg-border-strong',\n )}\n />\n </Pressable>\n ))}\n </View>\n ) : (\n <View />\n )}\n {showArrows && (\n <View className=\"flex-row gap-2\">\n <IconButton\n accessibilityLabel={previousLabel}\n variant=\"tertiary\"\n disabled={active <= 0}\n onPress={() => goTo(active - 1)}\n >\n <PreviousIcon color={active <= 0 ? arrowDisabled : arrowColor} />\n </IconButton>\n <IconButton\n accessibilityLabel={nextLabel}\n variant=\"tertiary\"\n disabled={active >= count - 1}\n onPress={() => goTo(active + 1)}\n >\n <NextIcon color={active >= count - 1 ? arrowDisabled : arrowColor} />\n </IconButton>\n </View>\n )}\n </View>\n )}\n </View>\n )\n}\n\n/** Um item do `Carousel`. A largura vem do carrossel (`itemRatio`); o item preenche toda a altura. */\nexport function CarouselItem({\n className,\n ...props\n}: Omit<ViewProps, 'style'> & { className?: string }) {\n return <View className={cn('flex-1', className)} {...props} />\n}\n"],"mappings":"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"}
|
|
@@ -16,6 +16,16 @@ export type DrawerProps = VariantProps<typeof drawerVariants> & {
|
|
|
16
16
|
footer?: ReactNode;
|
|
17
17
|
/** Elemento que abre o painel (ex.: um `<Button>`); recebe `onPress`. Dispensável no modo controlado. */
|
|
18
18
|
trigger?: ReactNode;
|
|
19
|
+
/**
|
|
20
|
+
* Arrastar o painel para a borda de onde ele veio fecha. Não toma o gesto da rolagem do corpo
|
|
21
|
+
* (só arrasta com o corpo no começo, no `top`/`bottom`) e o backdrop clareia junto. Padrão: `true`.
|
|
22
|
+
*/
|
|
23
|
+
swipeToClose?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Barrinha de arraste na borda do painel. Só aparece em `top` e `bottom`, e só com
|
|
26
|
+
* `swipeToClose`. Padrão: `true`.
|
|
27
|
+
*/
|
|
28
|
+
showHandle?: boolean;
|
|
19
29
|
/** Nome acessível do botão de fechar (×). */
|
|
20
30
|
closeLabel?: string;
|
|
21
31
|
className?: string;
|
|
@@ -25,7 +35,7 @@ export type DrawerProps = VariantProps<typeof drawerVariants> & {
|
|
|
25
35
|
* Painel colado a uma borda da tela, que desliza ao abrir e fechar. Usa o `Modal` nativo (botão
|
|
26
36
|
* voltar do Android e toque no fundo fecham). Não há foco preso nem `Esc`.
|
|
27
37
|
*/
|
|
28
|
-
export declare function Drawer({ open, defaultOpen, onOpenChange, title, children, footer, trigger, closeLabel, side, className, testID }: DrawerProps): import("react").JSX.Element;
|
|
38
|
+
export declare function Drawer({ open, defaultOpen, onOpenChange, title, children, footer, trigger, closeLabel, swipeToClose, showHandle, side, className, testID }: DrawerProps): import("react").JSX.Element;
|
|
29
39
|
/** Fecha o painel ao pressionar. Envolva um `<Button>` do rodapé (injeta `onPress`, como o `asChild`). */
|
|
30
40
|
export declare function DrawerClose({ children }: {
|
|
31
41
|
children: ReactNode;
|
|
@@ -1,9 +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
|
-
import {
|
|
7
|
+
import { useSwipeToClose } from "./use-swipe-to-close.js";
|
|
8
|
+
import { createContext, useContext, useRef } from "react";
|
|
7
9
|
import { Animated, Modal, Pressable, ScrollView, Text, View, useWindowDimensions } from "react-native";
|
|
8
10
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
9
11
|
import { cva } from "class-variance-authority";
|
|
@@ -31,7 +33,7 @@ const CloseContext = createContext(null);
|
|
|
31
33
|
* Painel colado a uma borda da tela, que desliza ao abrir e fechar. Usa o `Modal` nativo (botão
|
|
32
34
|
* voltar do Android e toque no fundo fecham). Não há foco preso nem `Esc`.
|
|
33
35
|
*/
|
|
34
|
-
function Drawer({ open, defaultOpen, onOpenChange, title, children, footer, trigger, closeLabel = "Fechar", side, className, testID }) {
|
|
36
|
+
function Drawer({ open, defaultOpen, onOpenChange, title, children, footer, trigger, closeLabel = "Fechar", swipeToClose = true, showHandle = true, side, className, testID }) {
|
|
35
37
|
const [isOpen, setOpen] = useOpenState({
|
|
36
38
|
open,
|
|
37
39
|
defaultOpen,
|
|
@@ -42,25 +44,32 @@ function Drawer({ open, defaultOpen, onOpenChange, title, children, footer, trig
|
|
|
42
44
|
const { width, height } = useWindowDimensions();
|
|
43
45
|
const placement = side ?? "right";
|
|
44
46
|
const horizontal = placement === "left" || placement === "right";
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
47
|
+
const isOpenRef = useRef(isOpen);
|
|
48
|
+
isOpenRef.current = isOpen;
|
|
49
|
+
const swipe = useSwipeToClose({
|
|
50
|
+
side: placement,
|
|
51
|
+
enabled: swipeToClose,
|
|
52
|
+
fallbackSize: horizontal ? width : height,
|
|
53
|
+
onClose: close,
|
|
54
|
+
isOpen: () => isOpenRef.current
|
|
55
|
+
});
|
|
56
|
+
const hasHandle = showHandle && swipeToClose && !horizontal;
|
|
57
|
+
const { mounted, progress } = useTransition(isOpen, {
|
|
58
|
+
duration: 250,
|
|
59
|
+
exitDuration: 250,
|
|
60
|
+
onClosed: swipe.resetDrag
|
|
61
|
+
});
|
|
59
62
|
const offset = horizontal ? width : height;
|
|
60
63
|
const sign = placement === "right" || placement === "bottom" ? 1 : -1;
|
|
61
|
-
const translate = progress.interpolate({
|
|
64
|
+
const translate = Animated.add(progress.interpolate({
|
|
62
65
|
inputRange: [0, 1],
|
|
63
66
|
outputRange: [sign * offset, 0]
|
|
67
|
+
}), Animated.multiply(swipe.drag, sign));
|
|
68
|
+
const handle = hasHandle && /* @__PURE__ */ jsx(View, {
|
|
69
|
+
accessibilityElementsHidden: true,
|
|
70
|
+
importantForAccessibility: "no-hide-descendants",
|
|
71
|
+
className: "h-6 shrink-0 items-center justify-center",
|
|
72
|
+
children: /* @__PURE__ */ jsx(View, { className: "h-1 w-10 rounded-lg bg-border-strong" })
|
|
64
73
|
});
|
|
65
74
|
return /* @__PURE__ */ jsxs(Fragment$1, { children: [injectHandler(trigger, "onPress", () => setOpen(true)), /* @__PURE__ */ jsx(Modal, {
|
|
66
75
|
transparent: true,
|
|
@@ -81,13 +90,16 @@ function Drawer({ open, defaultOpen, onOpenChange, title, children, footer, trig
|
|
|
81
90
|
left: 0,
|
|
82
91
|
right: 0,
|
|
83
92
|
bottom: 0,
|
|
84
|
-
opacity: progress
|
|
93
|
+
opacity: Animated.multiply(progress, swipe.dragFade)
|
|
85
94
|
},
|
|
86
95
|
children: /* @__PURE__ */ jsx(Backdrop, {
|
|
87
96
|
onPress: close,
|
|
88
97
|
testID: testID ? `${testID}-backdrop` : void 0
|
|
89
98
|
})
|
|
90
99
|
}), /* @__PURE__ */ jsx(Animated.View, {
|
|
100
|
+
...swipe.panHandlers,
|
|
101
|
+
onLayout: swipe.onPanelLayout,
|
|
102
|
+
testID: testID ? `${testID}-sheet` : void 0,
|
|
91
103
|
style: [horizontal ? {
|
|
92
104
|
width: "85%",
|
|
93
105
|
maxWidth: 400
|
|
@@ -99,8 +111,9 @@ function Drawer({ open, defaultOpen, onOpenChange, title, children, footer, trig
|
|
|
99
111
|
testID,
|
|
100
112
|
className: cn(drawerVariants({ side }), horizontal && "flex-1", className),
|
|
101
113
|
children: [
|
|
114
|
+
placement === "bottom" && handle,
|
|
102
115
|
/* @__PURE__ */ jsxs(View, {
|
|
103
|
-
className: "flex-row items-center gap-4 border-b border-border-default px-6 py-4",
|
|
116
|
+
className: cn("flex-row items-center gap-4 border-b border-border-default px-6 py-4", hasHandle && placement === "bottom" && "pt-0"),
|
|
104
117
|
children: [/* @__PURE__ */ jsx(Text, {
|
|
105
118
|
accessibilityRole: "header",
|
|
106
119
|
className: "flex-1 text-lg font-semibold leading-6 text-text-primary",
|
|
@@ -116,16 +129,18 @@ function Drawer({ open, defaultOpen, onOpenChange, title, children, footer, trig
|
|
|
116
129
|
}),
|
|
117
130
|
/* @__PURE__ */ jsx(ScrollView, {
|
|
118
131
|
className: "shrink",
|
|
119
|
-
contentContainerClassName: "gap-4 p-6",
|
|
132
|
+
contentContainerClassName: cn("gap-4 p-6", hasHandle && placement === "top" && !footer && "pb-2"),
|
|
133
|
+
...swipe.scrollProps,
|
|
120
134
|
children: typeof children === "string" ? /* @__PURE__ */ jsx(Text, {
|
|
121
135
|
className: "text-sm leading-5 text-text-secondary",
|
|
122
136
|
children
|
|
123
137
|
}) : children
|
|
124
138
|
}),
|
|
125
139
|
footer && /* @__PURE__ */ jsx(View, {
|
|
126
|
-
className: "flex-row items-center justify-end gap-2 border-t border-border-default px-6 py-4",
|
|
140
|
+
className: cn("flex-row items-center justify-end gap-2 border-t border-border-default px-6 py-4", hasHandle && placement === "top" && "pb-0"),
|
|
127
141
|
children: footer
|
|
128
|
-
})
|
|
142
|
+
}),
|
|
143
|
+
placement === "top" && handle
|
|
129
144
|
]
|
|
130
145
|
})
|
|
131
146
|
})]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"drawer.js","names":["RNModal"],"sources":["../../../src/components/drawer/drawer.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { createContext, type ReactNode, useContext, useEffect, useRef, useState } from 'react'\nimport {\n Animated,\n Pressable,\n Modal as RNModal,\n ScrollView,\n Text,\n useWindowDimensions,\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'\n\nconst drawerVariants = cva('overflow-hidden border-border-default bg-surface-elevated shadow-2xl', {\n variants: {\n side: {\n right: 'border-l',\n left: 'border-r',\n top: 'border-b',\n bottom: 'border-t',\n },\n },\n defaultVariants: { side: 'right' },\n})\n\nconst containerVariants = cva('flex-1', {\n variants: {\n side: {\n right: 'flex-row justify-end',\n left: 'flex-row justify-start',\n top: 'justify-start',\n bottom: 'justify-end',\n },\n },\n defaultVariants: { side: 'right' },\n})\n\nconst CloseContext = createContext<(() => void) | null>(null)\n\ntype Side = 'right' | 'left' | 'top' | 'bottom'\n\nexport type DrawerProps = VariantProps<typeof drawerVariants> & {\n open?: boolean\n defaultOpen?: boolean\n onOpenChange?: (open: boolean) => void\n /** Título do painel; também é o nome acessível dele. */\n title: ReactNode\n /** O corpo do painel. */\n children: ReactNode\n /** Ações do rodapé (primária à direita). Use `DrawerClose` para fechar. Sem ele, não há rodapé. */\n footer?: ReactNode\n /** Elemento que abre o painel (ex.: um `<Button>`); recebe `onPress`. Dispensável no modo controlado. */\n trigger?: ReactNode\n /** Nome acessível do botão de fechar (×). */\n closeLabel?: string\n className?: string\n testID?: string\n}\n\n/**\n * Painel colado a uma borda da tela, que desliza ao abrir e fechar. Usa o `Modal` nativo (botão\n * voltar do Android e toque no fundo fecham). Não há foco preso nem `Esc`.\n */\nexport function Drawer({\n open,\n defaultOpen,\n onOpenChange,\n title,\n children,\n footer,\n trigger,\n closeLabel = 'Fechar',\n side,\n className,\n testID,\n}: DrawerProps) {\n const [isOpen, setOpen] = useOpenState({ open, defaultOpen, onOpenChange })\n const close = () => setOpen(false)\n const iconColor = useToken('--color-text-primary')\n const { width, height } = useWindowDimensions()\n const placement: Side = side ?? 'right'\n const horizontal = placement === 'left' || placement === 'right'\n\n // O Modal fica montado durante a animação de saída.\n const [mounted, setMounted] = useState(isOpen)\n const progress = useRef(new Animated.Value(isOpen ? 1 : 0)).current\n\n useEffect(() => {\n if (isOpen) setMounted(true)\n const animation = Animated.timing(progress, {\n toValue: isOpen ? 1 : 0,\n duration: 250,\n useNativeDriver: true,\n })\n animation.start(({ finished }) => {\n if (finished && !isOpen) setMounted(false)\n })\n return () => animation.stop()\n }, [isOpen, progress])\n\n const offset = horizontal ? width : height\n const sign = placement === 'right' || placement === 'bottom' ? 1 : -1\n const translate = progress.interpolate({ inputRange: [0, 1], outputRange: [sign * offset, 0] })\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={containerVariants({ side })} accessibilityViewIsModal>\n <Animated.View\n pointerEvents=\"box-none\"\n style={{\n position: 'absolute',\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n opacity: progress,\n }}\n >\n <Backdrop onPress={close} testID={testID ? `${testID}-backdrop` : undefined} />\n </Animated.View>\n <Animated.View\n style={[\n horizontal ? { width: '85%', maxWidth: 400 } : { width: '100%', maxHeight: '80%' },\n { transform: [horizontal ? { translateX: translate } : { translateY: translate }] },\n ]}\n >\n <View\n testID={testID}\n className={cn(drawerVariants({ side }), horizontal && 'flex-1', className)}\n >\n <View className=\"flex-row items-center gap-4 border-b border-border-default px-6 py-4\">\n <Text\n accessibilityRole=\"header\"\n className=\"flex-1 text-lg font-semibold leading-6 text-text-primary\"\n >\n {title}\n </Text>\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 </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 className=\"flex-row items-center justify-end gap-2 border-t border-border-default px-6 py-4\">\n {footer}\n </View>\n )}\n </View>\n </Animated.View>\n </View>\n </CloseContext.Provider>\n </RNModal>\n </>\n )\n}\n\n/** Fecha o painel ao pressionar. Envolva um `<Button>` do rodapé (injeta `onPress`, como o `asChild`). */\nexport function DrawerClose({ children }: { children: ReactNode }) {\n const close = useContext(CloseContext)\n return <>{injectHandler(children, 'onPress', () => close?.())}</>\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"drawer.js","names":["RNModal"],"sources":["../../../src/components/drawer/drawer.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { createContext, type ReactNode, useContext, useRef } from 'react'\nimport {\n Animated,\n Pressable,\n Modal as RNModal,\n ScrollView,\n Text,\n useWindowDimensions,\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'\nimport { useSwipeToClose } from './use-swipe-to-close'\n\nconst drawerVariants = cva('overflow-hidden border-border-default bg-surface-elevated shadow-2xl', {\n variants: {\n side: {\n right: 'border-l',\n left: 'border-r',\n top: 'border-b',\n bottom: 'border-t',\n },\n },\n defaultVariants: { side: 'right' },\n})\n\nconst containerVariants = cva('flex-1', {\n variants: {\n side: {\n right: 'flex-row justify-end',\n left: 'flex-row justify-start',\n top: 'justify-start',\n bottom: 'justify-end',\n },\n },\n defaultVariants: { side: 'right' },\n})\n\nconst CloseContext = createContext<(() => void) | null>(null)\n\ntype Side = 'right' | 'left' | 'top' | 'bottom'\n\nexport type DrawerProps = VariantProps<typeof drawerVariants> & {\n open?: boolean\n defaultOpen?: boolean\n onOpenChange?: (open: boolean) => void\n /** Título do painel; também é o nome acessível dele. */\n title: ReactNode\n /** O corpo do painel. */\n children: ReactNode\n /** Ações do rodapé (primária à direita). Use `DrawerClose` para fechar. Sem ele, não há rodapé. */\n footer?: ReactNode\n /** Elemento que abre o painel (ex.: um `<Button>`); recebe `onPress`. Dispensável no modo controlado. */\n trigger?: ReactNode\n /**\n * Arrastar o painel para a borda de onde ele veio fecha. Não toma o gesto da rolagem do corpo\n * (só arrasta com o corpo no começo, no `top`/`bottom`) e o backdrop clareia junto. Padrão: `true`.\n */\n swipeToClose?: boolean\n /**\n * Barrinha de arraste na borda do painel. Só aparece em `top` e `bottom`, e só com\n * `swipeToClose`. Padrão: `true`.\n */\n showHandle?: boolean\n /** Nome acessível do botão de fechar (×). */\n closeLabel?: string\n className?: string\n testID?: string\n}\n\n/**\n * Painel colado a uma borda da tela, que desliza ao abrir e fechar. Usa o `Modal` nativo (botão\n * voltar do Android e toque no fundo fecham). Não há foco preso nem `Esc`.\n */\nexport function Drawer({\n open,\n defaultOpen,\n onOpenChange,\n title,\n children,\n footer,\n trigger,\n closeLabel = 'Fechar',\n swipeToClose = true,\n showHandle = true,\n side,\n className,\n testID,\n}: DrawerProps) {\n const [isOpen, setOpen] = useOpenState({ open, defaultOpen, onOpenChange })\n const close = () => setOpen(false)\n const iconColor = useToken('--color-text-primary')\n const { width, height } = useWindowDimensions()\n const placement: Side = side ?? 'right'\n const horizontal = placement === 'left' || placement === 'right'\n const isOpenRef = useRef(isOpen)\n isOpenRef.current = isOpen\n const swipe = useSwipeToClose({\n side: placement,\n enabled: swipeToClose,\n fallbackSize: horizontal ? width : height,\n onClose: close,\n isOpen: () => isOpenRef.current,\n })\n const hasHandle = showHandle && swipeToClose && !horizontal\n\n // O Modal fica montado durante a animação de saída; o arraste zera quando ele desmonta.\n const { mounted, progress } = useTransition(isOpen, {\n duration: 250,\n exitDuration: 250,\n onClosed: swipe.resetDrag,\n })\n\n const offset = horizontal ? width : height\n const sign = placement === 'right' || placement === 'bottom' ? 1 : -1\n // abrir/fechar + o que o dedo arrastou (`drag` vai no sentido de fechar)\n const translate = Animated.add(\n progress.interpolate({ inputRange: [0, 1], outputRange: [sign * offset, 0] }),\n Animated.multiply(swipe.drag, sign),\n )\n const handle = hasHandle && (\n // Figma \"Drawer/navigation\": traço de 4px em `border/strong` numa faixa de 24px (variante xl, 40px)\n <View\n accessibilityElementsHidden\n importantForAccessibility=\"no-hide-descendants\"\n className=\"h-6 shrink-0 items-center justify-center\"\n >\n <View className=\"h-1 w-10 rounded-lg bg-border-strong\" />\n </View>\n )\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={containerVariants({ side })} accessibilityViewIsModal>\n <Animated.View\n pointerEvents=\"box-none\"\n style={{\n position: 'absolute',\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n opacity: Animated.multiply(progress, swipe.dragFade),\n }}\n >\n <Backdrop onPress={close} testID={testID ? `${testID}-backdrop` : undefined} />\n </Animated.View>\n <Animated.View\n {...swipe.panHandlers}\n onLayout={swipe.onPanelLayout}\n testID={testID ? `${testID}-sheet` : undefined}\n style={[\n horizontal ? { width: '85%', maxWidth: 400 } : { width: '100%', maxHeight: '80%' },\n { transform: [horizontal ? { translateX: translate } : { translateY: translate }] },\n ]}\n >\n <View\n testID={testID}\n className={cn(drawerVariants({ side }), horizontal && 'flex-1', className)}\n >\n {placement === 'bottom' && handle}\n {/* ao lado da barrinha, o conteúdo vizinho encosta na faixa de 24px (sem somar o padding) */}\n <View\n className={cn(\n 'flex-row items-center gap-4 border-b border-border-default px-6 py-4',\n hasHandle && placement === 'bottom' && 'pt-0',\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 <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 </View>\n <ScrollView\n className=\"shrink\"\n contentContainerClassName={cn(\n 'gap-4 p-6',\n hasHandle && placement === 'top' && !footer && 'pb-2',\n )}\n {...swipe.scrollProps}\n >\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 border-t border-border-default px-6 py-4',\n hasHandle && placement === 'top' && 'pb-0',\n )}\n >\n {footer}\n </View>\n )}\n {placement === 'top' && handle}\n </View>\n </Animated.View>\n </View>\n </CloseContext.Provider>\n </RNModal>\n </>\n )\n}\n\n/** Fecha o painel ao pressionar. Envolva um `<Button>` do rodapé (injeta `onPress`, como o `asChild`). */\nexport function DrawerClose({ children }: { children: ReactNode }) {\n const close = useContext(CloseContext)\n return <>{injectHandler(children, 'onPress', () => close?.())}</>\n}\n"],"mappings":"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"}
|
|
@@ -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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"}
|