@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,21 @@
|
|
|
1
|
+
import { LightboxItem } from "../lightbox/lightbox.js";
|
|
2
|
+
import { ViewProps } from "react-native";
|
|
3
|
+
//#region src/components/gallery/gallery.d.ts
|
|
4
|
+
export type GalleryItem = LightboxItem & {
|
|
5
|
+
/** Imagem menor para a grade; sem ela, a grade usa `src`. */
|
|
6
|
+
thumbSrc?: string;
|
|
7
|
+
};
|
|
8
|
+
export type GalleryProps = Omit<ViewProps, 'children' | 'style'> & {
|
|
9
|
+
className?: string;
|
|
10
|
+
items: GalleryItem[];
|
|
11
|
+
/** Colunas da grade. Padrão: 2 no celular e 3 a partir de 768px de tela. */
|
|
12
|
+
columns?: number;
|
|
13
|
+
/** Nome acessível do botão de cada miniatura. Padrão: "Ampliar foto N: descrição". */
|
|
14
|
+
openLabel?: (item: GalleryItem, index: number) => string;
|
|
15
|
+
/** Nome acessível do visualizador ampliado. */
|
|
16
|
+
lightboxLabel?: string;
|
|
17
|
+
};
|
|
18
|
+
/** Grade de miniaturas que abre a foto ampliada no `Lightbox`. */
|
|
19
|
+
export declare function Gallery({ className, items, columns, openLabel, lightboxLabel, ...props }: GalleryProps): import("react").JSX.Element;
|
|
20
|
+
//#endregion
|
|
21
|
+
//# sourceMappingURL=gallery.d.ts.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { cn } from "../../utils/cn.js";
|
|
2
|
+
import { useToken } from "../_shared/use-token.js";
|
|
3
|
+
import { ExpandIcon } from "../_shared/media-icons.js";
|
|
4
|
+
import { Lightbox } from "../lightbox/lightbox.js";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { Image, Pressable, View, useWindowDimensions } from "react-native";
|
|
7
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
//#region src/components/gallery/gallery.tsx
|
|
9
|
+
const GAP = 8;
|
|
10
|
+
const TABLET = 768;
|
|
11
|
+
/** Grade de miniaturas que abre a foto ampliada no `Lightbox`. */
|
|
12
|
+
function Gallery({ className, items, columns, openLabel = (item, i) => `Ampliar foto ${i + 1}: ${item.alt}`, lightboxLabel, ...props }) {
|
|
13
|
+
const { width: screen } = useWindowDimensions();
|
|
14
|
+
const [width, setWidth] = useState(0);
|
|
15
|
+
const [opened, setOpened] = useState(null);
|
|
16
|
+
const overlayColor = useToken("--color-media-on-surface");
|
|
17
|
+
const cols = columns ?? (screen >= TABLET ? 3 : 2);
|
|
18
|
+
const size = (width - GAP * (cols - 1)) / cols;
|
|
19
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(View, {
|
|
20
|
+
className: cn("flex-row flex-wrap gap-2", className),
|
|
21
|
+
onLayout: (event) => setWidth(event.nativeEvent.layout.width),
|
|
22
|
+
...props,
|
|
23
|
+
children: items.map((item, i) => /* @__PURE__ */ jsx(Pressable, {
|
|
24
|
+
accessibilityRole: "imagebutton",
|
|
25
|
+
accessibilityLabel: openLabel(item, i),
|
|
26
|
+
onPress: () => setOpened(i),
|
|
27
|
+
className: "overflow-hidden rounded-md bg-bg-skeleton",
|
|
28
|
+
style: {
|
|
29
|
+
width: size,
|
|
30
|
+
aspectRatio: 4 / 3
|
|
31
|
+
},
|
|
32
|
+
children: ({ pressed }) => /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Image, {
|
|
33
|
+
source: { uri: item.thumbSrc ?? item.src },
|
|
34
|
+
className: "size-full",
|
|
35
|
+
resizeMode: "cover"
|
|
36
|
+
}), pressed && /* @__PURE__ */ jsx(View, {
|
|
37
|
+
pointerEvents: "none",
|
|
38
|
+
className: "absolute inset-0 items-center justify-center bg-media-surface/50",
|
|
39
|
+
children: /* @__PURE__ */ jsx(ExpandIcon, { color: overlayColor })
|
|
40
|
+
})] })
|
|
41
|
+
}, item.src))
|
|
42
|
+
}), /* @__PURE__ */ jsx(Lightbox, {
|
|
43
|
+
items,
|
|
44
|
+
label: lightboxLabel,
|
|
45
|
+
open: opened !== null,
|
|
46
|
+
onOpenChange: (open) => !open && setOpened(null),
|
|
47
|
+
index: opened ?? 0,
|
|
48
|
+
onIndexChange: setOpened
|
|
49
|
+
})] });
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
export { Gallery };
|
|
53
|
+
|
|
54
|
+
//# sourceMappingURL=gallery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gallery.js","names":[],"sources":["../../../src/components/gallery/gallery.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { useState } from 'react'\nimport {\n Image,\n type LayoutChangeEvent,\n Pressable,\n useWindowDimensions,\n View,\n type ViewProps,\n} from 'react-native'\nimport { cn } from '../../utils/cn'\nimport { ExpandIcon } from '../_shared/media-icons'\nimport { useToken } from '../_shared/use-token'\nimport { Lightbox, type LightboxItem } from '../lightbox'\n\nconst GAP = 8\nconst TABLET = 768\n\nexport type GalleryItem = LightboxItem & {\n /** Imagem menor para a grade; sem ela, a grade usa `src`. */\n thumbSrc?: string\n}\n\nexport type GalleryProps = Omit<ViewProps, 'children' | 'style'> & {\n className?: string\n items: GalleryItem[]\n /** Colunas da grade. Padrão: 2 no celular e 3 a partir de 768px de tela. */\n columns?: number\n /** Nome acessível do botão de cada miniatura. Padrão: \"Ampliar foto N: descrição\". */\n openLabel?: (item: GalleryItem, index: number) => string\n /** Nome acessível do visualizador ampliado. */\n lightboxLabel?: string\n}\n\n/** Grade de miniaturas que abre a foto ampliada no `Lightbox`. */\nexport function Gallery({\n className,\n items,\n columns,\n openLabel = (item, i) => `Ampliar foto ${i + 1}: ${item.alt}`,\n lightboxLabel,\n ...props\n}: GalleryProps) {\n const { width: screen } = useWindowDimensions()\n const [width, setWidth] = useState(0)\n const [opened, setOpened] = useState<number | null>(null)\n const overlayColor = useToken('--color-media-on-surface')\n const cols = columns ?? (screen >= TABLET ? 3 : 2)\n const size = (width - GAP * (cols - 1)) / cols\n\n return (\n <>\n <View\n className={cn('flex-row flex-wrap gap-2', className)}\n onLayout={(event: LayoutChangeEvent) => setWidth(event.nativeEvent.layout.width)}\n {...props}\n >\n {items.map((item, i) => (\n <Pressable\n key={item.src}\n accessibilityRole=\"imagebutton\"\n accessibilityLabel={openLabel(item, i)}\n onPress={() => setOpened(i)}\n className=\"overflow-hidden rounded-md bg-bg-skeleton\"\n style={{ width: size, aspectRatio: 4 / 3 }}\n >\n {({ pressed }) => (\n <>\n <Image\n source={{ uri: item.thumbSrc ?? item.src }}\n className=\"size-full\"\n resizeMode=\"cover\"\n />\n {pressed && (\n <View\n pointerEvents=\"none\"\n className=\"absolute inset-0 items-center justify-center bg-media-surface/50\"\n >\n <ExpandIcon color={overlayColor} />\n </View>\n )}\n </>\n )}\n </Pressable>\n ))}\n </View>\n <Lightbox\n items={items}\n label={lightboxLabel}\n open={opened !== null}\n onOpenChange={(open) => !open && setOpened(null)}\n index={opened ?? 0}\n onIndexChange={setOpened}\n />\n </>\n )\n}\n"],"mappings":";;;;;;;;AAgBA,MAAM,MAAM;AACZ,MAAM,SAAS;;AAmBf,SAAgB,QAAQ,EACtB,WACA,OACA,SACA,aAAa,MAAM,MAAM,gBAAgB,IAAI,EAAE,IAAI,KAAK,OACxD,eACA,GAAG,SACY;CACf,MAAM,EAAE,OAAO,WAAW,oBAAoB;CAC9C,MAAM,CAAC,OAAO,YAAY,SAAS,CAAC;CACpC,MAAM,CAAC,QAAQ,aAAa,SAAwB,IAAI;CACxD,MAAM,eAAe,SAAS,0BAA0B;CACxD,MAAM,OAAO,YAAY,UAAU,SAAS,IAAI;CAChD,MAAM,QAAQ,QAAQ,OAAO,OAAO,MAAM;CAE1C,OACE,qBAAA,YAAA,EAAA,UAAA,CACE,oBAAC,MAAD;EACE,WAAW,GAAG,4BAA4B,SAAS;EACnD,WAAW,UAA6B,SAAS,MAAM,YAAY,OAAO,KAAK;EAC/E,GAAI;EAEH,UAAA,MAAM,KAAK,MAAM,MAChB,oBAAC,WAAD;GAEE,mBAAkB;GAClB,oBAAoB,UAAU,MAAM,CAAC;GACrC,eAAe,UAAU,CAAC;GAC1B,WAAU;GACV,OAAO;IAAE,OAAO;IAAM,aAAa,IAAI;GAAE;GAEvC,WAAA,EAAE,cACF,qBAAA,YAAA,EAAA,UAAA,CACE,oBAAC,OAAD;IACE,QAAQ,EAAE,KAAK,KAAK,YAAY,KAAK,IAAI;IACzC,WAAU;IACV,YAAW;GACZ,CAAA,GACA,WACC,oBAAC,MAAD;IACE,eAAc;IACd,WAAU;IAEV,UAAA,oBAAC,YAAD,EAAY,OAAO,aAAe,CAAA;GAC9B,CAAA,CAER,EAAA,CAAA;EAEK,GAxBJ,KAAK,GAwBD,CACZ;CACG,CAAA,GACN,oBAAC,UAAD;EACS;EACP,OAAO;EACP,MAAM,WAAW;EACjB,eAAe,SAAS,CAAC,QAAQ,UAAU,IAAI;EAC/C,OAAO,UAAU;EACjB,eAAe;CAChB,CAAA,CACD,EAAA,CAAA;AAEN"}
|
|
@@ -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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//#region src/components/lightbox/lightbox.d.ts
|
|
2
|
+
export type LightboxItem = {
|
|
3
|
+
/** Endereço da imagem ampliada. */
|
|
4
|
+
src: string;
|
|
5
|
+
/** Descrição da imagem para leitores de tela. */
|
|
6
|
+
alt: string;
|
|
7
|
+
};
|
|
8
|
+
export type LightboxProps = {
|
|
9
|
+
items: LightboxItem[];
|
|
10
|
+
open?: boolean;
|
|
11
|
+
defaultOpen?: boolean;
|
|
12
|
+
onOpenChange?: (open: boolean) => void;
|
|
13
|
+
/** Índice da imagem exibida (modo controlado). */
|
|
14
|
+
index?: number;
|
|
15
|
+
defaultIndex?: number;
|
|
16
|
+
onIndexChange?: (index: number) => void;
|
|
17
|
+
/** Nome acessível do visualizador. */
|
|
18
|
+
label?: string;
|
|
19
|
+
closeLabel?: string;
|
|
20
|
+
previousLabel?: string;
|
|
21
|
+
nextLabel?: string;
|
|
22
|
+
testID?: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Imagem ampliada sobre um fundo escurecido, com contador (`1 / 6`), setas e fechar. Deslize para
|
|
26
|
+
* os lados para trocar de foto; as setas dão a volta (da última vai para a primeira).
|
|
27
|
+
*/
|
|
28
|
+
export declare function Lightbox({ items, open, defaultOpen, onOpenChange, index, defaultIndex, onIndexChange, label, closeLabel, previousLabel, nextLabel, testID }: LightboxProps): import("react").JSX.Element;
|
|
29
|
+
//#endregion
|
|
30
|
+
//# sourceMappingURL=lightbox.d.ts.map
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { CloseIcon } from "../_shared/status.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 { useTransition } from "../_shared/use-transition.js";
|
|
6
|
+
import { Backdrop } from "../_shared/backdrop.js";
|
|
7
|
+
import { useOpenState } from "../_shared/overlay-state.js";
|
|
8
|
+
import { useEffect, useRef, useState } from "react";
|
|
9
|
+
import { Animated, FlatList, Image, Modal, StyleSheet, Text, View, useWindowDimensions } from "react-native";
|
|
10
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
+
//#region src/components/lightbox/lightbox.tsx
|
|
12
|
+
const roundButton = "rounded-full border-border-default bg-surface-elevated active:bg-bg-secondary";
|
|
13
|
+
/**
|
|
14
|
+
* Imagem ampliada sobre um fundo escurecido, com contador (`1 / 6`), setas e fechar. Deslize para
|
|
15
|
+
* os lados para trocar de foto; as setas dão a volta (da última vai para a primeira).
|
|
16
|
+
*/
|
|
17
|
+
function Lightbox({ items, open, defaultOpen, onOpenChange, index, defaultIndex = 0, onIndexChange, label = "Visualizador de imagens", closeLabel = "Fechar", previousLabel = "Foto anterior", nextLabel = "Próxima foto", testID }) {
|
|
18
|
+
const [isOpen, setOpen] = useOpenState({
|
|
19
|
+
open,
|
|
20
|
+
defaultOpen,
|
|
21
|
+
onOpenChange
|
|
22
|
+
});
|
|
23
|
+
const { mounted, progress } = useTransition(isOpen);
|
|
24
|
+
const { width, height } = useWindowDimensions();
|
|
25
|
+
const iconColor = useToken("--color-text-primary");
|
|
26
|
+
const list = useRef(null);
|
|
27
|
+
const [inner, setInner] = useState(defaultIndex);
|
|
28
|
+
const count = items.length;
|
|
29
|
+
const current = Math.min(Math.max(index ?? inner, 0), Math.max(count - 1, 0));
|
|
30
|
+
const shown = useRef(current);
|
|
31
|
+
function show(next) {
|
|
32
|
+
shown.current = next;
|
|
33
|
+
if (index === void 0) setInner(next);
|
|
34
|
+
onIndexChange?.(next);
|
|
35
|
+
}
|
|
36
|
+
function go(next) {
|
|
37
|
+
const wrapped = (next + count) % count;
|
|
38
|
+
list.current?.scrollToIndex({
|
|
39
|
+
index: wrapped,
|
|
40
|
+
animated: true
|
|
41
|
+
});
|
|
42
|
+
show(wrapped);
|
|
43
|
+
}
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!mounted || shown.current === current) return;
|
|
46
|
+
shown.current = current;
|
|
47
|
+
list.current?.scrollToIndex({
|
|
48
|
+
index: current,
|
|
49
|
+
animated: false
|
|
50
|
+
});
|
|
51
|
+
}, [current, mounted]);
|
|
52
|
+
function onMomentumScrollEnd(event) {
|
|
53
|
+
const next = Math.round(event.nativeEvent.contentOffset.x / width);
|
|
54
|
+
if (next !== shown.current && next >= 0 && next < count) show(next);
|
|
55
|
+
}
|
|
56
|
+
return /* @__PURE__ */ jsx(Modal, {
|
|
57
|
+
transparent: true,
|
|
58
|
+
statusBarTranslucent: true,
|
|
59
|
+
animationType: "none",
|
|
60
|
+
visible: mounted,
|
|
61
|
+
onRequestClose: () => setOpen(false),
|
|
62
|
+
children: /* @__PURE__ */ jsxs(View, {
|
|
63
|
+
className: "flex-1",
|
|
64
|
+
accessibilityViewIsModal: true,
|
|
65
|
+
accessibilityLabel: label,
|
|
66
|
+
testID,
|
|
67
|
+
children: [/* @__PURE__ */ jsx(Animated.View, {
|
|
68
|
+
pointerEvents: "box-none",
|
|
69
|
+
style: [StyleSheet.absoluteFill, { opacity: progress }],
|
|
70
|
+
children: /* @__PURE__ */ jsx(Backdrop, { onPress: () => setOpen(false) })
|
|
71
|
+
}), /* @__PURE__ */ jsxs(Animated.View, {
|
|
72
|
+
pointerEvents: "box-none",
|
|
73
|
+
style: [StyleSheet.absoluteFill, {
|
|
74
|
+
opacity: progress,
|
|
75
|
+
justifyContent: "center"
|
|
76
|
+
}],
|
|
77
|
+
children: [
|
|
78
|
+
/* @__PURE__ */ jsx(FlatList, {
|
|
79
|
+
ref: list,
|
|
80
|
+
data: items,
|
|
81
|
+
horizontal: true,
|
|
82
|
+
pagingEnabled: true,
|
|
83
|
+
showsHorizontalScrollIndicator: false,
|
|
84
|
+
keyExtractor: (item) => item.src,
|
|
85
|
+
initialScrollIndex: current,
|
|
86
|
+
getItemLayout: (_, i) => ({
|
|
87
|
+
length: width,
|
|
88
|
+
offset: width * i,
|
|
89
|
+
index: i
|
|
90
|
+
}),
|
|
91
|
+
onMomentumScrollEnd,
|
|
92
|
+
renderItem: ({ item }) => /* @__PURE__ */ jsx(View, {
|
|
93
|
+
style: {
|
|
94
|
+
width,
|
|
95
|
+
height: height * .7,
|
|
96
|
+
paddingHorizontal: 16
|
|
97
|
+
},
|
|
98
|
+
children: /* @__PURE__ */ jsx(Image, {
|
|
99
|
+
source: { uri: item.src },
|
|
100
|
+
accessibilityLabel: item.alt,
|
|
101
|
+
resizeMode: "contain",
|
|
102
|
+
className: "size-full"
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
}),
|
|
106
|
+
count > 1 && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs(View, {
|
|
107
|
+
pointerEvents: "box-none",
|
|
108
|
+
className: "absolute inset-x-3 flex-row justify-between",
|
|
109
|
+
style: { top: height / 2 - 22 },
|
|
110
|
+
children: [/* @__PURE__ */ jsx(IconButton, {
|
|
111
|
+
accessibilityLabel: previousLabel,
|
|
112
|
+
variant: "tertiary",
|
|
113
|
+
className: roundButton,
|
|
114
|
+
onPress: () => go(shown.current - 1),
|
|
115
|
+
children: /* @__PURE__ */ jsx(PreviousIcon, { color: iconColor })
|
|
116
|
+
}), /* @__PURE__ */ jsx(IconButton, {
|
|
117
|
+
accessibilityLabel: nextLabel,
|
|
118
|
+
variant: "tertiary",
|
|
119
|
+
className: roundButton,
|
|
120
|
+
onPress: () => go(shown.current + 1),
|
|
121
|
+
children: /* @__PURE__ */ jsx(NextIcon, { color: iconColor })
|
|
122
|
+
})]
|
|
123
|
+
}), /* @__PURE__ */ jsxs(Text, {
|
|
124
|
+
accessibilityLiveRegion: "polite",
|
|
125
|
+
className: "absolute bottom-12 self-center text-sm font-medium leading-5 text-media-on-surface",
|
|
126
|
+
children: [
|
|
127
|
+
current + 1,
|
|
128
|
+
" / ",
|
|
129
|
+
count
|
|
130
|
+
]
|
|
131
|
+
})] }),
|
|
132
|
+
/* @__PURE__ */ jsx(IconButton, {
|
|
133
|
+
accessibilityLabel: closeLabel,
|
|
134
|
+
variant: "tertiary",
|
|
135
|
+
className: `absolute right-4 top-12 ${roundButton}`,
|
|
136
|
+
onPress: () => setOpen(false),
|
|
137
|
+
children: /* @__PURE__ */ jsx(CloseIcon, { color: iconColor })
|
|
138
|
+
})
|
|
139
|
+
]
|
|
140
|
+
})]
|
|
141
|
+
})
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
//#endregion
|
|
145
|
+
export { Lightbox };
|
|
146
|
+
|
|
147
|
+
//# sourceMappingURL=lightbox.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lightbox.js","names":["RNModal"],"sources":["../../../src/components/lightbox/lightbox.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { useEffect, useRef, useState } from 'react'\nimport {\n Animated,\n FlatList,\n Image,\n type NativeScrollEvent,\n type NativeSyntheticEvent,\n Modal as RNModal,\n StyleSheet,\n Text,\n useWindowDimensions,\n View,\n} from 'react-native'\nimport { Backdrop } from '../_shared/backdrop'\nimport { NextIcon, PreviousIcon } from '../_shared/media-icons'\nimport { useOpenState } from '../_shared/overlay-state'\nimport { CloseIcon } from '../_shared/status'\nimport { useToken } from '../_shared/use-token'\nimport { useTransition } from '../_shared/use-transition'\nimport { IconButton } from '../icon-button'\n\nexport type LightboxItem = {\n /** Endereço da imagem ampliada. */\n src: string\n /** Descrição da imagem para leitores de tela. */\n alt: string\n}\n\nexport type LightboxProps = {\n items: LightboxItem[]\n open?: boolean\n defaultOpen?: boolean\n onOpenChange?: (open: boolean) => void\n /** Índice da imagem exibida (modo controlado). */\n index?: number\n defaultIndex?: number\n onIndexChange?: (index: number) => void\n /** Nome acessível do visualizador. */\n label?: string\n closeLabel?: string\n previousLabel?: string\n nextLabel?: string\n testID?: string\n}\n\nconst roundButton = 'rounded-full border-border-default bg-surface-elevated active:bg-bg-secondary'\n\n/**\n * Imagem ampliada sobre um fundo escurecido, com contador (`1 / 6`), setas e fechar. Deslize para\n * os lados para trocar de foto; as setas dão a volta (da última vai para a primeira).\n */\nexport function Lightbox({\n items,\n open,\n defaultOpen,\n onOpenChange,\n index,\n defaultIndex = 0,\n onIndexChange,\n label = 'Visualizador de imagens',\n closeLabel = 'Fechar',\n previousLabel = 'Foto anterior',\n nextLabel = 'Próxima foto',\n testID,\n}: LightboxProps) {\n const [isOpen, setOpen] = useOpenState({ open, defaultOpen, onOpenChange })\n const { mounted, progress } = useTransition(isOpen)\n const { width, height } = useWindowDimensions()\n const iconColor = useToken('--color-text-primary')\n const list = useRef<FlatList<LightboxItem>>(null)\n const [inner, setInner] = useState(defaultIndex)\n const count = items.length\n const current = Math.min(Math.max(index ?? inner, 0), Math.max(count - 1, 0))\n // índice em que a lista está de fato, para só rolar quando o pai troca `index` por fora\n const shown = useRef(current)\n\n function show(next: number) {\n shown.current = next\n if (index === undefined) setInner(next)\n onIndexChange?.(next)\n }\n\n function go(next: number) {\n const wrapped = (next + count) % count\n list.current?.scrollToIndex({ index: wrapped, animated: true })\n show(wrapped)\n }\n\n useEffect(() => {\n if (!mounted || shown.current === current) return\n shown.current = current\n list.current?.scrollToIndex({ index: current, animated: false })\n }, [current, mounted])\n\n function onMomentumScrollEnd(event: NativeSyntheticEvent<NativeScrollEvent>) {\n const next = Math.round(event.nativeEvent.contentOffset.x / width)\n if (next !== shown.current && next >= 0 && next < count) show(next)\n }\n\n return (\n <RNModal\n transparent\n statusBarTranslucent\n animationType=\"none\"\n visible={mounted}\n onRequestClose={() => setOpen(false)}\n >\n <View className=\"flex-1\" accessibilityViewIsModal accessibilityLabel={label} testID={testID}>\n <Animated.View\n pointerEvents=\"box-none\"\n style={[StyleSheet.absoluteFill, { opacity: progress }]}\n >\n <Backdrop onPress={() => setOpen(false)} />\n </Animated.View>\n <Animated.View\n pointerEvents=\"box-none\"\n style={[StyleSheet.absoluteFill, { opacity: progress, justifyContent: 'center' }]}\n >\n <FlatList\n ref={list}\n data={items}\n horizontal\n pagingEnabled\n showsHorizontalScrollIndicator={false}\n keyExtractor={(item) => item.src}\n initialScrollIndex={current}\n getItemLayout={(_, i) => ({ length: width, offset: width * i, index: i })}\n onMomentumScrollEnd={onMomentumScrollEnd}\n renderItem={({ item }) => (\n <View style={{ width, height: height * 0.7, paddingHorizontal: 16 }}>\n <Image\n source={{ uri: item.src }}\n accessibilityLabel={item.alt}\n resizeMode=\"contain\"\n className=\"size-full\"\n />\n </View>\n )}\n />\n {count > 1 && (\n <>\n <View\n pointerEvents=\"box-none\"\n className=\"absolute inset-x-3 flex-row justify-between\"\n style={{ top: height / 2 - 22 }}\n >\n <IconButton\n accessibilityLabel={previousLabel}\n variant=\"tertiary\"\n className={roundButton}\n onPress={() => go(shown.current - 1)}\n >\n <PreviousIcon color={iconColor} />\n </IconButton>\n <IconButton\n accessibilityLabel={nextLabel}\n variant=\"tertiary\"\n className={roundButton}\n onPress={() => go(shown.current + 1)}\n >\n <NextIcon color={iconColor} />\n </IconButton>\n </View>\n <Text\n accessibilityLiveRegion=\"polite\"\n className=\"absolute bottom-12 self-center text-sm font-medium leading-5 text-media-on-surface\"\n >\n {current + 1} / {count}\n </Text>\n </>\n )}\n <IconButton\n accessibilityLabel={closeLabel}\n variant=\"tertiary\"\n className={`absolute right-4 top-12 ${roundButton}`}\n onPress={() => setOpen(false)}\n >\n <CloseIcon color={iconColor} />\n </IconButton>\n </Animated.View>\n </View>\n </RNModal>\n )\n}\n"],"mappings":"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"}
|
|
@@ -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"),
|