@innovui/react-native 0.4.0 → 0.4.2
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/dist/components/alert/alert.js +1 -1
- package/dist/components/alert/alert.js.map +1 -1
- package/dist/components/audio-player/audio-player.js +1 -1
- package/dist/components/audio-player/audio-player.js.map +1 -1
- package/dist/components/card/card.js +1 -1
- package/dist/components/card/card.js.map +1 -1
- package/dist/components/combobox/combobox.js +2 -2
- package/dist/components/combobox/combobox.js.map +1 -1
- package/dist/components/date-picker/date-sheet.js +1 -1
- package/dist/components/date-picker/date-sheet.js.map +1 -1
- package/dist/components/drawer/drawer.js +1 -1
- package/dist/components/drawer/drawer.js.map +1 -1
- package/dist/components/gallery/gallery.js +1 -1
- package/dist/components/gallery/gallery.js.map +1 -1
- package/dist/components/input/input.d.ts +5 -1
- package/dist/components/input/input.js +9 -5
- package/dist/components/input/input.js.map +1 -1
- package/dist/components/menu/menu.js +1 -1
- package/dist/components/menu/menu.js.map +1 -1
- package/dist/components/modal/modal.js +1 -1
- package/dist/components/modal/modal.js.map +1 -1
- package/dist/components/popover/popover.js +1 -1
- package/dist/components/popover/popover.js.map +1 -1
- package/dist/components/segmented-control/segmented-control.js +3 -3
- package/dist/components/segmented-control/segmented-control.js.map +1 -1
- package/dist/components/select/field-trigger.js +1 -1
- package/dist/components/select/field-trigger.js.map +1 -1
- package/dist/components/select/select.js +1 -1
- package/dist/components/select/select.js.map +1 -1
- package/dist/components/skeleton/skeleton.js +2 -2
- package/dist/components/skeleton/skeleton.js.map +1 -1
- package/dist/components/textarea/textarea.js +1 -1
- package/dist/components/textarea/textarea.js.map +1 -1
- package/dist/components/toast/toast.js +1 -1
- package/dist/components/toast/toast.js.map +1 -1
- package/dist/components/upload/upload.js +2 -2
- package/dist/components/upload/upload.js.map +1 -1
- package/dist/components/video-player/video-player.d.ts +5 -1
- package/dist/components/video-player/video-player.js +14 -4
- package/dist/components/video-player/video-player.js.map +1 -1
- package/package.json +1 -1
|
@@ -21,7 +21,7 @@ function Alert({ className, status = "info", title, showIcon = true, action, onD
|
|
|
21
21
|
const closeColor = useToken("--color-button-ghost-text");
|
|
22
22
|
return /* @__PURE__ */ jsxs(View, {
|
|
23
23
|
accessibilityRole: statusRole(status),
|
|
24
|
-
className: cn("flex-row items-start gap-3 rounded-
|
|
24
|
+
className: cn("flex-row items-start gap-3 rounded-[8px] border-l-[3px] py-3 pr-3 pl-4", statusBg[status], border[status], className),
|
|
25
25
|
...props,
|
|
26
26
|
children: [
|
|
27
27
|
showIcon && /* @__PURE__ */ jsx(View, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"alert.js","names":[],"sources":["../../../src/components/alert/alert.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport type { ReactNode } from 'react'\nimport { Text, View, type ViewProps } from 'react-native'\nimport { cn } from '../../utils/cn'\nimport {\n CloseIcon,\n type Status,\n StatusIcon,\n statusBg,\n statusRole,\n statusText,\n statusTextToken,\n} from '../_shared/status'\nimport { useToken } from '../_shared/use-token'\nimport { IconButton } from '../icon-button/icon-button'\n\nconst border: Record<Status, string> = {\n success: 'border-status-success-border',\n error: 'border-status-error-border',\n warning: 'border-status-warning-border',\n info: 'border-status-info-border',\n neutral: 'border-status-neutral-border',\n}\n\nexport type AlertProps = ViewProps & {\n className?: string\n status?: Status\n /** Resumo do alerta (opcional). */\n title?: string\n /** Mostra o ícone semântico do status. */\n showIcon?: boolean\n /** Ação contextual (ex.: um `<Button variant=\"ghost\">`). */\n action?: ReactNode\n /** Mostra o botão de fechar (×) e o chama ao tocar. */\n onDismiss?: () => void\n /** Nome acessível do botão de fechar. */\n dismissLabel?: string\n}\n\n/**\n * Mensagem contextual em bloco, persistente. Para avisos temporários use Toast; para globais,\n * Banner. `children` string vira `Text`; outros nós são renderizados como estão.\n */\nexport function Alert({\n className,\n status = 'info',\n title,\n showIcon = true,\n action,\n onDismiss,\n dismissLabel = 'Fechar',\n children,\n ...props\n}: AlertProps) {\n const statusColor = useToken(statusTextToken[status])\n const closeColor = useToken('--color-button-ghost-text')\n\n return (\n <View\n accessibilityRole={statusRole(status)}\n className={cn(\n 'flex-row items-start gap-3 rounded-
|
|
1
|
+
{"version":3,"file":"alert.js","names":[],"sources":["../../../src/components/alert/alert.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport type { ReactNode } from 'react'\nimport { Text, View, type ViewProps } from 'react-native'\nimport { cn } from '../../utils/cn'\nimport {\n CloseIcon,\n type Status,\n StatusIcon,\n statusBg,\n statusRole,\n statusText,\n statusTextToken,\n} from '../_shared/status'\nimport { useToken } from '../_shared/use-token'\nimport { IconButton } from '../icon-button/icon-button'\n\nconst border: Record<Status, string> = {\n success: 'border-status-success-border',\n error: 'border-status-error-border',\n warning: 'border-status-warning-border',\n info: 'border-status-info-border',\n neutral: 'border-status-neutral-border',\n}\n\nexport type AlertProps = ViewProps & {\n className?: string\n status?: Status\n /** Resumo do alerta (opcional). */\n title?: string\n /** Mostra o ícone semântico do status. */\n showIcon?: boolean\n /** Ação contextual (ex.: um `<Button variant=\"ghost\">`). */\n action?: ReactNode\n /** Mostra o botão de fechar (×) e o chama ao tocar. */\n onDismiss?: () => void\n /** Nome acessível do botão de fechar. */\n dismissLabel?: string\n}\n\n/**\n * Mensagem contextual em bloco, persistente. Para avisos temporários use Toast; para globais,\n * Banner. `children` string vira `Text`; outros nós são renderizados como estão.\n */\nexport function Alert({\n className,\n status = 'info',\n title,\n showIcon = true,\n action,\n onDismiss,\n dismissLabel = 'Fechar',\n children,\n ...props\n}: AlertProps) {\n const statusColor = useToken(statusTextToken[status])\n const closeColor = useToken('--color-button-ghost-text')\n\n return (\n <View\n accessibilityRole={statusRole(status)}\n className={cn(\n 'flex-row items-start gap-3 rounded-[8px] border-l-[3px] py-3 pr-3 pl-4',\n statusBg[status],\n border[status],\n className,\n )}\n {...props}\n >\n {showIcon && (\n <View className=\"shrink-0\">\n <StatusIcon status={status} color={statusColor} />\n </View>\n )}\n <View className=\"min-w-0 flex-1 items-start gap-1\">\n {title ? (\n <Text className={cn('text-sm font-medium leading-5', statusText[status])}>{title}</Text>\n ) : null}\n {typeof children === 'string' ? (\n <Text className=\"text-sm leading-5 text-text-primary\">{children}</Text>\n ) : (\n children\n )}\n {action}\n </View>\n {onDismiss && (\n <IconButton accessibilityLabel={dismissLabel} variant=\"ghost\" size=\"sm\" onPress={onDismiss}>\n <CloseIcon color={closeColor} />\n </IconButton>\n )}\n </View>\n )\n}\n"],"mappings":";;;;;;;AAiBA,MAAM,SAAiC;CACrC,SAAS;CACT,OAAO;CACP,SAAS;CACT,MAAM;CACN,SAAS;AACX;;;;;AAqBA,SAAgB,MAAM,EACpB,WACA,SAAS,QACT,OACA,WAAW,MACX,QACA,WACA,eAAe,UACf,UACA,GAAG,SACU;CACb,MAAM,cAAc,SAAS,gBAAgB,OAAO;CACpD,MAAM,aAAa,SAAS,2BAA2B;CAEvD,OACE,qBAAC,MAAD;EACE,mBAAmB,WAAW,MAAM;EACpC,WAAW,GACT,0EACA,SAAS,SACT,OAAO,SACP,SACF;EACA,GAAI;EARN,UAAA;GAUG,YACC,oBAAC,MAAD;IAAM,WAAU;IACd,UAAA,oBAAC,YAAD;KAAoB;KAAQ,OAAO;IAAc,CAAA;GAC7C,CAAA;GAER,qBAAC,MAAD;IAAM,WAAU;IAAhB,UAAA;KACG,QACC,oBAAC,MAAD;MAAM,WAAW,GAAG,iCAAiC,WAAW,OAAO;MAAI,UAAA;KAAY,CAAA,IACrF;KACH,OAAO,aAAa,WACnB,oBAAC,MAAD;MAAM,WAAU;MAAuC;KAAe,CAAA,IAEtE;KAED;IACG;;GACL,aACC,oBAAC,YAAD;IAAY,oBAAoB;IAAc,SAAQ;IAAQ,MAAK;IAAK,SAAS;IAC/E,UAAA,oBAAC,WAAD,EAAW,OAAO,WAAa,CAAA;GACrB,CAAA;EAEV;;AAEV"}
|
|
@@ -43,7 +43,7 @@ function AudioPlayer({ className, label = "Áudio", playing = false, position =
|
|
|
43
43
|
setCardWidth(event.nativeEvent.layout.width);
|
|
44
44
|
onLayout?.(event);
|
|
45
45
|
},
|
|
46
|
-
className: cn("w-full max-w-[480px] flex-row items-center gap-3 rounded-
|
|
46
|
+
className: cn("w-full max-w-[480px] flex-row items-center gap-3 rounded-[12px] border border-border-default bg-surface-elevated p-4 shadow-sm", className),
|
|
47
47
|
...props,
|
|
48
48
|
children: [
|
|
49
49
|
/* @__PURE__ */ jsx(Pressable, {
|
|
@@ -1 +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 onLayout,\n ...props\n}: AudioPlayerProps) {\n const [chosen, setChosen] = useState(speeds[0] ?? 1)\n const [cardWidth, setCardWidth] = useState(0)\n // abaixo de 416px (26rem) o tempo desce para baixo da onda, que ganha a largura toda\n const stacked = cardWidth > 0 && cardWidth < 416\n const speed = rate ?? chosen\n const iconColor = useToken('--color-media-on-surface')\n const seek = useSeek({ position, duration, label: 'Posição do áudio', onSeek })\n // Em telas estreitas a onda perde barras (2px + 2px de espaço), em vez de invadir o tempo.\n const shown = seek.width ? Math.min(bars, Math.max(1, Math.floor((seek.width + 2) / 4))) : bars\n const peaks = resamplePeaks(waveform ?? DEFAULT_WAVEFORM, shown)\n const played = duration ? Math.round((position / duration) * shown) : 0\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 onLayout={(event) => {\n setCardWidth(event.nativeEvent.layout.width)\n onLayout?.(event)\n }}\n className={cn(\n 'w-full max-w-[480px] flex-row items-center gap-3 rounded-
|
|
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 onLayout,\n ...props\n}: AudioPlayerProps) {\n const [chosen, setChosen] = useState(speeds[0] ?? 1)\n const [cardWidth, setCardWidth] = useState(0)\n // abaixo de 416px (26rem) o tempo desce para baixo da onda, que ganha a largura toda\n const stacked = cardWidth > 0 && cardWidth < 416\n const speed = rate ?? chosen\n const iconColor = useToken('--color-media-on-surface')\n const seek = useSeek({ position, duration, label: 'Posição do áudio', onSeek })\n // Em telas estreitas a onda perde barras (2px + 2px de espaço), em vez de invadir o tempo.\n const shown = seek.width ? Math.min(bars, Math.max(1, Math.floor((seek.width + 2) / 4))) : bars\n const peaks = resamplePeaks(waveform ?? DEFAULT_WAVEFORM, shown)\n const played = duration ? Math.round((position / duration) * shown) : 0\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 onLayout={(event) => {\n setCardWidth(event.nativeEvent.layout.width)\n onLayout?.(event)\n }}\n className={cn(\n 'w-full max-w-[480px] flex-row items-center gap-3 rounded-[12px] 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\n className={cn(\n 'min-w-0 flex-1',\n stacked ? 'flex-col items-stretch gap-1' : 'flex-row items-center gap-2',\n )}\n >\n <View\n {...seek.props}\n className={cn('h-[34px] min-w-0 justify-center', stacked ? 'w-full' : 'flex-1')}\n >\n {/* Filhos ignoram toques para que `locationX` seja sempre relativo à onda. */}\n <View\n pointerEvents=\"none\"\n className=\"h-full flex-row items-center gap-0.5 overflow-hidden\"\n >\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,UACA,GAAG,SACgB;CACnB,MAAM,CAAC,QAAQ,aAAa,SAAS,OAAO,MAAM,CAAC;CACnD,MAAM,CAAC,WAAW,gBAAgB,SAAS,CAAC;CAE5C,MAAM,UAAU,YAAY,KAAK,YAAY;CAC7C,MAAM,QAAQ,QAAQ;CACtB,MAAM,YAAY,SAAS,0BAA0B;CACrD,MAAM,OAAO,QAAQ;EAAE;EAAU;EAAU,OAAO;EAAoB;CAAO,CAAC;CAE9E,MAAM,QAAQ,KAAK,QAAQ,KAAK,IAAI,MAAM,KAAK,IAAI,GAAG,KAAK,OAAO,KAAK,QAAQ,KAAK,CAAC,CAAC,CAAC,IAAI;CAC3F,MAAM,QAAQ,cAAc,YAAY,kBAAkB,KAAK;CAC/D,MAAM,SAAS,WAAW,KAAK,MAAO,WAAW,WAAY,KAAK,IAAI;CAEtE,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,UAAU;GACnB,aAAa,MAAM,YAAY,OAAO,KAAK;GAC3C,WAAW,KAAK;EAClB;EACA,WAAW,GACT,kIACA,SACF;EACA,GAAI;EAVN,UAAA;GAYE,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;IACE,WAAW,GACT,kBACA,UAAU,iCAAiC,6BAC7C;IAJF,UAAA,CAME,oBAAC,MAAD;KACE,GAAI,KAAK;KACT,WAAW,GAAG,mCAAmC,UAAU,WAAW,QAAQ;KAG9E,UAAA,oBAAC,MAAD;MACE,eAAc;MACd,WAAU;MAET,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"}
|
|
@@ -3,7 +3,7 @@ import { View } from "react-native";
|
|
|
3
3
|
import { jsx } from "react/jsx-runtime";
|
|
4
4
|
import { cva } from "class-variance-authority";
|
|
5
5
|
//#region src/components/card/card.tsx
|
|
6
|
-
const cardVariants = cva("rounded-
|
|
6
|
+
const cardVariants = cva("rounded-[8px] border border-border-default bg-surface-elevated shadow-md", {
|
|
7
7
|
variants: { padding: {
|
|
8
8
|
none: "",
|
|
9
9
|
md: "p-4",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"card.js","names":[],"sources":["../../../src/components/card/card.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { View, type ViewProps } from 'react-native'\nimport { cn } from '../../utils/cn'\n\nconst cardVariants = cva('rounded-
|
|
1
|
+
{"version":3,"file":"card.js","names":[],"sources":["../../../src/components/card/card.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { View, type ViewProps } from 'react-native'\nimport { cn } from '../../utils/cn'\n\nconst cardVariants = cva(\n 'rounded-[8px] border border-border-default bg-surface-elevated shadow-md',\n {\n variants: {\n padding: { none: '', md: 'p-4', lg: 'p-6' },\n },\n defaultVariants: { padding: 'md' },\n },\n)\n\nexport type CardProps = ViewProps & VariantProps<typeof cardVariants> & { className?: string }\n\n/**\n * Contêiner elevado (borda, raio e sombra) para agrupar conteúdo relacionado.\n * Use `padding=\"none\"` para mídia de ponta a ponta. No native, o texto não herda cor: use\n * `text-text-primary` nos `Text` filhos.\n */\nexport function Card({ className, padding, ...props }: CardProps) {\n return <View className={cn(cardVariants({ padding }), className)} {...props} />\n}\n"],"mappings":";;;;;AAMA,MAAM,eAAe,IACnB,4EACA;CACE,UAAU,EACR,SAAS;EAAE,MAAM;EAAI,IAAI;EAAO,IAAI;CAAM,EAC5C;CACA,iBAAiB,EAAE,SAAS,KAAK;AACnC,CACF;;;;;;AASA,SAAgB,KAAK,EAAE,WAAW,SAAS,GAAG,SAAoB;CAChE,OAAO,oBAAC,MAAD;EAAM,WAAW,GAAG,aAAa,EAAE,QAAQ,CAAC,GAAG,SAAS;EAAG,GAAI;CAAQ,CAAA;AAChF"}
|
|
@@ -54,7 +54,7 @@ function Combobox({ options, value, defaultValue = "", onValueChange, placeholde
|
|
|
54
54
|
onRequestClose: close,
|
|
55
55
|
matchAnchorWidth: true,
|
|
56
56
|
testID: testID ? `${testID}-list` : void 0,
|
|
57
|
-
className: "overflow-hidden rounded-
|
|
57
|
+
className: "overflow-hidden rounded-[8px] border border-select-dropdown-border bg-select-dropdown-bg shadow-md",
|
|
58
58
|
children: [/* @__PURE__ */ jsx(TextInput, {
|
|
59
59
|
autoFocus: true,
|
|
60
60
|
autoCorrect: false,
|
|
@@ -64,7 +64,7 @@ function Combobox({ options, value, defaultValue = "", onValueChange, placeholde
|
|
|
64
64
|
placeholder: searchPlaceholder,
|
|
65
65
|
placeholderTextColor: placeholderColor,
|
|
66
66
|
accessibilityLabel: searchPlaceholder,
|
|
67
|
-
className: "h-11 border-b border-select-dropdown-border px-3 text-
|
|
67
|
+
className: "h-11 border-b border-select-dropdown-border px-3 text-[16px] text-field-text"
|
|
68
68
|
}), /* @__PURE__ */ jsx(ScrollView, {
|
|
69
69
|
className: "max-h-60 p-1",
|
|
70
70
|
keyboardShouldPersistTaps: "handled",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"combobox.js","names":[],"sources":["../../../src/components/combobox/combobox.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { useState } from 'react'\nimport { Pressable, ScrollView, Text, TextInput, View } from 'react-native'\nimport { cn } from '../../utils/cn'\nimport { AnchoredOverlay, useAnchor } from '../_shared/anchored-overlay'\nimport { CheckIcon } from '../_shared/status'\nimport { useToken } from '../_shared/use-token'\nimport { type FieldSize, FieldTrigger } from '../select/field-trigger'\n\nexport type ComboboxOption = {\n /** Valor guardado e devolvido em `onValueChange`. */\n value: string\n /** Texto mostrado na lista e no gatilho; também é onde a busca procura. */\n label: string\n disabled?: boolean\n}\n\nexport type ComboboxProps = {\n options: ComboboxOption[]\n /** Valor selecionado (modo controlado). */\n value?: string\n defaultValue?: string\n /** Chamado com o `value` da opção escolhida. */\n onValueChange?: (value: string) => void\n /** Texto do gatilho sem valor. */\n placeholder?: string\n /** Placeholder do campo de busca dentro da lista. */\n searchPlaceholder?: string\n /** Texto quando a busca não acha nada. */\n emptyMessage?: string\n disabled?: boolean\n readOnly?: boolean\n error?: boolean\n size?: FieldSize\n className?: string\n accessibilityLabel?: string\n accessibilityHint?: string\n testID?: string\n}\n\n// busca sem distinguir maiúsculas nem acentos: \"sao\" acha \"São Paulo\"\nconst fold = (text: string) => text.normalize('NFD').replace(/\\p{M}/gu, '').toLowerCase()\n\n/**\n * Campo de escolha com busca. No native o gatilho é um botão (como o Select) e a busca fica num\n * TextInput no topo da lista, que abre já focado: digitar no próprio gatilho não funciona com a\n * lista numa camada acima dele. Diferenças do web: sem navegação por teclado e sem limpar o valor\n * apagando o texto (`onValueChange` só recebe valores de opções).\n */\nexport function Combobox({\n options,\n value,\n defaultValue = '',\n onValueChange,\n placeholder = 'Selecione...',\n searchPlaceholder = 'Buscar...',\n emptyMessage = 'Nenhum resultado',\n size,\n error,\n disabled,\n readOnly,\n className,\n accessibilityLabel,\n accessibilityHint,\n testID,\n}: ComboboxProps) {\n const [inner, setInner] = useState(defaultValue)\n const [query, setQuery] = useState('')\n const anchor = useAnchor()\n const placeholderColor = useToken('--color-field-text-placeholder')\n const checkColor = useToken('--color-select-option-text')\n const selected = value ?? inner\n const selectedLabel = options.find((o) => o.value === selected)?.label\n const open = anchor.visible && !disabled && !readOnly\n const shown = query ? options.filter((o) => fold(o.label).includes(fold(query))) : options\n\n function close() {\n anchor.close()\n setQuery('')\n }\n\n function choose(option: ComboboxOption) {\n if (value === undefined) setInner(option.value)\n onValueChange?.(option.value)\n close()\n }\n\n return (\n <>\n <FieldTrigger\n anchorRef={anchor.ref}\n icon=\"chevron\"\n text={selectedLabel}\n placeholder={placeholder}\n size={size}\n error={error}\n open={open}\n disabled={disabled}\n readOnly={readOnly}\n onPress={anchor.open}\n className={className}\n accessibilityLabel={accessibilityLabel}\n accessibilityHint={accessibilityHint}\n testID={testID}\n />\n <AnchoredOverlay\n visible={open}\n rect={anchor.rect}\n onRequestClose={close}\n matchAnchorWidth\n testID={testID ? `${testID}-list` : undefined}\n className=\"overflow-hidden rounded-
|
|
1
|
+
{"version":3,"file":"combobox.js","names":[],"sources":["../../../src/components/combobox/combobox.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { useState } from 'react'\nimport { Pressable, ScrollView, Text, TextInput, View } from 'react-native'\nimport { cn } from '../../utils/cn'\nimport { AnchoredOverlay, useAnchor } from '../_shared/anchored-overlay'\nimport { CheckIcon } from '../_shared/status'\nimport { useToken } from '../_shared/use-token'\nimport { type FieldSize, FieldTrigger } from '../select/field-trigger'\n\nexport type ComboboxOption = {\n /** Valor guardado e devolvido em `onValueChange`. */\n value: string\n /** Texto mostrado na lista e no gatilho; também é onde a busca procura. */\n label: string\n disabled?: boolean\n}\n\nexport type ComboboxProps = {\n options: ComboboxOption[]\n /** Valor selecionado (modo controlado). */\n value?: string\n defaultValue?: string\n /** Chamado com o `value` da opção escolhida. */\n onValueChange?: (value: string) => void\n /** Texto do gatilho sem valor. */\n placeholder?: string\n /** Placeholder do campo de busca dentro da lista. */\n searchPlaceholder?: string\n /** Texto quando a busca não acha nada. */\n emptyMessage?: string\n disabled?: boolean\n readOnly?: boolean\n error?: boolean\n size?: FieldSize\n className?: string\n accessibilityLabel?: string\n accessibilityHint?: string\n testID?: string\n}\n\n// busca sem distinguir maiúsculas nem acentos: \"sao\" acha \"São Paulo\"\nconst fold = (text: string) => text.normalize('NFD').replace(/\\p{M}/gu, '').toLowerCase()\n\n/**\n * Campo de escolha com busca. No native o gatilho é um botão (como o Select) e a busca fica num\n * TextInput no topo da lista, que abre já focado: digitar no próprio gatilho não funciona com a\n * lista numa camada acima dele. Diferenças do web: sem navegação por teclado e sem limpar o valor\n * apagando o texto (`onValueChange` só recebe valores de opções).\n */\nexport function Combobox({\n options,\n value,\n defaultValue = '',\n onValueChange,\n placeholder = 'Selecione...',\n searchPlaceholder = 'Buscar...',\n emptyMessage = 'Nenhum resultado',\n size,\n error,\n disabled,\n readOnly,\n className,\n accessibilityLabel,\n accessibilityHint,\n testID,\n}: ComboboxProps) {\n const [inner, setInner] = useState(defaultValue)\n const [query, setQuery] = useState('')\n const anchor = useAnchor()\n const placeholderColor = useToken('--color-field-text-placeholder')\n const checkColor = useToken('--color-select-option-text')\n const selected = value ?? inner\n const selectedLabel = options.find((o) => o.value === selected)?.label\n const open = anchor.visible && !disabled && !readOnly\n const shown = query ? options.filter((o) => fold(o.label).includes(fold(query))) : options\n\n function close() {\n anchor.close()\n setQuery('')\n }\n\n function choose(option: ComboboxOption) {\n if (value === undefined) setInner(option.value)\n onValueChange?.(option.value)\n close()\n }\n\n return (\n <>\n <FieldTrigger\n anchorRef={anchor.ref}\n icon=\"chevron\"\n text={selectedLabel}\n placeholder={placeholder}\n size={size}\n error={error}\n open={open}\n disabled={disabled}\n readOnly={readOnly}\n onPress={anchor.open}\n className={className}\n accessibilityLabel={accessibilityLabel}\n accessibilityHint={accessibilityHint}\n testID={testID}\n />\n <AnchoredOverlay\n visible={open}\n rect={anchor.rect}\n onRequestClose={close}\n matchAnchorWidth\n testID={testID ? `${testID}-list` : undefined}\n className=\"overflow-hidden rounded-[8px] border border-select-dropdown-border bg-select-dropdown-bg shadow-md\"\n >\n <TextInput\n autoFocus\n autoCorrect={false}\n autoCapitalize=\"none\"\n value={query}\n onChangeText={setQuery}\n placeholder={searchPlaceholder}\n placeholderTextColor={placeholderColor}\n accessibilityLabel={searchPlaceholder}\n className=\"h-11 border-b border-select-dropdown-border px-3 text-[16px] text-field-text\"\n />\n <ScrollView className=\"max-h-60 p-1\" keyboardShouldPersistTaps=\"handled\">\n {shown.length === 0 ? (\n <Text className=\"px-3 py-2 text-base text-text-secondary\">{emptyMessage}</Text>\n ) : (\n shown.map((option) => {\n const isSelected = option.value === selected\n return (\n <Pressable\n key={option.value}\n role=\"option\"\n accessibilityLabel={option.label}\n accessibilityState={{ selected: isSelected, disabled: Boolean(option.disabled) }}\n disabled={option.disabled}\n onPress={() => choose(option)}\n className={cn(\n 'flex-row items-center gap-2 rounded-sm px-3 py-2 active:bg-select-option-bg-hover',\n isSelected && 'bg-select-option-bg-selected',\n )}\n >\n <Text\n className={cn(\n 'flex-1 text-base text-select-option-text',\n option.disabled && 'text-select-option-text-disabled',\n )}\n >\n {option.label}\n </Text>\n {isSelected ? (\n <View className=\"size-4 items-center justify-center\">\n <CheckIcon color={checkColor} />\n </View>\n ) : null}\n </Pressable>\n )\n })\n )}\n </ScrollView>\n </AnchoredOverlay>\n </>\n )\n}\n"],"mappings":";;;;;;;;;AA0CA,MAAM,QAAQ,SAAiB,KAAK,UAAU,KAAK,CAAC,CAAC,QAAQ,WAAW,EAAE,CAAC,CAAC,YAAY;;;;;;;AAQxF,SAAgB,SAAS,EACvB,SACA,OACA,eAAe,IACf,eACA,cAAc,gBACd,oBAAoB,aACpB,eAAe,oBACf,MACA,OACA,UACA,UACA,WACA,oBACA,mBACA,UACgB;CAChB,MAAM,CAAC,OAAO,YAAY,SAAS,YAAY;CAC/C,MAAM,CAAC,OAAO,YAAY,SAAS,EAAE;CACrC,MAAM,SAAS,UAAU;CACzB,MAAM,mBAAmB,SAAS,gCAAgC;CAClE,MAAM,aAAa,SAAS,4BAA4B;CACxD,MAAM,WAAW,SAAS;CAC1B,MAAM,gBAAgB,QAAQ,MAAM,MAAM,EAAE,UAAU,QAAQ,CAAC,EAAE;CACjE,MAAM,OAAO,OAAO,WAAW,CAAC,YAAY,CAAC;CAC7C,MAAM,QAAQ,QAAQ,QAAQ,QAAQ,MAAM,KAAK,EAAE,KAAK,CAAC,CAAC,SAAS,KAAK,KAAK,CAAC,CAAC,IAAI;CAEnF,SAAS,QAAQ;EACf,OAAO,MAAM;EACb,SAAS,EAAE;CACb;CAEA,SAAS,OAAO,QAAwB;EACtC,IAAI,UAAU,KAAA,GAAW,SAAS,OAAO,KAAK;EAC9C,gBAAgB,OAAO,KAAK;EAC5B,MAAM;CACR;CAEA,OACE,qBAAA,YAAA,EAAA,UAAA,CACE,oBAAC,cAAD;EACE,WAAW,OAAO;EAClB,MAAK;EACL,MAAM;EACO;EACP;EACC;EACD;EACI;EACA;EACV,SAAS,OAAO;EACL;EACS;EACD;EACX;CACT,CAAA,GACD,qBAAC,iBAAD;EACE,SAAS;EACT,MAAM,OAAO;EACb,gBAAgB;EAChB,kBAAA;EACA,QAAQ,SAAS,GAAG,OAAO,SAAS,KAAA;EACpC,WAAU;EANZ,UAAA,CAQE,oBAAC,WAAD;GACE,WAAA;GACA,aAAa;GACb,gBAAe;GACf,OAAO;GACP,cAAc;GACd,aAAa;GACb,sBAAsB;GACtB,oBAAoB;GACpB,WAAU;EACX,CAAA,GACD,oBAAC,YAAD;GAAY,WAAU;GAAe,2BAA0B;GAC5D,UAAA,MAAM,WAAW,IAChB,oBAAC,MAAD;IAAM,WAAU;IAA2C,UAAA;GAAmB,CAAA,IAE9E,MAAM,KAAK,WAAW;IACpB,MAAM,aAAa,OAAO,UAAU;IACpC,OACE,qBAAC,WAAD;KAEE,MAAK;KACL,oBAAoB,OAAO;KAC3B,oBAAoB;MAAE,UAAU;MAAY,UAAU,QAAQ,OAAO,QAAQ;KAAE;KAC/E,UAAU,OAAO;KACjB,eAAe,OAAO,MAAM;KAC5B,WAAW,GACT,qFACA,cAAc,8BAChB;KAVF,UAAA,CAYE,oBAAC,MAAD;MACE,WAAW,GACT,4CACA,OAAO,YAAY,kCACrB;MAEC,UAAA,OAAO;KACJ,CAAA,GACL,aACC,oBAAC,MAAD;MAAM,WAAU;MACd,UAAA,oBAAC,WAAD,EAAW,OAAO,WAAa,CAAA;KAC3B,CAAA,IACJ,IACK;IAxBJ,GAAA,OAAO,KAwBH;GAEf,CAAC;EAEO,CAAA,CACG;CACjB,CAAA,CAAA,EAAA,CAAA;AAEN"}
|
|
@@ -21,7 +21,7 @@ function DateSheet({ visible, onRequestClose, children, testID }) {
|
|
|
21
21
|
testID: testID ? `${testID}-backdrop` : void 0
|
|
22
22
|
}), /* @__PURE__ */ jsx(View, {
|
|
23
23
|
testID,
|
|
24
|
-
className: "max-w-full rounded-
|
|
24
|
+
className: "max-w-full rounded-[12px] border border-border-default bg-surface-elevated p-3 shadow-md",
|
|
25
25
|
children
|
|
26
26
|
})]
|
|
27
27
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"date-sheet.js","names":[],"sources":["../../../src/components/date-picker/date-sheet.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport type { ReactNode } from 'react'\nimport { Modal, View } from 'react-native'\nimport { Backdrop } from '../_shared/backdrop'\n\n/**\n * Painel do calendário (uso interno). Decisão: Modal nativo centralizado com Backdrop em vez de\n * camada ancorada, porque o calendário (280 de largura, mais os atalhos no período) não cabe de\n * forma previsível junto a gatilhos estreitos ou perto da borda da tela.\n */\nexport function DateSheet({\n visible,\n onRequestClose,\n children,\n testID,\n}: {\n visible: boolean\n onRequestClose: () => void\n children: ReactNode\n testID?: string\n}) {\n return (\n <Modal\n transparent\n visible={visible}\n animationType=\"fade\"\n onRequestClose={onRequestClose}\n statusBarTranslucent\n >\n <View className=\"flex-1 items-center justify-center p-4\">\n <Backdrop onPress={onRequestClose} testID={testID ? `${testID}-backdrop` : undefined} />\n <View\n testID={testID}\n className=\"max-w-full rounded-
|
|
1
|
+
{"version":3,"file":"date-sheet.js","names":[],"sources":["../../../src/components/date-picker/date-sheet.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport type { ReactNode } from 'react'\nimport { Modal, View } from 'react-native'\nimport { Backdrop } from '../_shared/backdrop'\n\n/**\n * Painel do calendário (uso interno). Decisão: Modal nativo centralizado com Backdrop em vez de\n * camada ancorada, porque o calendário (280 de largura, mais os atalhos no período) não cabe de\n * forma previsível junto a gatilhos estreitos ou perto da borda da tela.\n */\nexport function DateSheet({\n visible,\n onRequestClose,\n children,\n testID,\n}: {\n visible: boolean\n onRequestClose: () => void\n children: ReactNode\n testID?: string\n}) {\n return (\n <Modal\n transparent\n visible={visible}\n animationType=\"fade\"\n onRequestClose={onRequestClose}\n statusBarTranslucent\n >\n <View className=\"flex-1 items-center justify-center p-4\">\n <Backdrop onPress={onRequestClose} testID={testID ? `${testID}-backdrop` : undefined} />\n <View\n testID={testID}\n className=\"max-w-full rounded-[12px] border border-border-default bg-surface-elevated p-3 shadow-md\"\n >\n {children}\n </View>\n </View>\n </Modal>\n )\n}\n"],"mappings":";;;;;;;;;AAWA,SAAgB,UAAU,EACxB,SACA,gBACA,UACA,UAMC;CACD,OACE,oBAAC,OAAD;EACE,aAAA;EACS;EACT,eAAc;EACE;EAChB,sBAAA;EAEA,UAAA,qBAAC,MAAD;GAAM,WAAU;GAAhB,UAAA,CACE,oBAAC,UAAD;IAAU,SAAS;IAAgB,QAAQ,SAAS,GAAG,OAAO,aAAa,KAAA;GAAY,CAAA,GACvF,oBAAC,MAAD;IACU;IACR,WAAU;IAET;GACG,CAAA,CACF;;CACD,CAAA;AAEX"}
|
|
@@ -69,7 +69,7 @@ function Drawer({ open, defaultOpen, onOpenChange, title, children, footer, trig
|
|
|
69
69
|
accessibilityElementsHidden: true,
|
|
70
70
|
importantForAccessibility: "no-hide-descendants",
|
|
71
71
|
className: "h-6 shrink-0 items-center justify-center",
|
|
72
|
-
children: /* @__PURE__ */ jsx(View, { className: "h-1 w-10 rounded-
|
|
72
|
+
children: /* @__PURE__ */ jsx(View, { className: "h-1 w-10 rounded-[12px] bg-border-strong" })
|
|
73
73
|
});
|
|
74
74
|
return /* @__PURE__ */ jsxs(Fragment$1, { children: [injectHandler(trigger, "onPress", () => setOpen(true)), /* @__PURE__ */ jsx(Modal, {
|
|
75
75
|
transparent: true,
|
|
@@ -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, 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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"}
|
|
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-[12px] 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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"}
|
|
@@ -24,7 +24,7 @@ function Gallery({ className, items, columns, openLabel = (item, i) => `Ampliar
|
|
|
24
24
|
accessibilityRole: "imagebutton",
|
|
25
25
|
accessibilityLabel: openLabel(item, i),
|
|
26
26
|
onPress: () => setOpened(i),
|
|
27
|
-
className: "overflow-hidden rounded-
|
|
27
|
+
className: "overflow-hidden rounded-[8px] bg-bg-skeleton",
|
|
28
28
|
style: {
|
|
29
29
|
width: size,
|
|
30
30
|
aspectRatio: 4 / 3
|
|
@@ -1 +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-
|
|
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-[8px] 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"}
|
|
@@ -2,7 +2,11 @@ import { ReactNode } from "react";
|
|
|
2
2
|
import { TextInputProps } from "react-native";
|
|
3
3
|
import { VariantProps } from "class-variance-authority";
|
|
4
4
|
//#region src/components/input/input.d.ts
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Estilo do campo, compartilhado com o Textarea (não faz parte da API pública).
|
|
7
|
+
* O tamanho da fonte vai em px (`text-[16px]`) e não em `text-base`: este também define
|
|
8
|
+
* `line-height`, e no iOS um `TextInput` de uma linha com `line-height` maior que a fonte desce o texto.
|
|
9
|
+
*/
|
|
6
10
|
export declare const inputVariants: (props?: ({
|
|
7
11
|
size?: "lg" | "md" | "sm" | null | undefined;
|
|
8
12
|
error?: boolean | null | undefined;
|
|
@@ -5,13 +5,17 @@ import { TextInput, View } from "react-native";
|
|
|
5
5
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
6
|
import { cva } from "class-variance-authority";
|
|
7
7
|
//#region src/components/input/input.tsx
|
|
8
|
-
/**
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Estilo do campo, compartilhado com o Textarea (não faz parte da API pública).
|
|
10
|
+
* O tamanho da fonte vai em px (`text-[16px]`) e não em `text-base`: este também define
|
|
11
|
+
* `line-height`, e no iOS um `TextInput` de uma linha com `line-height` maior que a fonte desce o texto.
|
|
12
|
+
*/
|
|
13
|
+
const inputVariants = cva("w-full rounded-[8px] border border-field-border bg-field-bg py-0 text-field-text focus:border-field-focus-ring", {
|
|
10
14
|
variants: {
|
|
11
15
|
size: {
|
|
12
|
-
lg: "h-14 px-4 text-
|
|
13
|
-
md: "h-12 px-3 text-
|
|
14
|
-
sm: "h-10 px-2 text-
|
|
16
|
+
lg: "h-14 px-4 text-[16px]",
|
|
17
|
+
md: "h-12 px-3 text-[16px]",
|
|
18
|
+
sm: "h-10 px-2 text-[14px]"
|
|
15
19
|
},
|
|
16
20
|
error: {
|
|
17
21
|
true: "border-field-border-error focus:border-field-border-error",
|
|
@@ -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 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
|
|
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/**\n * Estilo do campo, compartilhado com o Textarea (não faz parte da API pública).\n * O tamanho da fonte vai em px (`text-[16px]`) e não em `text-base`: este também define\n * `line-height`, e no iOS um `TextInput` de uma linha com `line-height` maior que a fonte desce o texto.\n */\nexport const inputVariants = cva(\n 'w-full rounded-[8px] 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-[16px]',\n md: 'h-12 px-3 text-[16px]',\n sm: 'h-10 px-2 text-[14px]',\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":";;;;;;;;;;;;AAaA,MAAa,gBAAgB,IAC3B,kHACA;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"}
|
|
@@ -31,7 +31,7 @@ function Menu({ open, defaultOpen, onOpenChange, side = "bottom", trigger, child
|
|
|
31
31
|
onRequestClose: close,
|
|
32
32
|
placement: side,
|
|
33
33
|
testID,
|
|
34
|
-
className: cn("min-w-[180px] rounded-
|
|
34
|
+
className: cn("min-w-[180px] rounded-[8px] border border-border-default bg-surface-elevated p-1 shadow-md", className),
|
|
35
35
|
children: /* @__PURE__ */ jsx(CloseContext.Provider, {
|
|
36
36
|
value: close,
|
|
37
37
|
children: /* @__PURE__ */ jsx(View, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"menu.js","names":[],"sources":["../../../src/components/menu/menu.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { createContext, type ReactNode, useContext } from 'react'\nimport { Pressable, type PressableProps, Text, View } from 'react-native'\nimport { cn } from '../../utils/cn'\nimport { AnchoredOverlay } from '../_shared/anchored-overlay'\nimport { AnchorTrigger, useControlledAnchor } from '../_shared/overlay-anchor'\nimport { useOpenState } from '../_shared/overlay-state'\n\nconst CloseContext = createContext<(() => void) | null>(null)\n\nexport type MenuProps = {\n open?: boolean\n defaultOpen?: boolean\n onOpenChange?: (open: boolean) => void\n /** Lado do gatilho em que o menu abre; inverte se não couber. Padrão: `bottom`. */\n side?: 'top' | 'bottom'\n /** Elemento que abre o menu (ex.: um `<Button>`); recebe `onPress`. */\n trigger: ReactNode\n /** Os `MenuItem`, `MenuLabel` e `MenuSeparator` do menu. */\n children: ReactNode\n className?: string\n testID?: string\n}\n\n/**\n * Menu de ações aberto por um botão, ancorado nele. Tocar num item o executa e fecha o menu;\n * tocar fora ou o botão voltar do Android também fecha. Diferenças do web: sem navegação por\n * teclado, sem `align` (alinha à esquerda do gatilho) e sem lados `left`/`right`.\n */\nexport function Menu({\n open,\n defaultOpen,\n onOpenChange,\n side = 'bottom',\n trigger,\n children,\n className,\n testID,\n}: MenuProps) {\n const [isOpen, setOpen] = useOpenState({ open, defaultOpen, onOpenChange })\n const { ref, rect } = useControlledAnchor(isOpen)\n const close = () => setOpen(false)\n\n return (\n <>\n <AnchorTrigger anchorRef={ref} event=\"onPress\" onTrigger={() => setOpen(!isOpen)}>\n {trigger}\n </AnchorTrigger>\n <AnchoredOverlay\n visible={isOpen}\n rect={rect}\n onRequestClose={close}\n placement={side}\n testID={testID}\n className={cn(\n 'min-w-[180px] rounded-
|
|
1
|
+
{"version":3,"file":"menu.js","names":[],"sources":["../../../src/components/menu/menu.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { createContext, type ReactNode, useContext } from 'react'\nimport { Pressable, type PressableProps, Text, View } from 'react-native'\nimport { cn } from '../../utils/cn'\nimport { AnchoredOverlay } from '../_shared/anchored-overlay'\nimport { AnchorTrigger, useControlledAnchor } from '../_shared/overlay-anchor'\nimport { useOpenState } from '../_shared/overlay-state'\n\nconst CloseContext = createContext<(() => void) | null>(null)\n\nexport type MenuProps = {\n open?: boolean\n defaultOpen?: boolean\n onOpenChange?: (open: boolean) => void\n /** Lado do gatilho em que o menu abre; inverte se não couber. Padrão: `bottom`. */\n side?: 'top' | 'bottom'\n /** Elemento que abre o menu (ex.: um `<Button>`); recebe `onPress`. */\n trigger: ReactNode\n /** Os `MenuItem`, `MenuLabel` e `MenuSeparator` do menu. */\n children: ReactNode\n className?: string\n testID?: string\n}\n\n/**\n * Menu de ações aberto por um botão, ancorado nele. Tocar num item o executa e fecha o menu;\n * tocar fora ou o botão voltar do Android também fecha. Diferenças do web: sem navegação por\n * teclado, sem `align` (alinha à esquerda do gatilho) e sem lados `left`/`right`.\n */\nexport function Menu({\n open,\n defaultOpen,\n onOpenChange,\n side = 'bottom',\n trigger,\n children,\n className,\n testID,\n}: MenuProps) {\n const [isOpen, setOpen] = useOpenState({ open, defaultOpen, onOpenChange })\n const { ref, rect } = useControlledAnchor(isOpen)\n const close = () => setOpen(false)\n\n return (\n <>\n <AnchorTrigger anchorRef={ref} event=\"onPress\" onTrigger={() => setOpen(!isOpen)}>\n {trigger}\n </AnchorTrigger>\n <AnchoredOverlay\n visible={isOpen}\n rect={rect}\n onRequestClose={close}\n placement={side}\n testID={testID}\n className={cn(\n 'min-w-[180px] rounded-[8px] border border-border-default bg-surface-elevated p-1 shadow-md',\n className,\n )}\n >\n <CloseContext.Provider value={close}>\n <View accessibilityRole=\"menu\">{children}</View>\n </CloseContext.Provider>\n </AnchoredOverlay>\n </>\n )\n}\n\nexport type MenuItemProps = Omit<PressableProps, 'children' | 'style'> & {\n /** Chamado ao escolher o item (antes de o menu fechar). Equivale ao `onSelect` do web. */\n onSelect?: () => void\n /** Ícone à esquerda do rótulo (16px). */\n icon?: ReactNode\n /** Ação que desfaz dados (excluir, remover): texto em vermelho. */\n destructive?: boolean\n children: string\n className?: string\n}\n\n/** Uma ação do menu. Usa `onSelect` (como o web); `onPress` também é chamado. */\nexport function MenuItem({\n className,\n icon,\n destructive = false,\n disabled,\n onSelect,\n onPress,\n children,\n accessibilityState,\n ...props\n}: MenuItemProps) {\n const close = useContext(CloseContext)\n\n return (\n <Pressable\n accessibilityRole=\"menuitem\"\n accessibilityState={{ ...accessibilityState, disabled: Boolean(disabled) }}\n disabled={disabled}\n onPress={(event) => {\n onPress?.(event)\n onSelect?.()\n close?.()\n }}\n className={cn(\n 'flex-row items-center gap-2 rounded-sm px-3 py-2 active:bg-bg-tertiary',\n className,\n )}\n {...props}\n >\n {icon}\n <Text\n className={cn(\n 'text-sm leading-5',\n disabled\n ? 'text-text-disabled'\n : destructive\n ? 'text-status-error-text'\n : 'text-text-primary',\n )}\n >\n {children}\n </Text>\n </Pressable>\n )\n}\n\nexport type MenuLabelProps = { children: string; className?: string }\n\n/** Título de um grupo de itens. */\nexport function MenuLabel({ className, children }: MenuLabelProps) {\n return (\n <Text\n className={cn('px-3 py-1.5 text-xs font-medium leading-4 text-text-secondary', className)}\n >\n {children}\n </Text>\n )\n}\n\nexport type MenuSeparatorProps = { className?: string }\n\n/** Linha entre grupos de itens. */\nexport function MenuSeparator({ className }: MenuSeparatorProps) {\n return <View className={cn('-mx-1 my-1 h-px bg-border-default', className)} />\n}\n"],"mappings":";;;;;;;;AASA,MAAM,eAAe,cAAmC,IAAI;;;;;;AAqB5D,SAAgB,KAAK,EACnB,MACA,aACA,cACA,OAAO,UACP,SACA,UACA,WACA,UACY;CACZ,MAAM,CAAC,QAAQ,WAAW,aAAa;EAAE;EAAM;EAAa;CAAa,CAAC;CAC1E,MAAM,EAAE,KAAK,SAAS,oBAAoB,MAAM;CAChD,MAAM,cAAc,QAAQ,KAAK;CAEjC,OACE,qBAAA,YAAA,EAAA,UAAA,CACE,oBAAC,eAAD;EAAe,WAAW;EAAK,OAAM;EAAU,iBAAiB,QAAQ,CAAC,MAAM;EAC5E,UAAA;CACY,CAAA,GACf,oBAAC,iBAAD;EACE,SAAS;EACH;EACN,gBAAgB;EAChB,WAAW;EACH;EACR,WAAW,GACT,8FACA,SACF;EAEA,UAAA,oBAAC,aAAa,UAAd;GAAuB,OAAO;GAC5B,UAAA,oBAAC,MAAD;IAAM,mBAAkB;IAAQ;GAAe,CAAA;EAC1B,CAAA;CACR,CAAA,CACjB,EAAA,CAAA;AAEN;;AAcA,SAAgB,SAAS,EACvB,WACA,MACA,cAAc,OACd,UACA,UACA,SACA,UACA,oBACA,GAAG,SACa;CAChB,MAAM,QAAQ,WAAW,YAAY;CAErC,OACE,qBAAC,WAAD;EACE,mBAAkB;EAClB,oBAAoB;GAAE,GAAG;GAAoB,UAAU,QAAQ,QAAQ;EAAE;EAC/D;EACV,UAAU,UAAU;GAClB,UAAU,KAAK;GACf,WAAW;GACX,QAAQ;EACV;EACA,WAAW,GACT,0EACA,SACF;EACA,GAAI;EAbN,UAAA,CAeG,MACD,oBAAC,MAAD;GACE,WAAW,GACT,qBACA,WACI,uBACA,cACE,2BACA,mBACR;GAEC;EACG,CAAA,CACG;;AAEf;;AAKA,SAAgB,UAAU,EAAE,WAAW,YAA4B;CACjE,OACE,oBAAC,MAAD;EACE,WAAW,GAAG,iEAAiE,SAAS;EAEvF;CACG,CAAA;AAEV;;AAKA,SAAgB,cAAc,EAAE,aAAiC;CAC/D,OAAO,oBAAC,MAAD,EAAM,WAAW,GAAG,qCAAqC,SAAS,EAAI,CAAA;AAC/E"}
|
|
@@ -9,7 +9,7 @@ import { Animated, Modal, Pressable, ScrollView, StyleSheet, Text, View } from "
|
|
|
9
9
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
10
10
|
import { cva } from "class-variance-authority";
|
|
11
11
|
//#region src/components/modal/modal.tsx
|
|
12
|
-
const modalVariants = cva("max-h-full w-full overflow-hidden rounded-
|
|
12
|
+
const modalVariants = cva("max-h-full w-full overflow-hidden rounded-[12px] border border-border-default bg-surface-elevated shadow-2xl", {
|
|
13
13
|
variants: { size: {
|
|
14
14
|
sm: "max-w-[480px]",
|
|
15
15
|
md: "max-w-[640px]",
|
|
@@ -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 {\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-
|
|
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-[12px] 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,gHACA;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"}
|
|
@@ -32,7 +32,7 @@ function Popover({ open, defaultOpen, onOpenChange, side = "bottom", trigger, la
|
|
|
32
32
|
placement: side,
|
|
33
33
|
offset: 8,
|
|
34
34
|
testID,
|
|
35
|
-
className: cn("max-w-xs rounded-
|
|
35
|
+
className: cn("max-w-xs rounded-[8px] border border-border-default bg-surface-elevated p-4 shadow-md", className),
|
|
36
36
|
children: /* @__PURE__ */ jsx(CloseContext.Provider, {
|
|
37
37
|
value: close,
|
|
38
38
|
children: /* @__PURE__ */ jsx(View, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"popover.js","names":[],"sources":["../../../src/components/popover/popover.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { createContext, type ReactNode, useContext } from 'react'\nimport { View } from 'react-native'\nimport { cn } from '../../utils/cn'\nimport { AnchoredOverlay } from '../_shared/anchored-overlay'\nimport { AnchorTrigger, useControlledAnchor } from '../_shared/overlay-anchor'\nimport { injectHandler, useOpenState } from '../_shared/overlay-state'\n\nconst CloseContext = createContext<(() => void) | null>(null)\n\nexport type PopoverProps = {\n open?: boolean\n defaultOpen?: boolean\n onOpenChange?: (open: boolean) => void\n /** Lado do gatilho em que o painel abre; inverte se não couber. Padrão: `bottom`. */\n side?: 'top' | 'bottom'\n /** Elemento que abre o popover (ex.: um `<Button>`); recebe `onPress`. */\n trigger: ReactNode\n /** Nome acessível do painel: descreve o que há nele, ex.: \"Filtros\". */\n label: string\n /** O conteúdo do popover. Pode ter elementos interativos. */\n children: ReactNode\n className?: string\n testID?: string\n}\n\n/**\n * Painel flutuante ancorado ao gatilho, aberto por toque. O conteúdo pode ser interativo; tocar\n * fora ou o botão voltar do Android fecha. Diferenças do web: sem `align` (o painel alinha à\n * esquerda do gatilho), sem lados `left`/`right` e sem devolução de foco.\n */\nexport function Popover({\n open,\n defaultOpen,\n onOpenChange,\n side = 'bottom',\n trigger,\n label,\n children,\n className,\n testID,\n}: PopoverProps) {\n const [isOpen, setOpen] = useOpenState({ open, defaultOpen, onOpenChange })\n const { ref, rect } = useControlledAnchor(isOpen)\n const close = () => setOpen(false)\n\n return (\n <>\n <AnchorTrigger anchorRef={ref} event=\"onPress\" onTrigger={() => setOpen(!isOpen)}>\n {trigger}\n </AnchorTrigger>\n <AnchoredOverlay\n visible={isOpen}\n rect={rect}\n onRequestClose={close}\n placement={side}\n offset={8}\n testID={testID}\n className={cn(\n 'max-w-xs rounded-
|
|
1
|
+
{"version":3,"file":"popover.js","names":[],"sources":["../../../src/components/popover/popover.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { createContext, type ReactNode, useContext } from 'react'\nimport { View } from 'react-native'\nimport { cn } from '../../utils/cn'\nimport { AnchoredOverlay } from '../_shared/anchored-overlay'\nimport { AnchorTrigger, useControlledAnchor } from '../_shared/overlay-anchor'\nimport { injectHandler, useOpenState } from '../_shared/overlay-state'\n\nconst CloseContext = createContext<(() => void) | null>(null)\n\nexport type PopoverProps = {\n open?: boolean\n defaultOpen?: boolean\n onOpenChange?: (open: boolean) => void\n /** Lado do gatilho em que o painel abre; inverte se não couber. Padrão: `bottom`. */\n side?: 'top' | 'bottom'\n /** Elemento que abre o popover (ex.: um `<Button>`); recebe `onPress`. */\n trigger: ReactNode\n /** Nome acessível do painel: descreve o que há nele, ex.: \"Filtros\". */\n label: string\n /** O conteúdo do popover. Pode ter elementos interativos. */\n children: ReactNode\n className?: string\n testID?: string\n}\n\n/**\n * Painel flutuante ancorado ao gatilho, aberto por toque. O conteúdo pode ser interativo; tocar\n * fora ou o botão voltar do Android fecha. Diferenças do web: sem `align` (o painel alinha à\n * esquerda do gatilho), sem lados `left`/`right` e sem devolução de foco.\n */\nexport function Popover({\n open,\n defaultOpen,\n onOpenChange,\n side = 'bottom',\n trigger,\n label,\n children,\n className,\n testID,\n}: PopoverProps) {\n const [isOpen, setOpen] = useOpenState({ open, defaultOpen, onOpenChange })\n const { ref, rect } = useControlledAnchor(isOpen)\n const close = () => setOpen(false)\n\n return (\n <>\n <AnchorTrigger anchorRef={ref} event=\"onPress\" onTrigger={() => setOpen(!isOpen)}>\n {trigger}\n </AnchorTrigger>\n <AnchoredOverlay\n visible={isOpen}\n rect={rect}\n onRequestClose={close}\n placement={side}\n offset={8}\n testID={testID}\n className={cn(\n 'max-w-xs rounded-[8px] border border-border-default bg-surface-elevated p-4 shadow-md',\n className,\n )}\n >\n <CloseContext.Provider value={close}>\n <View accessibilityLabel={label} accessibilityViewIsModal>\n {children}\n </View>\n </CloseContext.Provider>\n </AnchoredOverlay>\n </>\n )\n}\n\n/** Fecha o popover ao pressionar. Envolva um `<Button>` dentro do conteúdo (injeta `onPress`). */\nexport function PopoverClose({ children }: { children: ReactNode }) {\n const close = useContext(CloseContext)\n return <>{injectHandler(children, 'onPress', () => close?.())}</>\n}\n"],"mappings":";;;;;;;;AASA,MAAM,eAAe,cAAmC,IAAI;;;;;;AAuB5D,SAAgB,QAAQ,EACtB,MACA,aACA,cACA,OAAO,UACP,SACA,OACA,UACA,WACA,UACe;CACf,MAAM,CAAC,QAAQ,WAAW,aAAa;EAAE;EAAM;EAAa;CAAa,CAAC;CAC1E,MAAM,EAAE,KAAK,SAAS,oBAAoB,MAAM;CAChD,MAAM,cAAc,QAAQ,KAAK;CAEjC,OACE,qBAAA,YAAA,EAAA,UAAA,CACE,oBAAC,eAAD;EAAe,WAAW;EAAK,OAAM;EAAU,iBAAiB,QAAQ,CAAC,MAAM;EAC5E,UAAA;CACY,CAAA,GACf,oBAAC,iBAAD;EACE,SAAS;EACH;EACN,gBAAgB;EAChB,WAAW;EACX,QAAQ;EACA;EACR,WAAW,GACT,yFACA,SACF;EAEA,UAAA,oBAAC,aAAa,UAAd;GAAuB,OAAO;GAC5B,UAAA,oBAAC,MAAD;IAAM,oBAAoB;IAAO,0BAAA;IAC9B;GACG,CAAA;EACe,CAAA;CACR,CAAA,CACjB,EAAA,CAAA;AAEN;;AAGA,SAAgB,aAAa,EAAE,YAAqC;CAClE,MAAM,QAAQ,WAAW,YAAY;CACrC,OAAO,oBAAA,YAAA,EAAA,UAAG,cAAc,UAAU,iBAAiB,QAAQ,CAAC,EAAI,CAAA;AAClE"}
|
|
@@ -25,11 +25,11 @@ function SegmentedControl({ className, value, defaultValue, onValueChange, child
|
|
|
25
25
|
},
|
|
26
26
|
children: /* @__PURE__ */ jsxs(View, {
|
|
27
27
|
accessibilityRole: "radiogroup",
|
|
28
|
-
className: cn("flex-row items-start gap-1 self-start rounded-
|
|
28
|
+
className: cn("flex-row items-start gap-1 self-start rounded-[8px] bg-bg-tertiary p-1", className),
|
|
29
29
|
...props,
|
|
30
30
|
children: [/* @__PURE__ */ jsx(SlidingIndicator, {
|
|
31
31
|
target: active,
|
|
32
|
-
className: "rounded-
|
|
32
|
+
className: "rounded-[8px] bg-surface-primary shadow-sm"
|
|
33
33
|
}), children]
|
|
34
34
|
})
|
|
35
35
|
});
|
|
@@ -52,7 +52,7 @@ function SegmentedItem({ className, value, children, disabled, accessibilityStat
|
|
|
52
52
|
group.setLayout(value, event.nativeEvent.layout);
|
|
53
53
|
onLayout?.(event);
|
|
54
54
|
},
|
|
55
|
-
className: cn("items-center justify-center rounded-
|
|
55
|
+
className: cn("items-center justify-center rounded-[8px] px-4 py-2", checked ? !group.slid && "bg-surface-primary shadow-sm" : "active:bg-bg-secondary", className),
|
|
56
56
|
...props,
|
|
57
57
|
children: typeof children === "string" ? /* @__PURE__ */ jsx(Text, {
|
|
58
58
|
className: cn("text-sm leading-5 text-text-secondary", checked && "font-medium text-text-primary", isDisabled && "text-text-disabled"),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"segmented-control.js","names":[],"sources":["../../../src/components/segmented-control/segmented-control.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { createContext, type ReactNode, useContext, useState } from 'react'\nimport { Pressable, type PressableProps, Text, View, type ViewProps } from 'react-native'\nimport { cn } from '../../utils/cn'\nimport { type Rect, SlidingIndicator, useItemLayouts } from '../_shared/sliding-indicator'\n\ntype SegmentedContext = {\n value: string | undefined\n select: (value: string) => void\n /** O pill já mediu o item marcado: os itens deixam de desenhar o próprio fundo. */\n slid: boolean\n setLayout: (value: string, rect: Rect) => void\n}\n\nconst Context = createContext<SegmentedContext | null>(null)\n\nexport type SegmentedControlProps = Omit<ViewProps, 'style'> & {\n className?: string\n /** Obrigatório: descreve o que o controle escolhe (ex.: \"Modo de exibição\"). */\n accessibilityLabel: string\n /** Controlado. Sem ele, o componente guarda o próprio estado (começa em `defaultValue`). */\n value?: string\n defaultValue?: string\n onValueChange?: (value: string) => void\n children: ReactNode\n}\n\n/** Escolha única entre 2–5 views do mesmo conteúdo. Para navegar entre seções, use Tabs. */\nexport function SegmentedControl({\n className,\n value,\n defaultValue,\n onValueChange,\n children,\n ...props\n}: SegmentedControlProps) {\n const [uncontrolled, setUncontrolled] = useState(defaultValue)\n const current = value ?? uncontrolled\n const { layouts, setLayout } = useItemLayouts()\n const active = current === undefined ? undefined : layouts[current]\n\n const select = (next: string) => {\n if (next === current) return\n setUncontrolled(next)\n onValueChange?.(next)\n }\n\n return (\n <Context.Provider value={{ value: current, select, slid: active !== undefined, setLayout }}>\n <View\n accessibilityRole=\"radiogroup\"\n className={cn(\n 'flex-row items-start gap-1 self-start rounded-
|
|
1
|
+
{"version":3,"file":"segmented-control.js","names":[],"sources":["../../../src/components/segmented-control/segmented-control.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { createContext, type ReactNode, useContext, useState } from 'react'\nimport { Pressable, type PressableProps, Text, View, type ViewProps } from 'react-native'\nimport { cn } from '../../utils/cn'\nimport { type Rect, SlidingIndicator, useItemLayouts } from '../_shared/sliding-indicator'\n\ntype SegmentedContext = {\n value: string | undefined\n select: (value: string) => void\n /** O pill já mediu o item marcado: os itens deixam de desenhar o próprio fundo. */\n slid: boolean\n setLayout: (value: string, rect: Rect) => void\n}\n\nconst Context = createContext<SegmentedContext | null>(null)\n\nexport type SegmentedControlProps = Omit<ViewProps, 'style'> & {\n className?: string\n /** Obrigatório: descreve o que o controle escolhe (ex.: \"Modo de exibição\"). */\n accessibilityLabel: string\n /** Controlado. Sem ele, o componente guarda o próprio estado (começa em `defaultValue`). */\n value?: string\n defaultValue?: string\n onValueChange?: (value: string) => void\n children: ReactNode\n}\n\n/** Escolha única entre 2–5 views do mesmo conteúdo. Para navegar entre seções, use Tabs. */\nexport function SegmentedControl({\n className,\n value,\n defaultValue,\n onValueChange,\n children,\n ...props\n}: SegmentedControlProps) {\n const [uncontrolled, setUncontrolled] = useState(defaultValue)\n const current = value ?? uncontrolled\n const { layouts, setLayout } = useItemLayouts()\n const active = current === undefined ? undefined : layouts[current]\n\n const select = (next: string) => {\n if (next === current) return\n setUncontrolled(next)\n onValueChange?.(next)\n }\n\n return (\n <Context.Provider value={{ value: current, select, slid: active !== undefined, setLayout }}>\n <View\n accessibilityRole=\"radiogroup\"\n className={cn(\n 'flex-row items-start gap-1 self-start rounded-[8px] bg-bg-tertiary p-1',\n className,\n )}\n {...props}\n >\n {/* o fundo do item marcado é do indicador: fica sob os itens e desliza para o próximo */}\n <SlidingIndicator target={active} className=\"rounded-[8px] bg-surface-primary shadow-sm\" />\n {children}\n </View>\n </Context.Provider>\n )\n}\n\nexport type SegmentedItemProps = Omit<PressableProps, 'style' | 'children' | 'onPress'> & {\n className?: string\n value: string\n children: ReactNode\n}\n\nexport function SegmentedItem({\n className,\n value,\n children,\n disabled,\n accessibilityState,\n onLayout,\n ...props\n}: SegmentedItemProps) {\n const group = useContext(Context)\n if (!group) throw new Error('SegmentedItem deve ser usado dentro de SegmentedControl')\n\n const checked = group.value === value\n const isDisabled = Boolean(disabled)\n\n return (\n <Pressable\n accessibilityRole=\"radio\"\n accessibilityState={{ ...accessibilityState, checked, disabled: isDisabled }}\n disabled={isDisabled}\n onPress={() => group.select(value)}\n onLayout={(event) => {\n group.setLayout(value, event.nativeEvent.layout)\n onLayout?.(event)\n }}\n className={cn(\n 'items-center justify-center rounded-[8px] px-4 py-2',\n checked ? !group.slid && 'bg-surface-primary shadow-sm' : 'active:bg-bg-secondary',\n className,\n )}\n {...props}\n >\n {typeof children === 'string' ? (\n <Text\n className={cn(\n 'text-sm leading-5 text-text-secondary',\n checked && 'font-medium text-text-primary',\n isDisabled && 'text-text-disabled',\n )}\n >\n {children}\n </Text>\n ) : (\n children\n )}\n </Pressable>\n )\n}\n"],"mappings":";;;;;;AAeA,MAAM,UAAU,cAAuC,IAAI;;AAc3D,SAAgB,iBAAiB,EAC/B,WACA,OACA,cACA,eACA,UACA,GAAG,SACqB;CACxB,MAAM,CAAC,cAAc,mBAAmB,SAAS,YAAY;CAC7D,MAAM,UAAU,SAAS;CACzB,MAAM,EAAE,SAAS,cAAc,eAAe;CAC9C,MAAM,SAAS,YAAY,KAAA,IAAY,KAAA,IAAY,QAAQ;CAE3D,MAAM,UAAU,SAAiB;EAC/B,IAAI,SAAS,SAAS;EACtB,gBAAgB,IAAI;EACpB,gBAAgB,IAAI;CACtB;CAEA,OACE,oBAAC,QAAQ,UAAT;EAAkB,OAAO;GAAE,OAAO;GAAS;GAAQ,MAAM,WAAW,KAAA;GAAW;EAAU;EACvF,UAAA,qBAAC,MAAD;GACE,mBAAkB;GAClB,WAAW,GACT,0EACA,SACF;GACA,GAAI;GANN,UAAA,CASE,oBAAC,kBAAD;IAAkB,QAAQ;IAAQ,WAAU;GAA8C,CAAA,GACzF,QACG;;CACU,CAAA;AAEtB;AAQA,SAAgB,cAAc,EAC5B,WACA,OACA,UACA,UACA,oBACA,UACA,GAAG,SACkB;CACrB,MAAM,QAAQ,WAAW,OAAO;CAChC,IAAI,CAAC,OAAO,MAAM,IAAI,MAAM,yDAAyD;CAErF,MAAM,UAAU,MAAM,UAAU;CAChC,MAAM,aAAa,QAAQ,QAAQ;CAEnC,OACE,oBAAC,WAAD;EACE,mBAAkB;EAClB,oBAAoB;GAAE,GAAG;GAAoB;GAAS,UAAU;EAAW;EAC3E,UAAU;EACV,eAAe,MAAM,OAAO,KAAK;EACjC,WAAW,UAAU;GACnB,MAAM,UAAU,OAAO,MAAM,YAAY,MAAM;GAC/C,WAAW,KAAK;EAClB;EACA,WAAW,GACT,uDACA,UAAU,CAAC,MAAM,QAAQ,iCAAiC,0BAC1D,SACF;EACA,GAAI;EAEH,UAAA,OAAO,aAAa,WACnB,oBAAC,MAAD;GACE,WAAW,GACT,yCACA,WAAW,iCACX,cAAc,oBAChB;GAEC;EACG,CAAA,IAEN;CAEO,CAAA;AAEf"}
|
|
@@ -6,7 +6,7 @@ import Svg, { Path } from "react-native-svg";
|
|
|
6
6
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
7
|
import { cva } from "class-variance-authority";
|
|
8
8
|
//#region src/components/select/field-trigger.tsx
|
|
9
|
-
const triggerVariants = cva("w-full flex-row items-center justify-between gap-2 rounded-
|
|
9
|
+
const triggerVariants = cva("w-full flex-row items-center justify-between gap-2 rounded-[8px] border", {
|
|
10
10
|
variants: {
|
|
11
11
|
size: {
|
|
12
12
|
lg: "h-14 px-4",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"field-trigger.js","names":[],"sources":["../../../src/components/select/field-trigger.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { cva } from 'class-variance-authority'\nimport type { ReactNode, RefObject } from 'react'\nimport { Pressable, Text, View } from 'react-native'\nimport Svg, { Path } from 'react-native-svg'\nimport { cn } from '../../utils/cn'\nimport { ChevronRightIcon } from '../_shared/status'\nimport { useToken } from '../_shared/use-token'\n\nexport type FieldSize = 'sm' | 'md' | 'lg'\n\nconst triggerVariants = cva(\n 'w-full flex-row items-center justify-between gap-2 rounded-
|
|
1
|
+
{"version":3,"file":"field-trigger.js","names":[],"sources":["../../../src/components/select/field-trigger.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { cva } from 'class-variance-authority'\nimport type { ReactNode, RefObject } from 'react'\nimport { Pressable, Text, View } from 'react-native'\nimport Svg, { Path } from 'react-native-svg'\nimport { cn } from '../../utils/cn'\nimport { ChevronRightIcon } from '../_shared/status'\nimport { useToken } from '../_shared/use-token'\n\nexport type FieldSize = 'sm' | 'md' | 'lg'\n\nconst triggerVariants = cva(\n 'w-full flex-row items-center justify-between gap-2 rounded-[8px] border',\n {\n variants: {\n size: {\n lg: 'h-14 px-4',\n md: 'h-12 px-3',\n sm: 'h-10 px-2',\n },\n state: {\n idle: 'border-field-border bg-field-bg',\n open: 'border-field-focus-ring bg-field-bg',\n error: 'border-field-border-error bg-field-bg',\n disabled: 'border-field-border-disabled bg-field-bg-disabled',\n readOnly: 'border-transparent bg-field-bg-disabled',\n },\n },\n defaultVariants: { size: 'md', state: 'idle' },\n },\n)\n\nconst textVariants = cva('flex-1 text-base', {\n variants: {\n tone: {\n value: 'text-field-text',\n placeholder: 'text-field-text-placeholder',\n disabled: 'text-field-text-disabled',\n },\n },\n})\n\nexport type FieldTriggerProps = {\n /** Âncora medida por `useAnchor` (a View que envolve o gatilho). */\n anchorRef?: RefObject<View>\n /** Texto do valor; sem ele, mostra o placeholder. */\n text?: ReactNode\n placeholder: string\n icon: 'chevron' | 'calendar'\n size?: FieldSize\n error?: boolean\n open?: boolean\n disabled?: boolean\n readOnly?: boolean\n onPress?: () => void\n className?: string\n accessibilityLabel?: string\n accessibilityHint?: string\n testID?: string\n}\n\nfunction CalendarIcon({ color }: { color?: string }) {\n return (\n <Svg width={20} height={20} viewBox=\"0 0 24 24\" fill=\"none\" stroke={color} strokeWidth={1.5}>\n <Path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n d=\"M8 3v3M16 3v3M4 9h16M5 5h14a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1Z\"\n />\n </Svg>\n )\n}\n\n/**\n * Gatilho com visual de Input para Select, Combobox, DatePicker e DateRangePicker (uso interno,\n * não faz parte da API pública). Sem hover/focus-visible no native: o estado aberto usa a cor do\n * anel de foco na borda.\n */\nexport function FieldTrigger({\n anchorRef,\n text,\n placeholder,\n icon,\n size,\n error,\n open,\n disabled,\n readOnly,\n onPress,\n className,\n accessibilityLabel,\n accessibilityHint,\n testID,\n}: FieldTriggerProps) {\n const iconColor = useToken('--color-field-text-placeholder')\n const hasText = text !== undefined && text !== null && text !== ''\n const state = disabled\n ? 'disabled'\n : readOnly\n ? 'readOnly'\n : open\n ? 'open'\n : error\n ? 'error'\n : 'idle'\n\n return (\n <View ref={anchorRef} collapsable={false} className=\"w-full\">\n <Pressable\n accessibilityRole=\"combobox\"\n accessibilityLabel={accessibilityLabel}\n accessibilityHint={accessibilityHint}\n accessibilityState={{ expanded: Boolean(open), disabled: Boolean(disabled) }}\n disabled={disabled}\n onPress={readOnly ? undefined : onPress}\n testID={testID}\n className={cn(triggerVariants({ size, state }), className)}\n >\n <Text\n numberOfLines={1}\n className={textVariants({\n tone: disabled ? 'disabled' : hasText ? 'value' : 'placeholder',\n })}\n >\n {hasText ? text : placeholder}\n </Text>\n {icon === 'calendar' ? (\n <CalendarIcon color={iconColor} />\n ) : (\n <View className={open ? '-rotate-90' : 'rotate-90'}>\n <ChevronRightIcon color={iconColor} />\n </View>\n )}\n </Pressable>\n </View>\n )\n}\n"],"mappings":";;;;;;;;AAYA,MAAM,kBAAkB,IACtB,2EACA;CACE,UAAU;EACR,MAAM;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EACN;EACA,OAAO;GACL,MAAM;GACN,MAAM;GACN,OAAO;GACP,UAAU;GACV,UAAU;EACZ;CACF;CACA,iBAAiB;EAAE,MAAM;EAAM,OAAO;CAAO;AAC/C,CACF;AAEA,MAAM,eAAe,IAAI,oBAAoB,EAC3C,UAAU,EACR,MAAM;CACJ,OAAO;CACP,aAAa;CACb,UAAU;AACZ,EACF,EACF,CAAC;AAqBD,SAAS,aAAa,EAAE,SAA6B;CACnD,OACE,oBAAC,KAAD;EAAK,OAAO;EAAI,QAAQ;EAAI,SAAQ;EAAY,MAAK;EAAO,QAAQ;EAAO,aAAa;EACtF,UAAA,oBAAC,MAAD;GACE,eAAc;GACd,gBAAe;GACf,GAAE;EACH,CAAA;CACE,CAAA;AAET;;;;;;AAOA,SAAgB,aAAa,EAC3B,WACA,MACA,aACA,MACA,MACA,OACA,MACA,UACA,UACA,SACA,WACA,oBACA,mBACA,UACoB;CACpB,MAAM,YAAY,SAAS,gCAAgC;CAC3D,MAAM,UAAU,SAAS,KAAA,KAAa,SAAS,QAAQ,SAAS;CAWhE,OACE,oBAAC,MAAD;EAAM,KAAK;EAAW,aAAa;EAAO,WAAU;EAClD,UAAA,qBAAC,WAAD;GACE,mBAAkB;GACE;GACD;GACnB,oBAAoB;IAAE,UAAU,QAAQ,IAAI;IAAG,UAAU,QAAQ,QAAQ;GAAE;GACjE;GACV,SAAS,WAAW,KAAA,IAAY;GACxB;GACR,WAAW,GAAG,gBAAgB;IAAE;IAAM,OApB9B,WACV,aACA,WACE,aACA,OACE,SACA,QACE,UACA;GAYwC,CAAC,GAAG,SAAS;GAR3D,UAAA,CAUE,oBAAC,MAAD;IACE,eAAe;IACf,WAAW,aAAa,EACtB,MAAM,WAAW,aAAa,UAAU,UAAU,cACpD,CAAC;IAEA,UAAA,UAAU,OAAO;GACd,CAAA,GACL,SAAS,aACR,oBAAC,cAAD,EAAc,OAAO,UAAY,CAAA,IAEjC,oBAAC,MAAD;IAAM,WAAW,OAAO,eAAe;IACrC,UAAA,oBAAC,kBAAD,EAAkB,OAAO,UAAY,CAAA;GACjC,CAAA,CAEC;;CACP,CAAA;AAEV"}
|
|
@@ -56,7 +56,7 @@ function Select({ children, placeholder = "Selecione...", value, defaultValue, o
|
|
|
56
56
|
onRequestClose: anchor.close,
|
|
57
57
|
matchAnchorWidth: true,
|
|
58
58
|
testID: testID ? `${testID}-list` : void 0,
|
|
59
|
-
className: "overflow-hidden rounded-
|
|
59
|
+
className: "overflow-hidden rounded-[8px] border border-select-dropdown-border bg-select-dropdown-bg p-1 shadow-md",
|
|
60
60
|
children: /* @__PURE__ */ jsx(SelectContext.Provider, {
|
|
61
61
|
value: {
|
|
62
62
|
value: selected,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"select.js","names":[],"sources":["../../../src/components/select/select.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport {\n Children,\n createContext,\n isValidElement,\n type ReactElement,\n type ReactNode,\n useContext,\n useState,\n} from 'react'\nimport { Pressable, ScrollView, Text, View } from 'react-native'\nimport { cn } from '../../utils/cn'\nimport { AnchoredOverlay, useAnchor } from '../_shared/anchored-overlay'\nimport { CheckIcon } from '../_shared/status'\nimport { useToken } from '../_shared/use-token'\nimport { type FieldSize, FieldTrigger } from './field-trigger'\n\ntype SelectContextValue = { value: string | undefined; onSelect: (value: string) => void }\nconst SelectContext = createContext<SelectContextValue>({ value: undefined, onSelect: () => {} })\n\nexport type SelectProps = {\n /** Opções: um ou mais `<SelectOption>`. */\n children: ReactNode\n placeholder?: string\n value?: string\n defaultValue?: string\n onValueChange?: (value: string) => void\n disabled?: boolean\n /** Mostra o valor mas nunca abre a lista, como um `<TextInput editable={false}>`. */\n readOnly?: boolean\n error?: boolean\n size?: FieldSize\n className?: string\n accessibilityLabel?: string\n accessibilityHint?: string\n testID?: string\n}\n\nexport type SelectOptionProps = {\n value: string\n disabled?: boolean\n children: ReactNode\n}\n\n// Procura o rótulo da opção escolhida nos filhos (inclusive dentro de Fragments).\nfunction findLabel(children: ReactNode, value: string | undefined): ReactNode | undefined {\n for (const child of Children.toArray(children)) {\n if (!isValidElement(child)) continue\n const element = child as ReactElement<{ value?: string; children?: ReactNode }>\n if (element.type === SelectOption && element.props.value === value)\n return element.props.children\n const nested = findLabel(element.props.children, value)\n if (nested !== undefined) return nested\n }\n return undefined\n}\n\n/**\n * Campo de escolha única. Toque no gatilho abre a lista numa camada ancorada (largura do gatilho,\n * rolável). Diferenças do web: sem navegação por teclado, sem `name`/`required`/`id` (não há\n * formulário nativo).\n */\nexport function Select({\n children,\n placeholder = 'Selecione...',\n value,\n defaultValue,\n onValueChange,\n size,\n error,\n disabled,\n readOnly = false,\n className,\n accessibilityLabel,\n accessibilityHint,\n testID,\n}: SelectProps) {\n const [inner, setInner] = useState(defaultValue)\n const selected = value ?? inner\n const anchor = useAnchor()\n const open = anchor.visible && !disabled && !readOnly\n\n function onSelect(next: string) {\n if (value === undefined) setInner(next)\n onValueChange?.(next)\n anchor.close()\n }\n\n return (\n <>\n <FieldTrigger\n anchorRef={anchor.ref}\n icon=\"chevron\"\n text={selected === undefined ? undefined : findLabel(children, selected)}\n placeholder={placeholder}\n size={size}\n error={error}\n open={open}\n disabled={disabled}\n readOnly={readOnly}\n onPress={anchor.open}\n className={className}\n accessibilityLabel={accessibilityLabel}\n accessibilityHint={accessibilityHint}\n testID={testID}\n />\n <AnchoredOverlay\n visible={open}\n rect={anchor.rect}\n onRequestClose={anchor.close}\n matchAnchorWidth\n testID={testID ? `${testID}-list` : undefined}\n className=\"overflow-hidden rounded-
|
|
1
|
+
{"version":3,"file":"select.js","names":[],"sources":["../../../src/components/select/select.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport {\n Children,\n createContext,\n isValidElement,\n type ReactElement,\n type ReactNode,\n useContext,\n useState,\n} from 'react'\nimport { Pressable, ScrollView, Text, View } from 'react-native'\nimport { cn } from '../../utils/cn'\nimport { AnchoredOverlay, useAnchor } from '../_shared/anchored-overlay'\nimport { CheckIcon } from '../_shared/status'\nimport { useToken } from '../_shared/use-token'\nimport { type FieldSize, FieldTrigger } from './field-trigger'\n\ntype SelectContextValue = { value: string | undefined; onSelect: (value: string) => void }\nconst SelectContext = createContext<SelectContextValue>({ value: undefined, onSelect: () => {} })\n\nexport type SelectProps = {\n /** Opções: um ou mais `<SelectOption>`. */\n children: ReactNode\n placeholder?: string\n value?: string\n defaultValue?: string\n onValueChange?: (value: string) => void\n disabled?: boolean\n /** Mostra o valor mas nunca abre a lista, como um `<TextInput editable={false}>`. */\n readOnly?: boolean\n error?: boolean\n size?: FieldSize\n className?: string\n accessibilityLabel?: string\n accessibilityHint?: string\n testID?: string\n}\n\nexport type SelectOptionProps = {\n value: string\n disabled?: boolean\n children: ReactNode\n}\n\n// Procura o rótulo da opção escolhida nos filhos (inclusive dentro de Fragments).\nfunction findLabel(children: ReactNode, value: string | undefined): ReactNode | undefined {\n for (const child of Children.toArray(children)) {\n if (!isValidElement(child)) continue\n const element = child as ReactElement<{ value?: string; children?: ReactNode }>\n if (element.type === SelectOption && element.props.value === value)\n return element.props.children\n const nested = findLabel(element.props.children, value)\n if (nested !== undefined) return nested\n }\n return undefined\n}\n\n/**\n * Campo de escolha única. Toque no gatilho abre a lista numa camada ancorada (largura do gatilho,\n * rolável). Diferenças do web: sem navegação por teclado, sem `name`/`required`/`id` (não há\n * formulário nativo).\n */\nexport function Select({\n children,\n placeholder = 'Selecione...',\n value,\n defaultValue,\n onValueChange,\n size,\n error,\n disabled,\n readOnly = false,\n className,\n accessibilityLabel,\n accessibilityHint,\n testID,\n}: SelectProps) {\n const [inner, setInner] = useState(defaultValue)\n const selected = value ?? inner\n const anchor = useAnchor()\n const open = anchor.visible && !disabled && !readOnly\n\n function onSelect(next: string) {\n if (value === undefined) setInner(next)\n onValueChange?.(next)\n anchor.close()\n }\n\n return (\n <>\n <FieldTrigger\n anchorRef={anchor.ref}\n icon=\"chevron\"\n text={selected === undefined ? undefined : findLabel(children, selected)}\n placeholder={placeholder}\n size={size}\n error={error}\n open={open}\n disabled={disabled}\n readOnly={readOnly}\n onPress={anchor.open}\n className={className}\n accessibilityLabel={accessibilityLabel}\n accessibilityHint={accessibilityHint}\n testID={testID}\n />\n <AnchoredOverlay\n visible={open}\n rect={anchor.rect}\n onRequestClose={anchor.close}\n matchAnchorWidth\n testID={testID ? `${testID}-list` : undefined}\n className=\"overflow-hidden rounded-[8px] border border-select-dropdown-border bg-select-dropdown-bg p-1 shadow-md\"\n >\n <SelectContext.Provider value={{ value: selected, onSelect }}>\n <ScrollView className=\"max-h-60\">{children}</ScrollView>\n </SelectContext.Provider>\n </AnchoredOverlay>\n </>\n )\n}\n\n/** Opção da lista do Select. O `children` é o rótulo mostrado na lista e no gatilho. */\nexport function SelectOption({ value, disabled, children }: SelectOptionProps) {\n const ctx = useContext(SelectContext)\n const selected = ctx.value === value\n const checkColor = useToken('--color-select-option-text')\n\n return (\n <Pressable\n role=\"option\"\n accessibilityState={{ selected, disabled: Boolean(disabled) }}\n disabled={disabled}\n onPress={() => ctx.onSelect(value)}\n className={cn(\n 'flex-row items-center gap-2 rounded-sm px-3 py-2 active:bg-select-option-bg-hover',\n selected && 'bg-select-option-bg-selected',\n )}\n >\n <Text\n className={cn(\n 'flex-1 text-base text-select-option-text',\n disabled && 'text-select-option-text-disabled',\n )}\n >\n {children}\n </Text>\n {selected ? (\n <View className=\"size-4 items-center justify-center\">\n <CheckIcon color={checkColor} />\n </View>\n ) : null}\n </Pressable>\n )\n}\n"],"mappings":";;;;;;;;;AAmBA,MAAM,gBAAgB,cAAkC;CAAE,OAAO,KAAA;CAAW,gBAAgB,CAAC;AAAE,CAAC;AA2BhG,SAAS,UAAU,UAAqB,OAAkD;CACxF,KAAK,MAAM,SAAS,SAAS,QAAQ,QAAQ,GAAG;EAC9C,IAAI,CAAC,eAAe,KAAK,GAAG;EAC5B,MAAM,UAAU;EAChB,IAAI,QAAQ,SAAS,gBAAgB,QAAQ,MAAM,UAAU,OAC3D,OAAO,QAAQ,MAAM;EACvB,MAAM,SAAS,UAAU,QAAQ,MAAM,UAAU,KAAK;EACtD,IAAI,WAAW,KAAA,GAAW,OAAO;CACnC;AAEF;;;;;;AAOA,SAAgB,OAAO,EACrB,UACA,cAAc,gBACd,OACA,cACA,eACA,MACA,OACA,UACA,WAAW,OACX,WACA,oBACA,mBACA,UACc;CACd,MAAM,CAAC,OAAO,YAAY,SAAS,YAAY;CAC/C,MAAM,WAAW,SAAS;CAC1B,MAAM,SAAS,UAAU;CACzB,MAAM,OAAO,OAAO,WAAW,CAAC,YAAY,CAAC;CAE7C,SAAS,SAAS,MAAc;EAC9B,IAAI,UAAU,KAAA,GAAW,SAAS,IAAI;EACtC,gBAAgB,IAAI;EACpB,OAAO,MAAM;CACf;CAEA,OACE,qBAAA,YAAA,EAAA,UAAA,CACE,oBAAC,cAAD;EACE,WAAW,OAAO;EAClB,MAAK;EACL,MAAM,aAAa,KAAA,IAAY,KAAA,IAAY,UAAU,UAAU,QAAQ;EAC1D;EACP;EACC;EACD;EACI;EACA;EACV,SAAS,OAAO;EACL;EACS;EACD;EACX;CACT,CAAA,GACD,oBAAC,iBAAD;EACE,SAAS;EACT,MAAM,OAAO;EACb,gBAAgB,OAAO;EACvB,kBAAA;EACA,QAAQ,SAAS,GAAG,OAAO,SAAS,KAAA;EACpC,WAAU;EAEV,UAAA,oBAAC,cAAc,UAAf;GAAwB,OAAO;IAAE,OAAO;IAAU;GAAS;GACzD,UAAA,oBAAC,YAAD;IAAY,WAAU;IAAY;GAAqB,CAAA;EACjC,CAAA;CACT,CAAA,CACjB,EAAA,CAAA;AAEN;;AAGA,SAAgB,aAAa,EAAE,OAAO,UAAU,YAA+B;CAC7E,MAAM,MAAM,WAAW,aAAa;CACpC,MAAM,WAAW,IAAI,UAAU;CAC/B,MAAM,aAAa,SAAS,4BAA4B;CAExD,OACE,qBAAC,WAAD;EACE,MAAK;EACL,oBAAoB;GAAE;GAAU,UAAU,QAAQ,QAAQ;EAAE;EAClD;EACV,eAAe,IAAI,SAAS,KAAK;EACjC,WAAW,GACT,qFACA,YAAY,8BACd;EARF,UAAA,CAUE,oBAAC,MAAD;GACE,WAAW,GACT,4CACA,YAAY,kCACd;GAEC;EACG,CAAA,GACL,WACC,oBAAC,MAAD;GAAM,WAAU;GACd,UAAA,oBAAC,WAAD,EAAW,OAAO,WAAa,CAAA;EAC3B,CAAA,IACJ,IACK;;AAEf"}
|
|
@@ -9,8 +9,8 @@ const skeletonVariants = cva("overflow-hidden", {
|
|
|
9
9
|
text: "h-3 w-[200px] rounded-sm",
|
|
10
10
|
title: "h-5 w-[160px] rounded-sm",
|
|
11
11
|
avatar: "size-10 rounded-full",
|
|
12
|
-
card: "h-[120px] w-[300px] rounded-
|
|
13
|
-
rectangle: "h-6 w-[200px] rounded-
|
|
12
|
+
card: "h-[120px] w-[300px] rounded-[8px]",
|
|
13
|
+
rectangle: "h-6 w-[200px] rounded-[8px]"
|
|
14
14
|
} },
|
|
15
15
|
defaultVariants: { variant: "text" }
|
|
16
16
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skeleton.js","names":[],"sources":["../../../src/components/skeleton/skeleton.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { useEffect, useRef } from 'react'\nimport { Animated, Easing, StyleSheet, View, type ViewProps } from 'react-native'\nimport { cn } from '../../utils/cn'\n\n// Dimensões padrão do Figma; sobrescreva com className (ex.: `w-full`) para casar com o conteúdo real.\nconst skeletonVariants = cva('overflow-hidden', {\n variants: {\n variant: {\n text: 'h-3 w-[200px] rounded-sm',\n title: 'h-5 w-[160px] rounded-sm',\n avatar: 'size-10 rounded-full',\n card: 'h-[120px] w-[300px] rounded-
|
|
1
|
+
{"version":3,"file":"skeleton.js","names":[],"sources":["../../../src/components/skeleton/skeleton.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { useEffect, useRef } from 'react'\nimport { Animated, Easing, StyleSheet, View, type ViewProps } from 'react-native'\nimport { cn } from '../../utils/cn'\n\n// Dimensões padrão do Figma; sobrescreva com className (ex.: `w-full`) para casar com o conteúdo real.\nconst skeletonVariants = cva('overflow-hidden', {\n variants: {\n variant: {\n text: 'h-3 w-[200px] rounded-sm',\n title: 'h-5 w-[160px] rounded-sm',\n avatar: 'size-10 rounded-full',\n card: 'h-[120px] w-[300px] rounded-[8px]',\n rectangle: 'h-6 w-[200px] rounded-[8px]',\n },\n },\n defaultVariants: { variant: 'text' },\n})\n\nexport type SkeletonProps = Omit<ViewProps, 'children'> &\n VariantProps<typeof skeletonVariants> & {\n className?: string\n }\n\n/**\n * Placeholder de conteúdo em carregamento. É decorativo (escondido do leitor de tela): anuncie o\n * carregamento no contêiner, com `accessibilityState={{ busy: true }}`. Para ações pontuais, use\n * Spinner. Não respeita \"reduzir movimento\" (a web faz via `motion-reduce`).\n */\nexport function Skeleton({ className, variant, ...props }: SkeletonProps) {\n const opacity = useRef(new Animated.Value(1)).current\n\n useEffect(() => {\n const loop = Animated.loop(\n Animated.sequence([\n Animated.timing(opacity, {\n toValue: 0.5,\n duration: 1000,\n easing: Easing.inOut(Easing.ease),\n useNativeDriver: true,\n }),\n Animated.timing(opacity, {\n toValue: 1,\n duration: 1000,\n easing: Easing.inOut(Easing.ease),\n useNativeDriver: true,\n }),\n ]),\n )\n loop.start()\n return () => loop.stop()\n }, [opacity])\n\n return (\n <View\n accessibilityElementsHidden\n importantForAccessibility=\"no-hide-descendants\"\n className={cn(skeletonVariants({ variant }), className)}\n {...props}\n >\n {/* O Animated não recebe className: só pulsa; o visual fica no View de dentro. */}\n <Animated.View style={[StyleSheet.absoluteFill, { opacity }]}>\n <View className=\"size-full bg-bg-skeleton\" />\n </Animated.View>\n </View>\n )\n}\n"],"mappings":";;;;;;AAQA,MAAM,mBAAmB,IAAI,mBAAmB;CAC9C,UAAU,EACR,SAAS;EACP,MAAM;EACN,OAAO;EACP,QAAQ;EACR,MAAM;EACN,WAAW;CACb,EACF;CACA,iBAAiB,EAAE,SAAS,OAAO;AACrC,CAAC;;;;;;AAYD,SAAgB,SAAS,EAAE,WAAW,SAAS,GAAG,SAAwB;CACxE,MAAM,UAAU,OAAO,IAAI,SAAS,MAAM,CAAC,CAAC,CAAC,CAAC;CAE9C,gBAAgB;EACd,MAAM,OAAO,SAAS,KACpB,SAAS,SAAS,CAChB,SAAS,OAAO,SAAS;GACvB,SAAS;GACT,UAAU;GACV,QAAQ,OAAO,MAAM,OAAO,IAAI;GAChC,iBAAiB;EACnB,CAAC,GACD,SAAS,OAAO,SAAS;GACvB,SAAS;GACT,UAAU;GACV,QAAQ,OAAO,MAAM,OAAO,IAAI;GAChC,iBAAiB;EACnB,CAAC,CACH,CAAC,CACH;EACA,KAAK,MAAM;EACX,aAAa,KAAK,KAAK;CACzB,GAAG,CAAC,OAAO,CAAC;CAEZ,OACE,oBAAC,MAAD;EACE,6BAAA;EACA,2BAA0B;EAC1B,WAAW,GAAG,iBAAiB,EAAE,QAAQ,CAAC,GAAG,SAAS;EACtD,GAAI;EAGJ,UAAA,oBAAC,SAAS,MAAV;GAAe,OAAO,CAAC,WAAW,cAAc,EAAE,QAAQ,CAAC;GACzD,UAAA,oBAAC,MAAD,EAAM,WAAU,2BAA4B,CAAA;EAC/B,CAAA;CACX,CAAA;AAEV"}
|
|
@@ -4,7 +4,7 @@ import { TextInput } from "react-native";
|
|
|
4
4
|
import { jsx } from "react/jsx-runtime";
|
|
5
5
|
import { cva } from "class-variance-authority";
|
|
6
6
|
//#region src/components/textarea/textarea.tsx
|
|
7
|
-
const textareaVariants = cva("min-h-30 w-full rounded-
|
|
7
|
+
const textareaVariants = cva("min-h-30 w-full rounded-[8px] border border-field-border bg-field-bg text-field-text focus:border-field-focus-ring", {
|
|
8
8
|
variants: {
|
|
9
9
|
size: {
|
|
10
10
|
lg: "p-4 text-base",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"textarea.js","names":[],"sources":["../../../src/components/textarea/textarea.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { TextInput, type TextInputProps } from 'react-native'\nimport { cn } from '../../utils/cn'\nimport { useToken } from '../_shared/use-token'\n\nconst textareaVariants = cva(\n 'min-h-30 w-full rounded-
|
|
1
|
+
{"version":3,"file":"textarea.js","names":[],"sources":["../../../src/components/textarea/textarea.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { TextInput, type TextInputProps } from 'react-native'\nimport { cn } from '../../utils/cn'\nimport { useToken } from '../_shared/use-token'\n\nconst textareaVariants = cva(\n 'min-h-30 w-full rounded-[8px] border border-field-border bg-field-bg text-field-text focus:border-field-focus-ring',\n {\n variants: {\n size: {\n lg: 'p-4 text-base',\n md: 'p-3 text-base',\n sm: 'p-2 text-sm',\n },\n error: {\n true: 'border-field-border-error focus:border-field-border-error',\n false: '',\n },\n },\n defaultVariants: { size: 'md', error: false },\n },\n)\n\nexport type TextareaProps = Omit<TextInputProps, 'style' | 'editable' | 'multiline'> &\n Omit<VariantProps<typeof textareaVariants>, 'error'> & {\n className?: string\n /** Borda e foco em vermelho. */\n error?: boolean\n disabled?: boolean\n }\n\nexport function Textarea({\n className,\n size,\n error = false,\n disabled = false,\n readOnly = false,\n accessibilityState,\n ...props\n}: TextareaProps) {\n const placeholderColor = useToken('--color-field-text-placeholder')\n\n return (\n <TextInput\n multiline\n textAlignVertical=\"top\"\n editable={!disabled && !readOnly}\n readOnly={readOnly}\n placeholderTextColor={placeholderColor}\n accessibilityState={{ ...accessibilityState, disabled }}\n className={cn(\n textareaVariants({ size, error }),\n disabled &&\n 'border-field-border-disabled bg-field-bg-disabled text-field-text-disabled focus:border-field-border-disabled',\n readOnly && !disabled && 'border-transparent bg-field-bg-disabled focus:border-transparent',\n className,\n )}\n {...props}\n />\n )\n}\n"],"mappings":";;;;;;AAOA,MAAM,mBAAmB,IACvB,sHACA;CACE,UAAU;EACR,MAAM;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EACN;EACA,OAAO;GACL,MAAM;GACN,OAAO;EACT;CACF;CACA,iBAAiB;EAAE,MAAM;EAAM,OAAO;CAAM;AAC9C,CACF;AAUA,SAAgB,SAAS,EACvB,WACA,MACA,QAAQ,OACR,WAAW,OACX,WAAW,OACX,oBACA,GAAG,SACa;CAChB,MAAM,mBAAmB,SAAS,gCAAgC;CAElE,OACE,oBAAC,WAAD;EACE,WAAA;EACA,mBAAkB;EAClB,UAAU,CAAC,YAAY,CAAC;EACd;EACV,sBAAsB;EACtB,oBAAoB;GAAE,GAAG;GAAoB;EAAS;EACtD,WAAW,GACT,iBAAiB;GAAE;GAAM;EAAM,CAAC,GAChC,YACE,iHACF,YAAY,CAAC,YAAY,oEACzB,SACF;EACA,GAAI;CACL,CAAA;AAEL"}
|
|
@@ -177,7 +177,7 @@ function ToastView({ item, dismissLabel, onDismiss, onRemove }) {
|
|
|
177
177
|
children: /* @__PURE__ */ jsxs(View, {
|
|
178
178
|
accessibilityRole: statusRole(status),
|
|
179
179
|
accessibilityLiveRegion: statusRole(status) ? "assertive" : "polite",
|
|
180
|
-
className: "flex-row items-start gap-3 rounded-
|
|
180
|
+
className: "flex-row items-start gap-3 rounded-[8px] border border-border-default bg-surface-elevated py-3 pr-3 pl-4 shadow-md",
|
|
181
181
|
style: { elevation: 6 },
|
|
182
182
|
children: [
|
|
183
183
|
showIcon && /* @__PURE__ */ jsx(StatusIcon, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toast.js","names":[],"sources":["../../../src/components/toast/toast.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport {\n createContext,\n type ReactNode,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport {\n Animated,\n LayoutAnimation,\n PanResponder,\n Platform,\n Pressable,\n StatusBar,\n Text,\n UIManager,\n View,\n} from 'react-native'\nimport { useReducedMotion } from '../_shared/sliding-indicator'\nimport {\n CloseIcon,\n type Status,\n StatusIcon,\n statusRole,\n statusText,\n statusTextToken,\n} from '../_shared/status'\nimport { useToken } from '../_shared/use-token'\nimport { Button } from '../button/button'\n\nexport type ToastOptions = {\n status?: Status\n /** Resumo (opcional). */\n title?: string\n description: string\n /** Ação contextual (ex.: \"Desfazer\"). Tocar nela também fecha o toast. */\n action?: { label: string; onClick: () => void }\n /** Tempo até fechar sozinho, em ms. `0` não fecha sozinho. Padrão: 5000. */\n duration?: number\n /** Mostra o ícone do status. Padrão: `true`. */\n showIcon?: boolean\n /** Mostra o botão de fechar (×). Sem ele, o toast ainda fecha pelo `duration`. Padrão: `true`. */\n dismissible?: boolean\n}\n\ntype ToastItem = ToastOptions & { id: number; closing: boolean }\n\ntype ToastContextValue = {\n /** Exibe um toast e devolve o id, para fechá-lo com `dismiss`. */\n toast: (options: ToastOptions) => number\n dismiss: (id: number) => void\n}\n\nconst ToastContext = createContext<ToastContextValue | null>(null)\n\nexport type ToastProviderProps = {\n children: ReactNode\n /** Nome acessível da região de notificações. */\n label?: string\n /** Nome acessível do botão de fechar de cada toast. */\n dismissLabel?: string\n testID?: string\n}\n\n// No Android antigo o LayoutAnimation precisa ser habilitado. Na New Architecture a chamada é no-op.\nif (Platform.OS === 'android') {\n UIManager.setLayoutAnimationEnabledExperimental?.(true)\n}\n\n/** Arrastar o toast para o lado além disto (px), ou mais rápido que isto (px/ms), o dispensa. */\nconst SWIPE_DISTANCE = 50\nconst SWIPE_VELOCITY = 0.5\n\n// Sem react-native-safe-area-context: margem fixa para o notch no iOS e a barra de status no Android.\nconst topInset = Platform.OS === 'ios' ? 56 : (StatusBar.currentHeight ?? 24) + 8\n\n/**\n * Envolva o app uma vez; qualquer componente abaixo dispara toasts com `useToast()`. A pilha fica\n * no topo da tela, acima do conteúdo do app, mas abaixo de Modal/Drawer (que são janelas nativas).\n */\nexport function ToastProvider({\n children,\n label = 'Notificações',\n dismissLabel = 'Fechar',\n testID,\n}: ToastProviderProps) {\n const [items, setItems] = useState<ToastItem[]>([])\n const nextId = useRef(0)\n\n // Marca como \"closing\"; o próprio toast anima a saída e então chama `remove`.\n const dismiss = useCallback((id: number) => {\n setItems((current) =>\n current.map((item) => (item.id === id ? { ...item, closing: true } : item)),\n )\n }, [])\n const reduced = useReducedMotion()\n const remove = useCallback(\n (id: number) => {\n // os de baixo deslizam para o lugar em vez de pular\n if (!reduced) LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)\n setItems((current) => current.filter((item) => item.id !== id))\n },\n [reduced],\n )\n const toast = useCallback((options: ToastOptions) => {\n const id = nextId.current++\n setItems((current) => [...current, { ...options, id, closing: false }])\n return id\n }, [])\n const value = useMemo(() => ({ toast, dismiss }), [toast, dismiss])\n\n return (\n <ToastContext.Provider value={value}>\n {children}\n <View\n pointerEvents=\"box-none\"\n accessibilityLabel={label}\n testID={testID}\n style={{ position: 'absolute', top: 0, left: 0, right: 0, zIndex: 9999, elevation: 9999 }}\n >\n <View\n pointerEvents=\"box-none\"\n className=\"w-full max-w-[360px] gap-2 self-center px-4\"\n style={{ paddingTop: topInset }}\n >\n {items.map((item) => (\n <ToastView\n key={item.id}\n item={item}\n dismissLabel={dismissLabel}\n onDismiss={dismiss}\n onRemove={remove}\n />\n ))}\n </View>\n </View>\n </ToastContext.Provider>\n )\n}\n\nfunction ToastView({\n item,\n dismissLabel,\n onDismiss,\n onRemove,\n}: {\n item: ToastItem\n dismissLabel: string\n onDismiss: (id: number) => void\n onRemove: (id: number) => void\n}) {\n const {\n id,\n status = 'info',\n title,\n description,\n action,\n duration = 5000,\n showIcon = true,\n dismissible = true,\n closing,\n } = item\n const progress = useRef(new Animated.Value(0)).current\n // arrastar para o lado dispensa (como o swipe do Radix no web); o auto-fechar pausa enquanto isso\n const dragX = useRef(new Animated.Value(0)).current\n const [dragging, setDragging] = useState(false)\n const latest = useRef({ id, onDismiss })\n latest.current = { id, onDismiss }\n const panResponder = useRef(\n PanResponder.create({\n onMoveShouldSetPanResponder: (_, g) => Math.abs(g.dx) > 6 && Math.abs(g.dx) > Math.abs(g.dy),\n onPanResponderTerminationRequest: () => false,\n onPanResponderGrant: () => setDragging(true),\n onPanResponderMove: (_, g) => dragX.setValue(g.dx),\n onPanResponderRelease: (_, g) => {\n setDragging(false)\n if (Math.abs(g.dx) > SWIPE_DISTANCE || Math.abs(g.vx) > SWIPE_VELOCITY) {\n // continua para o lado de onde o dedo ia, enquanto o toast some\n Animated.timing(dragX, {\n toValue: (g.dx || g.vx) > 0 ? 400 : -400,\n duration: 150,\n useNativeDriver: true,\n }).start()\n latest.current.onDismiss(latest.current.id)\n } else {\n Animated.timing(dragX, { toValue: 0, duration: 200, useNativeDriver: true }).start()\n }\n },\n onPanResponderTerminate: () => {\n setDragging(false)\n Animated.timing(dragX, { toValue: 0, duration: 200, useNativeDriver: true }).start()\n },\n }),\n ).current\n const swipeFade = dragX.interpolate({\n inputRange: [-150, 0, 150],\n outputRange: [0.2, 1, 0.2],\n extrapolate: 'clamp',\n })\n const iconColor = useToken(statusTextToken[status])\n const closeColor = useToken('--color-text-primary')\n\n useEffect(() => {\n if (closing) return\n const animation = Animated.timing(progress, {\n toValue: 1,\n duration: 200,\n useNativeDriver: true,\n })\n animation.start()\n return () => animation.stop()\n }, [closing, progress])\n\n useEffect(() => {\n if (!closing) return\n const animation = Animated.timing(progress, {\n toValue: 0,\n duration: 150,\n useNativeDriver: true,\n })\n animation.start(({ finished }) => finished && onRemove(id))\n return () => animation.stop()\n }, [closing, id, onRemove, progress])\n\n useEffect(() => {\n if (closing || dragging || duration <= 0) return\n const timer = setTimeout(() => onDismiss(id), duration)\n return () => clearTimeout(timer)\n }, [closing, dragging, duration, id, onDismiss])\n\n const translateY = progress.interpolate({ inputRange: [0, 1], outputRange: [-16, 0] })\n\n return (\n <Animated.View\n {...panResponder.panHandlers}\n style={{\n opacity: Animated.multiply(progress, swipeFade),\n transform: [{ translateY }, { translateX: dragX }],\n }}\n >\n <View\n accessibilityRole={statusRole(status)}\n // Erro e aviso interrompem o leitor de tela; o resto é anunciado com calma.\n accessibilityLiveRegion={statusRole(status) ? 'assertive' : 'polite'}\n className=\"flex-row items-start gap-3 rounded-md border border-border-default bg-surface-elevated py-3 pr-3 pl-4 shadow-md\"\n style={{ elevation: 6 }}\n >\n {showIcon && <StatusIcon status={status} color={iconColor} />}\n <View className=\"min-w-0 flex-1 items-start gap-1\">\n {title ? (\n <Text className={`text-sm font-medium leading-5 ${statusText[status]}`}>{title}</Text>\n ) : null}\n <Text className=\"text-sm leading-5 text-text-primary\">{description}</Text>\n {action && (\n <Button\n variant=\"ghost\"\n onPress={() => {\n action.onClick()\n onDismiss(id)\n }}\n >\n {action.label}\n </Button>\n )}\n </View>\n {dismissible && (\n <Pressable\n accessibilityRole=\"button\"\n accessibilityLabel={dismissLabel}\n hitSlop={8}\n onPress={() => onDismiss(id)}\n className=\"size-8 items-center justify-center rounded-full active:bg-button-secondary-bg-hover\"\n >\n <CloseIcon color={closeColor} />\n </Pressable>\n )}\n </View>\n </Animated.View>\n )\n}\n\nexport function useToast(): ToastContextValue {\n const context = useContext(ToastContext)\n if (!context) throw new Error('useToast precisa estar dentro de <ToastProvider>.')\n return context\n}\n"],"mappings":";;;;;;;;AA0DA,MAAM,eAAe,cAAwC,IAAI;AAYjE,IAAI,SAAS,OAAO,WAClB,UAAU,wCAAwC,IAAI;;AAIxD,MAAM,iBAAiB;AACvB,MAAM,iBAAiB;AAGvB,MAAM,WAAW,SAAS,OAAO,QAAQ,MAAM,UAAU,iBAAiB,MAAM;;;;;AAMhF,SAAgB,cAAc,EAC5B,UACA,QAAQ,gBACR,eAAe,UACf,UACqB;CACrB,MAAM,CAAC,OAAO,YAAY,SAAsB,CAAC,CAAC;CAClD,MAAM,SAAS,OAAO,CAAC;CAGvB,MAAM,UAAU,aAAa,OAAe;EAC1C,UAAU,YACR,QAAQ,KAAK,SAAU,KAAK,OAAO,KAAK;GAAE,GAAG;GAAM,SAAS;EAAK,IAAI,IAAK,CAC5E;CACF,GAAG,CAAC,CAAC;CACL,MAAM,UAAU,iBAAiB;CACjC,MAAM,SAAS,aACZ,OAAe;EAEd,IAAI,CAAC,SAAS,gBAAgB,cAAc,gBAAgB,QAAQ,aAAa;EACjF,UAAU,YAAY,QAAQ,QAAQ,SAAS,KAAK,OAAO,EAAE,CAAC;CAChE,GACA,CAAC,OAAO,CACV;CACA,MAAM,QAAQ,aAAa,YAA0B;EACnD,MAAM,KAAK,OAAO;EAClB,UAAU,YAAY,CAAC,GAAG,SAAS;GAAE,GAAG;GAAS;GAAI,SAAS;EAAM,CAAC,CAAC;EACtE,OAAO;CACT,GAAG,CAAC,CAAC;CACL,MAAM,QAAQ,eAAe;EAAE;EAAO;CAAQ,IAAI,CAAC,OAAO,OAAO,CAAC;CAElE,OACE,qBAAC,aAAa,UAAd;EAA8B;EAA9B,UAAA,CACG,UACD,oBAAC,MAAD;GACE,eAAc;GACd,oBAAoB;GACZ;GACR,OAAO;IAAE,UAAU;IAAY,KAAK;IAAG,MAAM;IAAG,OAAO;IAAG,QAAQ;IAAM,WAAW;GAAK;GAExF,UAAA,oBAAC,MAAD;IACE,eAAc;IACd,WAAU;IACV,OAAO,EAAE,YAAY,SAAS;IAE7B,UAAA,MAAM,KAAK,SACV,oBAAC,WAAD;KAEQ;KACQ;KACd,WAAW;KACX,UAAU;IACX,GALM,KAAK,EAKX,CACF;GACG,CAAA;EACF,CAAA,CACe;;AAE3B;AAEA,SAAS,UAAU,EACjB,MACA,cACA,WACA,YAMC;CACD,MAAM,EACJ,IACA,SAAS,QACT,OACA,aACA,QACA,WAAW,KACX,WAAW,MACX,cAAc,MACd,YACE;CACJ,MAAM,WAAW,OAAO,IAAI,SAAS,MAAM,CAAC,CAAC,CAAC,CAAC;CAE/C,MAAM,QAAQ,OAAO,IAAI,SAAS,MAAM,CAAC,CAAC,CAAC,CAAC;CAC5C,MAAM,CAAC,UAAU,eAAe,SAAS,KAAK;CAC9C,MAAM,SAAS,OAAO;EAAE;EAAI;CAAU,CAAC;CACvC,OAAO,UAAU;EAAE;EAAI;CAAU;CACjC,MAAM,eAAe,OACnB,aAAa,OAAO;EAClB,8BAA8B,GAAG,MAAM,KAAK,IAAI,EAAE,EAAE,IAAI,KAAK,KAAK,IAAI,EAAE,EAAE,IAAI,KAAK,IAAI,EAAE,EAAE;EAC3F,wCAAwC;EACxC,2BAA2B,YAAY,IAAI;EAC3C,qBAAqB,GAAG,MAAM,MAAM,SAAS,EAAE,EAAE;EACjD,wBAAwB,GAAG,MAAM;GAC/B,YAAY,KAAK;GACjB,IAAI,KAAK,IAAI,EAAE,EAAE,IAAI,kBAAkB,KAAK,IAAI,EAAE,EAAE,IAAI,gBAAgB;IAEtE,SAAS,OAAO,OAAO;KACrB,UAAU,EAAE,MAAM,EAAE,MAAM,IAAI,MAAM;KACpC,UAAU;KACV,iBAAiB;IACnB,CAAC,CAAC,CAAC,MAAM;IACT,OAAO,QAAQ,UAAU,OAAO,QAAQ,EAAE;GAC5C,OACE,SAAS,OAAO,OAAO;IAAE,SAAS;IAAG,UAAU;IAAK,iBAAiB;GAAK,CAAC,CAAC,CAAC,MAAM;EAEvF;EACA,+BAA+B;GAC7B,YAAY,KAAK;GACjB,SAAS,OAAO,OAAO;IAAE,SAAS;IAAG,UAAU;IAAK,iBAAiB;GAAK,CAAC,CAAC,CAAC,MAAM;EACrF;CACF,CAAC,CACH,CAAC,CAAC;CACF,MAAM,YAAY,MAAM,YAAY;EAClC,YAAY;GAAC;GAAM;GAAG;EAAG;EACzB,aAAa;GAAC;GAAK;GAAG;EAAG;EACzB,aAAa;CACf,CAAC;CACD,MAAM,YAAY,SAAS,gBAAgB,OAAO;CAClD,MAAM,aAAa,SAAS,sBAAsB;CAElD,gBAAgB;EACd,IAAI,SAAS;EACb,MAAM,YAAY,SAAS,OAAO,UAAU;GAC1C,SAAS;GACT,UAAU;GACV,iBAAiB;EACnB,CAAC;EACD,UAAU,MAAM;EAChB,aAAa,UAAU,KAAK;CAC9B,GAAG,CAAC,SAAS,QAAQ,CAAC;CAEtB,gBAAgB;EACd,IAAI,CAAC,SAAS;EACd,MAAM,YAAY,SAAS,OAAO,UAAU;GAC1C,SAAS;GACT,UAAU;GACV,iBAAiB;EACnB,CAAC;EACD,UAAU,OAAO,EAAE,eAAe,YAAY,SAAS,EAAE,CAAC;EAC1D,aAAa,UAAU,KAAK;CAC9B,GAAG;EAAC;EAAS;EAAI;EAAU;CAAQ,CAAC;CAEpC,gBAAgB;EACd,IAAI,WAAW,YAAY,YAAY,GAAG;EAC1C,MAAM,QAAQ,iBAAiB,UAAU,EAAE,GAAG,QAAQ;EACtD,aAAa,aAAa,KAAK;CACjC,GAAG;EAAC;EAAS;EAAU;EAAU;EAAI;CAAS,CAAC;CAE/C,MAAM,aAAa,SAAS,YAAY;EAAE,YAAY,CAAC,GAAG,CAAC;EAAG,aAAa,CAAC,KAAK,CAAC;CAAE,CAAC;CAErF,OACE,oBAAC,SAAS,MAAV;EACE,GAAI,aAAa;EACjB,OAAO;GACL,SAAS,SAAS,SAAS,UAAU,SAAS;GAC9C,WAAW,CAAC,EAAE,WAAW,GAAG,EAAE,YAAY,MAAM,CAAC;EACnD;EAEA,UAAA,qBAAC,MAAD;GACE,mBAAmB,WAAW,MAAM;GAEpC,yBAAyB,WAAW,MAAM,IAAI,cAAc;GAC5D,WAAU;GACV,OAAO,EAAE,WAAW,EAAE;GALxB,UAAA;IAOG,YAAY,oBAAC,YAAD;KAAoB;KAAQ,OAAO;IAAY,CAAA;IAC5D,qBAAC,MAAD;KAAM,WAAU;KAAhB,UAAA;MACG,QACC,oBAAC,MAAD;OAAM,WAAW,iCAAiC,WAAW;OAAY,UAAA;MAAY,CAAA,IACnF;MACJ,oBAAC,MAAD;OAAM,WAAU;OAAuC,UAAA;MAAkB,CAAA;MACxE,UACC,oBAAC,QAAD;OACE,SAAQ;OACR,eAAe;QACb,OAAO,QAAQ;QACf,UAAU,EAAE;OACd;OAEC,UAAA,OAAO;MACF,CAAA;KAEN;;IACL,eACC,oBAAC,WAAD;KACE,mBAAkB;KAClB,oBAAoB;KACpB,SAAS;KACT,eAAe,UAAU,EAAE;KAC3B,WAAU;KAEV,UAAA,oBAAC,WAAD,EAAW,OAAO,WAAa,CAAA;IACtB,CAAA;GAET;;CACO,CAAA;AAEnB;AAEA,SAAgB,WAA8B;CAC5C,MAAM,UAAU,WAAW,YAAY;CACvC,IAAI,CAAC,SAAS,MAAM,IAAI,MAAM,mDAAmD;CACjF,OAAO;AACT"}
|
|
1
|
+
{"version":3,"file":"toast.js","names":[],"sources":["../../../src/components/toast/toast.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport {\n createContext,\n type ReactNode,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport {\n Animated,\n LayoutAnimation,\n PanResponder,\n Platform,\n Pressable,\n StatusBar,\n Text,\n UIManager,\n View,\n} from 'react-native'\nimport { useReducedMotion } from '../_shared/sliding-indicator'\nimport {\n CloseIcon,\n type Status,\n StatusIcon,\n statusRole,\n statusText,\n statusTextToken,\n} from '../_shared/status'\nimport { useToken } from '../_shared/use-token'\nimport { Button } from '../button/button'\n\nexport type ToastOptions = {\n status?: Status\n /** Resumo (opcional). */\n title?: string\n description: string\n /** Ação contextual (ex.: \"Desfazer\"). Tocar nela também fecha o toast. */\n action?: { label: string; onClick: () => void }\n /** Tempo até fechar sozinho, em ms. `0` não fecha sozinho. Padrão: 5000. */\n duration?: number\n /** Mostra o ícone do status. Padrão: `true`. */\n showIcon?: boolean\n /** Mostra o botão de fechar (×). Sem ele, o toast ainda fecha pelo `duration`. Padrão: `true`. */\n dismissible?: boolean\n}\n\ntype ToastItem = ToastOptions & { id: number; closing: boolean }\n\ntype ToastContextValue = {\n /** Exibe um toast e devolve o id, para fechá-lo com `dismiss`. */\n toast: (options: ToastOptions) => number\n dismiss: (id: number) => void\n}\n\nconst ToastContext = createContext<ToastContextValue | null>(null)\n\nexport type ToastProviderProps = {\n children: ReactNode\n /** Nome acessível da região de notificações. */\n label?: string\n /** Nome acessível do botão de fechar de cada toast. */\n dismissLabel?: string\n testID?: string\n}\n\n// No Android antigo o LayoutAnimation precisa ser habilitado. Na New Architecture a chamada é no-op.\nif (Platform.OS === 'android') {\n UIManager.setLayoutAnimationEnabledExperimental?.(true)\n}\n\n/** Arrastar o toast para o lado além disto (px), ou mais rápido que isto (px/ms), o dispensa. */\nconst SWIPE_DISTANCE = 50\nconst SWIPE_VELOCITY = 0.5\n\n// Sem react-native-safe-area-context: margem fixa para o notch no iOS e a barra de status no Android.\nconst topInset = Platform.OS === 'ios' ? 56 : (StatusBar.currentHeight ?? 24) + 8\n\n/**\n * Envolva o app uma vez; qualquer componente abaixo dispara toasts com `useToast()`. A pilha fica\n * no topo da tela, acima do conteúdo do app, mas abaixo de Modal/Drawer (que são janelas nativas).\n */\nexport function ToastProvider({\n children,\n label = 'Notificações',\n dismissLabel = 'Fechar',\n testID,\n}: ToastProviderProps) {\n const [items, setItems] = useState<ToastItem[]>([])\n const nextId = useRef(0)\n\n // Marca como \"closing\"; o próprio toast anima a saída e então chama `remove`.\n const dismiss = useCallback((id: number) => {\n setItems((current) =>\n current.map((item) => (item.id === id ? { ...item, closing: true } : item)),\n )\n }, [])\n const reduced = useReducedMotion()\n const remove = useCallback(\n (id: number) => {\n // os de baixo deslizam para o lugar em vez de pular\n if (!reduced) LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)\n setItems((current) => current.filter((item) => item.id !== id))\n },\n [reduced],\n )\n const toast = useCallback((options: ToastOptions) => {\n const id = nextId.current++\n setItems((current) => [...current, { ...options, id, closing: false }])\n return id\n }, [])\n const value = useMemo(() => ({ toast, dismiss }), [toast, dismiss])\n\n return (\n <ToastContext.Provider value={value}>\n {children}\n <View\n pointerEvents=\"box-none\"\n accessibilityLabel={label}\n testID={testID}\n style={{ position: 'absolute', top: 0, left: 0, right: 0, zIndex: 9999, elevation: 9999 }}\n >\n <View\n pointerEvents=\"box-none\"\n className=\"w-full max-w-[360px] gap-2 self-center px-4\"\n style={{ paddingTop: topInset }}\n >\n {items.map((item) => (\n <ToastView\n key={item.id}\n item={item}\n dismissLabel={dismissLabel}\n onDismiss={dismiss}\n onRemove={remove}\n />\n ))}\n </View>\n </View>\n </ToastContext.Provider>\n )\n}\n\nfunction ToastView({\n item,\n dismissLabel,\n onDismiss,\n onRemove,\n}: {\n item: ToastItem\n dismissLabel: string\n onDismiss: (id: number) => void\n onRemove: (id: number) => void\n}) {\n const {\n id,\n status = 'info',\n title,\n description,\n action,\n duration = 5000,\n showIcon = true,\n dismissible = true,\n closing,\n } = item\n const progress = useRef(new Animated.Value(0)).current\n // arrastar para o lado dispensa (como o swipe do Radix no web); o auto-fechar pausa enquanto isso\n const dragX = useRef(new Animated.Value(0)).current\n const [dragging, setDragging] = useState(false)\n const latest = useRef({ id, onDismiss })\n latest.current = { id, onDismiss }\n const panResponder = useRef(\n PanResponder.create({\n onMoveShouldSetPanResponder: (_, g) => Math.abs(g.dx) > 6 && Math.abs(g.dx) > Math.abs(g.dy),\n onPanResponderTerminationRequest: () => false,\n onPanResponderGrant: () => setDragging(true),\n onPanResponderMove: (_, g) => dragX.setValue(g.dx),\n onPanResponderRelease: (_, g) => {\n setDragging(false)\n if (Math.abs(g.dx) > SWIPE_DISTANCE || Math.abs(g.vx) > SWIPE_VELOCITY) {\n // continua para o lado de onde o dedo ia, enquanto o toast some\n Animated.timing(dragX, {\n toValue: (g.dx || g.vx) > 0 ? 400 : -400,\n duration: 150,\n useNativeDriver: true,\n }).start()\n latest.current.onDismiss(latest.current.id)\n } else {\n Animated.timing(dragX, { toValue: 0, duration: 200, useNativeDriver: true }).start()\n }\n },\n onPanResponderTerminate: () => {\n setDragging(false)\n Animated.timing(dragX, { toValue: 0, duration: 200, useNativeDriver: true }).start()\n },\n }),\n ).current\n const swipeFade = dragX.interpolate({\n inputRange: [-150, 0, 150],\n outputRange: [0.2, 1, 0.2],\n extrapolate: 'clamp',\n })\n const iconColor = useToken(statusTextToken[status])\n const closeColor = useToken('--color-text-primary')\n\n useEffect(() => {\n if (closing) return\n const animation = Animated.timing(progress, {\n toValue: 1,\n duration: 200,\n useNativeDriver: true,\n })\n animation.start()\n return () => animation.stop()\n }, [closing, progress])\n\n useEffect(() => {\n if (!closing) return\n const animation = Animated.timing(progress, {\n toValue: 0,\n duration: 150,\n useNativeDriver: true,\n })\n animation.start(({ finished }) => finished && onRemove(id))\n return () => animation.stop()\n }, [closing, id, onRemove, progress])\n\n useEffect(() => {\n if (closing || dragging || duration <= 0) return\n const timer = setTimeout(() => onDismiss(id), duration)\n return () => clearTimeout(timer)\n }, [closing, dragging, duration, id, onDismiss])\n\n const translateY = progress.interpolate({ inputRange: [0, 1], outputRange: [-16, 0] })\n\n return (\n <Animated.View\n {...panResponder.panHandlers}\n style={{\n opacity: Animated.multiply(progress, swipeFade),\n transform: [{ translateY }, { translateX: dragX }],\n }}\n >\n <View\n accessibilityRole={statusRole(status)}\n // Erro e aviso interrompem o leitor de tela; o resto é anunciado com calma.\n accessibilityLiveRegion={statusRole(status) ? 'assertive' : 'polite'}\n className=\"flex-row items-start gap-3 rounded-[8px] border border-border-default bg-surface-elevated py-3 pr-3 pl-4 shadow-md\"\n style={{ elevation: 6 }}\n >\n {showIcon && <StatusIcon status={status} color={iconColor} />}\n <View className=\"min-w-0 flex-1 items-start gap-1\">\n {title ? (\n <Text className={`text-sm font-medium leading-5 ${statusText[status]}`}>{title}</Text>\n ) : null}\n <Text className=\"text-sm leading-5 text-text-primary\">{description}</Text>\n {action && (\n <Button\n variant=\"ghost\"\n onPress={() => {\n action.onClick()\n onDismiss(id)\n }}\n >\n {action.label}\n </Button>\n )}\n </View>\n {dismissible && (\n <Pressable\n accessibilityRole=\"button\"\n accessibilityLabel={dismissLabel}\n hitSlop={8}\n onPress={() => onDismiss(id)}\n className=\"size-8 items-center justify-center rounded-full active:bg-button-secondary-bg-hover\"\n >\n <CloseIcon color={closeColor} />\n </Pressable>\n )}\n </View>\n </Animated.View>\n )\n}\n\nexport function useToast(): ToastContextValue {\n const context = useContext(ToastContext)\n if (!context) throw new Error('useToast precisa estar dentro de <ToastProvider>.')\n return context\n}\n"],"mappings":"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"}
|
|
@@ -36,7 +36,7 @@ function FileRow({ file, onRemove, removeLabel }) {
|
|
|
36
36
|
const detail = isError && file.errorMessage ? file.errorMessage : void 0;
|
|
37
37
|
const size = file.size === void 0 ? void 0 : formatSize(file.size);
|
|
38
38
|
return /* @__PURE__ */ jsxs(View, {
|
|
39
|
-
className: cn("gap-2 rounded-
|
|
39
|
+
className: cn("gap-2 rounded-[8px] border bg-field-bg px-3 py-2", isError ? "border-field-border-error" : "border-field-border"),
|
|
40
40
|
children: [/* @__PURE__ */ jsxs(View, {
|
|
41
41
|
className: "flex-row items-center gap-2",
|
|
42
42
|
children: [
|
|
@@ -99,7 +99,7 @@ function Upload({ onPick, files, onRemoveFile, title = "Toque para escolher arqu
|
|
|
99
99
|
disabled,
|
|
100
100
|
onPress: onPick,
|
|
101
101
|
testID,
|
|
102
|
-
className: cn("items-center gap-1 rounded-
|
|
102
|
+
className: cn("items-center gap-1 rounded-[8px] border border-dashed bg-field-bg px-6 py-8", error ? "border-field-border-error" : "border-field-border", "active:border-interactive-primary active:bg-button-secondary-bg", "disabled:border-field-border-disabled disabled:bg-field-bg-disabled", className),
|
|
103
103
|
children: [
|
|
104
104
|
/* @__PURE__ */ jsx(View, {
|
|
105
105
|
className: "mb-1",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upload.js","names":[],"sources":["../../../src/components/upload/upload.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport type { ReactNode } from 'react'\nimport { Pressable, Text, View } from 'react-native'\nimport Svg, { Path } from 'react-native-svg'\nimport { cn } from '../../utils/cn'\nimport { CheckIcon, CloseIcon } from '../_shared/status'\nimport { useToken } from '../_shared/use-token'\n\nexport type UploadFileStatus = 'uploading' | 'done' | 'error'\n\nexport type UploadFile = {\n /** Identificador estável do arquivo; volta em `onRemoveFile`. */\n id: string\n name: string\n /** Tamanho em bytes; sem ele, nada é mostrado. */\n size?: number\n /** `uploading` mostra a barra de progresso; `done` e `error` mostram o resultado. */\n status?: UploadFileStatus\n /** Progresso de 0 a 100, só para `uploading`. Sem ele, a barra fica em 0. */\n progress?: number\n /** Texto do erro, mostrado no lugar do tamanho quando `status=\"error\"`. */\n errorMessage?: string\n}\n\nexport type UploadProps = {\n /**\n * Chamado ao tocar na área. O componente não escolhe arquivos: abra o seletor que preferir e\n * guarde o resultado no estado que alimenta `files`. Com expo-document-picker:\n *\n * import * as DocumentPicker from 'expo-document-picker'\n *\n * async function pick() {\n * const result = await DocumentPicker.getDocumentAsync({ multiple: true })\n * if (result.canceled) return\n * setFiles((prev) => [\n * ...prev,\n * ...result.assets.map((a) => ({\n * id: a.uri,\n * name: a.name,\n * size: a.size,\n * status: 'uploading' as const,\n * })),\n * ])\n * }\n * <Upload onPick={pick} files={files} onRemoveFile={(id) => setFiles(...)} />\n */\n onPick: () => void\n /** Arquivos já escolhidos, listados abaixo da área com tamanho, status e botão de remover. */\n files?: UploadFile[]\n /** Chamado ao tocar no × de um arquivo. Sem ele, o botão não aparece. */\n onRemoveFile?: (id: string) => void\n /** Texto principal da área. */\n title?: ReactNode\n /** Texto de apoio (formatos e tamanho aceitos, por exemplo). */\n description?: ReactNode\n /** Borda em vermelho, para quando os arquivos escolhidos têm problema. */\n error?: boolean\n disabled?: boolean\n /** Prefixo do rótulo acessível do botão de remover (\"Remover contrato.pdf\"). */\n removeLabel?: string\n className?: string\n accessibilityLabel?: string\n testID?: string\n}\n\n/** \"1,5 MB\", \"230 KB\", \"12 B\". */\nfunction formatSize(bytes: number) {\n if (bytes < 1024) return `${bytes} B`\n const kb = bytes / 1024\n if (kb < 1024) return `${Math.round(kb)} KB`\n const mb = kb / 1024\n return `${(mb < 10 ? Math.round(mb * 10) / 10 : Math.round(mb)).toString().replace('.', ',')} MB`\n}\n\nfunction UploadIcon({ color }: { color?: string }) {\n return (\n <Svg width={24} height={24} viewBox=\"0 0 24 24\" fill=\"none\" stroke={color} strokeWidth={1.5}>\n <Path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n d=\"M12 16V4M7 9l5-5 5 5M4 16v3a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-3\"\n />\n </Svg>\n )\n}\n\nfunction FileRow({\n file,\n onRemove,\n removeLabel,\n}: {\n file: UploadFile\n onRemove?: (id: string) => void\n removeLabel: string\n}) {\n const successColor = useToken('--color-status-success-text')\n const iconColor = useToken('--color-text-secondary')\n const progress = Math.min(100, Math.max(0, file.progress ?? 0))\n const isError = file.status === 'error'\n const detail = isError && file.errorMessage ? file.errorMessage : undefined\n const size = file.size === undefined ? undefined : formatSize(file.size)\n\n return (\n <View\n className={cn(\n 'gap-2 rounded-md border bg-field-bg px-3 py-2',\n isError ? 'border-field-border-error' : 'border-field-border',\n )}\n >\n <View className=\"flex-row items-center gap-2\">\n <View className=\"flex-1\">\n <Text numberOfLines={1} className=\"text-sm font-medium text-text-primary\">\n {file.name}\n </Text>\n {detail || size ? (\n <Text\n className={cn('text-xs', isError ? 'text-status-error-text' : 'text-text-secondary')}\n >\n {detail ?? size}\n </Text>\n ) : null}\n </View>\n {file.status === 'done' ? (\n <View accessible accessibilityLabel=\"Concluído\">\n <CheckIcon color={successColor} />\n </View>\n ) : null}\n {onRemove ? (\n <Pressable\n accessibilityRole=\"button\"\n accessibilityLabel={`${removeLabel} ${file.name}`}\n hitSlop={8}\n onPress={() => onRemove(file.id)}\n className=\"size-8 items-center justify-center rounded-full active:bg-bg-tertiary\"\n >\n <CloseIcon color={iconColor} />\n </Pressable>\n ) : null}\n </View>\n {file.status === 'uploading' ? (\n <View\n accessible\n accessibilityRole=\"progressbar\"\n accessibilityLabel={file.name}\n accessibilityValue={{ min: 0, max: 100, now: progress }}\n className=\"h-1 overflow-hidden rounded-full bg-bg-tertiary\"\n >\n <View\n className=\"h-full rounded-full bg-interactive-primary\"\n style={{ width: `${progress}%` }}\n />\n </View>\n ) : null}\n </View>\n )\n}\n\n/**\n * Área para escolher arquivos tocando, mais a lista dos escolhidos. No native não há dropzone nem\n * `<input type=\"file\">`, e o seletor depende do app (expo-document-picker, image-picker...): por\n * isso o componente é controlado e agnóstico. `onPick` abre o seu seletor; `files` e\n * `onRemoveFile` descrevem a lista. Validar tipo e tamanho e enviar ao servidor ficam com você.\n */\nexport function Upload({\n onPick,\n files,\n onRemoveFile,\n title = 'Toque para escolher arquivos',\n description,\n error,\n disabled,\n removeLabel = 'Remover',\n className,\n accessibilityLabel,\n testID,\n}: UploadProps) {\n const iconColor = useToken('--color-text-secondary')\n\n return (\n <View className=\"gap-2\">\n <Pressable\n accessibilityRole=\"button\"\n accessibilityLabel={\n accessibilityLabel ??\n (typeof title === 'string' && typeof description === 'string'\n ? `${title}. ${description}`\n : typeof title === 'string'\n ? title\n : undefined)\n }\n accessibilityState={{ disabled: Boolean(disabled) }}\n disabled={disabled}\n onPress={onPick}\n testID={testID}\n className={cn(\n 'items-center gap-1 rounded-md border border-dashed bg-field-bg px-6 py-8',\n error ? 'border-field-border-error' : 'border-field-border',\n 'active:border-interactive-primary active:bg-button-secondary-bg',\n 'disabled:border-field-border-disabled disabled:bg-field-bg-disabled',\n className,\n )}\n >\n <View className=\"mb-1\">\n <UploadIcon color={iconColor} />\n </View>\n <Text\n className={cn(\n 'text-center text-sm font-medium text-text-primary',\n disabled && 'text-field-text-disabled',\n )}\n >\n {title}\n </Text>\n {description ? (\n <Text\n className={cn(\n 'text-center text-xs text-text-secondary',\n disabled && 'text-field-text-disabled',\n )}\n >\n {description}\n </Text>\n ) : null}\n </Pressable>\n {files && files.length > 0 ? (\n <View className=\"gap-2\">\n {files.map((file) => (\n <FileRow key={file.id} file={file} onRemove={onRemoveFile} removeLabel={removeLabel} />\n ))}\n </View>\n ) : null}\n </View>\n )\n}\n"],"mappings":";;;;;;;;AAmEA,SAAS,WAAW,OAAe;CACjC,IAAI,QAAQ,MAAM,OAAO,GAAG,MAAM;CAClC,MAAM,KAAK,QAAQ;CACnB,IAAI,KAAK,MAAM,OAAO,GAAG,KAAK,MAAM,EAAE,EAAE;CACxC,MAAM,KAAK,KAAK;CAChB,OAAO,IAAI,KAAK,KAAK,KAAK,MAAM,KAAK,EAAE,IAAI,KAAK,KAAK,MAAM,EAAE,EAAA,CAAG,SAAS,CAAC,CAAC,QAAQ,KAAK,GAAG,EAAE;AAC/F;AAEA,SAAS,WAAW,EAAE,SAA6B;CACjD,OACE,oBAAC,KAAD;EAAK,OAAO;EAAI,QAAQ;EAAI,SAAQ;EAAY,MAAK;EAAO,QAAQ;EAAO,aAAa;EACtF,UAAA,oBAAC,MAAD;GACE,eAAc;GACd,gBAAe;GACf,GAAE;EACH,CAAA;CACE,CAAA;AAET;AAEA,SAAS,QAAQ,EACf,MACA,UACA,eAKC;CACD,MAAM,eAAe,SAAS,6BAA6B;CAC3D,MAAM,YAAY,SAAS,wBAAwB;CACnD,MAAM,WAAW,KAAK,IAAI,KAAK,KAAK,IAAI,GAAG,KAAK,YAAY,CAAC,CAAC;CAC9D,MAAM,UAAU,KAAK,WAAW;CAChC,MAAM,SAAS,WAAW,KAAK,eAAe,KAAK,eAAe,KAAA;CAClE,MAAM,OAAO,KAAK,SAAS,KAAA,IAAY,KAAA,IAAY,WAAW,KAAK,IAAI;CAEvE,OACE,qBAAC,MAAD;EACE,WAAW,GACT,iDACA,UAAU,8BAA8B,qBAC1C;EAJF,UAAA,CAME,qBAAC,MAAD;GAAM,WAAU;GAAhB,UAAA;IACE,qBAAC,MAAD;KAAM,WAAU;KAAhB,UAAA,CACE,oBAAC,MAAD;MAAM,eAAe;MAAG,WAAU;MAC/B,UAAA,KAAK;KACF,CAAA,GACL,UAAU,OACT,oBAAC,MAAD;MACE,WAAW,GAAG,WAAW,UAAU,2BAA2B,qBAAqB;MAElF,UAAA,UAAU;KACP,CAAA,IACJ,IACA;;IACL,KAAK,WAAW,SACf,oBAAC,MAAD;KAAM,YAAA;KAAW,oBAAmB;KAClC,UAAA,oBAAC,WAAD,EAAW,OAAO,aAAe,CAAA;IAC7B,CAAA,IACJ;IACH,WACC,oBAAC,WAAD;KACE,mBAAkB;KAClB,oBAAoB,GAAG,YAAY,GAAG,KAAK;KAC3C,SAAS;KACT,eAAe,SAAS,KAAK,EAAE;KAC/B,WAAU;KAEV,UAAA,oBAAC,WAAD,EAAW,OAAO,UAAY,CAAA;IACrB,CAAA,IACT;GACA;EACL,CAAA,GAAA,KAAK,WAAW,cACf,oBAAC,MAAD;GACE,YAAA;GACA,mBAAkB;GAClB,oBAAoB,KAAK;GACzB,oBAAoB;IAAE,KAAK;IAAG,KAAK;IAAK,KAAK;GAAS;GACtD,WAAU;GAEV,UAAA,oBAAC,MAAD;IACE,WAAU;IACV,OAAO,EAAE,OAAO,GAAG,SAAS,GAAG;GAChC,CAAA;EACG,CAAA,IACJ,IACA;;AAEV;;;;;;;AAQA,SAAgB,OAAO,EACrB,QACA,OACA,cACA,QAAQ,gCACR,aACA,OACA,UACA,cAAc,WACd,WACA,oBACA,UACc;CACd,MAAM,YAAY,SAAS,wBAAwB;CAEnD,OACE,qBAAC,MAAD;EAAM,WAAU;EAAhB,UAAA,CACE,qBAAC,WAAD;GACE,mBAAkB;GAClB,oBACE,uBACC,OAAO,UAAU,YAAY,OAAO,gBAAgB,WACjD,GAAG,MAAM,IAAI,gBACb,OAAO,UAAU,WACf,QACA,KAAA;GAER,oBAAoB,EAAE,UAAU,QAAQ,QAAQ,EAAE;GACxC;GACV,SAAS;GACD;GACR,WAAW,GACT,4EACA,QAAQ,8BAA8B,uBACtC,mEACA,uEACA,SACF;GApBF,UAAA;IAsBE,oBAAC,MAAD;KAAM,WAAU;KACd,UAAA,oBAAC,YAAD,EAAY,OAAO,UAAY,CAAA;IAC3B,CAAA;IACN,oBAAC,MAAD;KACE,WAAW,GACT,qDACA,YAAY,0BACd;KAEC,UAAA;IACG,CAAA;IACL,cACC,oBAAC,MAAD;KACE,WAAW,GACT,2CACA,YAAY,0BACd;KAEC,UAAA;IACG,CAAA,IACJ;GACK;EACV,CAAA,GAAA,SAAS,MAAM,SAAS,IACvB,oBAAC,MAAD;GAAM,WAAU;GACb,UAAA,MAAM,KAAK,SACV,oBAAC,SAAD;IAA6B;IAAM,UAAU;IAA2B;GAAc,GAAxE,KAAK,EAAmE,CACvF;EACG,CAAA,IACJ,IACA;;AAEV"}
|
|
1
|
+
{"version":3,"file":"upload.js","names":[],"sources":["../../../src/components/upload/upload.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport type { ReactNode } from 'react'\nimport { Pressable, Text, View } from 'react-native'\nimport Svg, { Path } from 'react-native-svg'\nimport { cn } from '../../utils/cn'\nimport { CheckIcon, CloseIcon } from '../_shared/status'\nimport { useToken } from '../_shared/use-token'\n\nexport type UploadFileStatus = 'uploading' | 'done' | 'error'\n\nexport type UploadFile = {\n /** Identificador estável do arquivo; volta em `onRemoveFile`. */\n id: string\n name: string\n /** Tamanho em bytes; sem ele, nada é mostrado. */\n size?: number\n /** `uploading` mostra a barra de progresso; `done` e `error` mostram o resultado. */\n status?: UploadFileStatus\n /** Progresso de 0 a 100, só para `uploading`. Sem ele, a barra fica em 0. */\n progress?: number\n /** Texto do erro, mostrado no lugar do tamanho quando `status=\"error\"`. */\n errorMessage?: string\n}\n\nexport type UploadProps = {\n /**\n * Chamado ao tocar na área. O componente não escolhe arquivos: abra o seletor que preferir e\n * guarde o resultado no estado que alimenta `files`. Com expo-document-picker:\n *\n * import * as DocumentPicker from 'expo-document-picker'\n *\n * async function pick() {\n * const result = await DocumentPicker.getDocumentAsync({ multiple: true })\n * if (result.canceled) return\n * setFiles((prev) => [\n * ...prev,\n * ...result.assets.map((a) => ({\n * id: a.uri,\n * name: a.name,\n * size: a.size,\n * status: 'uploading' as const,\n * })),\n * ])\n * }\n * <Upload onPick={pick} files={files} onRemoveFile={(id) => setFiles(...)} />\n */\n onPick: () => void\n /** Arquivos já escolhidos, listados abaixo da área com tamanho, status e botão de remover. */\n files?: UploadFile[]\n /** Chamado ao tocar no × de um arquivo. Sem ele, o botão não aparece. */\n onRemoveFile?: (id: string) => void\n /** Texto principal da área. */\n title?: ReactNode\n /** Texto de apoio (formatos e tamanho aceitos, por exemplo). */\n description?: ReactNode\n /** Borda em vermelho, para quando os arquivos escolhidos têm problema. */\n error?: boolean\n disabled?: boolean\n /** Prefixo do rótulo acessível do botão de remover (\"Remover contrato.pdf\"). */\n removeLabel?: string\n className?: string\n accessibilityLabel?: string\n testID?: string\n}\n\n/** \"1,5 MB\", \"230 KB\", \"12 B\". */\nfunction formatSize(bytes: number) {\n if (bytes < 1024) return `${bytes} B`\n const kb = bytes / 1024\n if (kb < 1024) return `${Math.round(kb)} KB`\n const mb = kb / 1024\n return `${(mb < 10 ? Math.round(mb * 10) / 10 : Math.round(mb)).toString().replace('.', ',')} MB`\n}\n\nfunction UploadIcon({ color }: { color?: string }) {\n return (\n <Svg width={24} height={24} viewBox=\"0 0 24 24\" fill=\"none\" stroke={color} strokeWidth={1.5}>\n <Path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n d=\"M12 16V4M7 9l5-5 5 5M4 16v3a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-3\"\n />\n </Svg>\n )\n}\n\nfunction FileRow({\n file,\n onRemove,\n removeLabel,\n}: {\n file: UploadFile\n onRemove?: (id: string) => void\n removeLabel: string\n}) {\n const successColor = useToken('--color-status-success-text')\n const iconColor = useToken('--color-text-secondary')\n const progress = Math.min(100, Math.max(0, file.progress ?? 0))\n const isError = file.status === 'error'\n const detail = isError && file.errorMessage ? file.errorMessage : undefined\n const size = file.size === undefined ? undefined : formatSize(file.size)\n\n return (\n <View\n className={cn(\n 'gap-2 rounded-[8px] border bg-field-bg px-3 py-2',\n isError ? 'border-field-border-error' : 'border-field-border',\n )}\n >\n <View className=\"flex-row items-center gap-2\">\n <View className=\"flex-1\">\n <Text numberOfLines={1} className=\"text-sm font-medium text-text-primary\">\n {file.name}\n </Text>\n {detail || size ? (\n <Text\n className={cn('text-xs', isError ? 'text-status-error-text' : 'text-text-secondary')}\n >\n {detail ?? size}\n </Text>\n ) : null}\n </View>\n {file.status === 'done' ? (\n <View accessible accessibilityLabel=\"Concluído\">\n <CheckIcon color={successColor} />\n </View>\n ) : null}\n {onRemove ? (\n <Pressable\n accessibilityRole=\"button\"\n accessibilityLabel={`${removeLabel} ${file.name}`}\n hitSlop={8}\n onPress={() => onRemove(file.id)}\n className=\"size-8 items-center justify-center rounded-full active:bg-bg-tertiary\"\n >\n <CloseIcon color={iconColor} />\n </Pressable>\n ) : null}\n </View>\n {file.status === 'uploading' ? (\n <View\n accessible\n accessibilityRole=\"progressbar\"\n accessibilityLabel={file.name}\n accessibilityValue={{ min: 0, max: 100, now: progress }}\n className=\"h-1 overflow-hidden rounded-full bg-bg-tertiary\"\n >\n <View\n className=\"h-full rounded-full bg-interactive-primary\"\n style={{ width: `${progress}%` }}\n />\n </View>\n ) : null}\n </View>\n )\n}\n\n/**\n * Área para escolher arquivos tocando, mais a lista dos escolhidos. No native não há dropzone nem\n * `<input type=\"file\">`, e o seletor depende do app (expo-document-picker, image-picker...): por\n * isso o componente é controlado e agnóstico. `onPick` abre o seu seletor; `files` e\n * `onRemoveFile` descrevem a lista. Validar tipo e tamanho e enviar ao servidor ficam com você.\n */\nexport function Upload({\n onPick,\n files,\n onRemoveFile,\n title = 'Toque para escolher arquivos',\n description,\n error,\n disabled,\n removeLabel = 'Remover',\n className,\n accessibilityLabel,\n testID,\n}: UploadProps) {\n const iconColor = useToken('--color-text-secondary')\n\n return (\n <View className=\"gap-2\">\n <Pressable\n accessibilityRole=\"button\"\n accessibilityLabel={\n accessibilityLabel ??\n (typeof title === 'string' && typeof description === 'string'\n ? `${title}. ${description}`\n : typeof title === 'string'\n ? title\n : undefined)\n }\n accessibilityState={{ disabled: Boolean(disabled) }}\n disabled={disabled}\n onPress={onPick}\n testID={testID}\n className={cn(\n 'items-center gap-1 rounded-[8px] border border-dashed bg-field-bg px-6 py-8',\n error ? 'border-field-border-error' : 'border-field-border',\n 'active:border-interactive-primary active:bg-button-secondary-bg',\n 'disabled:border-field-border-disabled disabled:bg-field-bg-disabled',\n className,\n )}\n >\n <View className=\"mb-1\">\n <UploadIcon color={iconColor} />\n </View>\n <Text\n className={cn(\n 'text-center text-sm font-medium text-text-primary',\n disabled && 'text-field-text-disabled',\n )}\n >\n {title}\n </Text>\n {description ? (\n <Text\n className={cn(\n 'text-center text-xs text-text-secondary',\n disabled && 'text-field-text-disabled',\n )}\n >\n {description}\n </Text>\n ) : null}\n </Pressable>\n {files && files.length > 0 ? (\n <View className=\"gap-2\">\n {files.map((file) => (\n <FileRow key={file.id} file={file} onRemove={onRemoveFile} removeLabel={removeLabel} />\n ))}\n </View>\n ) : null}\n </View>\n )\n}\n"],"mappings":"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"}
|
|
@@ -13,7 +13,11 @@ export type VideoPlayerProps = Omit<ViewProps, 'style'> & {
|
|
|
13
13
|
position?: number;
|
|
14
14
|
/** Duração total, em segundos. */
|
|
15
15
|
duration?: number;
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* Em tela cheia: mostra o ícone de sair. Só vale com `onToggleFullscreen`. Ligue ao estado do
|
|
18
|
+
* vídeo (`onFullscreenEnter`/`onFullscreenExit` do `VideoView`): no iOS, é com ele que o
|
|
19
|
+
* `VideoPlayer` liga os controles do sistema na tela cheia.
|
|
20
|
+
*/
|
|
17
21
|
fullscreen?: boolean;
|
|
18
22
|
onPlayPause?: () => void;
|
|
19
23
|
/** Chamado com a posição (em segundos) tocada ou arrastada na barra. */
|
|
@@ -2,7 +2,7 @@ import { cn } from "../../utils/cn.js";
|
|
|
2
2
|
import { useToken } from "../_shared/use-token.js";
|
|
3
3
|
import { formatTime, useSeek } from "../_shared/media.js";
|
|
4
4
|
import { MaximizeIcon, MinimizeIcon, PauseIcon, PlayIcon } from "../_shared/media-icons.js";
|
|
5
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
5
|
+
import { cloneElement, isValidElement, useCallback, useEffect, useRef, useState } from "react";
|
|
6
6
|
import { AccessibilityInfo, Pressable, Text, View } from "react-native";
|
|
7
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
8
|
//#region src/components/video-player/video-player.tsx
|
|
@@ -28,6 +28,10 @@ function VideoPlayer({ className, children, label = "Vídeo", playing = false, p
|
|
|
28
28
|
const visible = active || !playing || screenReader;
|
|
29
29
|
const progress = duration ? Math.min(position / duration, 1) * 100 : 0;
|
|
30
30
|
const ended = !playing && duration > 0 && position >= duration - .1;
|
|
31
|
+
const [entering, setEntering] = useState(false);
|
|
32
|
+
const toggleFullscreen = useRef(onToggleFullscreen);
|
|
33
|
+
toggleFullscreen.current = onToggleFullscreen;
|
|
34
|
+
const nativeControls = entering || fullscreen;
|
|
31
35
|
useEffect(() => {
|
|
32
36
|
AccessibilityInfo.isScreenReaderEnabled().then(setScreenReader);
|
|
33
37
|
const subscription = AccessibilityInfo.addEventListener("screenReaderChanged", setScreenReader);
|
|
@@ -47,19 +51,25 @@ function VideoPlayer({ className, children, label = "Vídeo", playing = false, p
|
|
|
47
51
|
else setActive(true);
|
|
48
52
|
return () => clearTimeout(timer.current);
|
|
49
53
|
}, [playing, show]);
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!entering) return;
|
|
56
|
+
toggleFullscreen.current?.();
|
|
57
|
+
const done = setTimeout(() => setEntering(false), 2e3);
|
|
58
|
+
return () => clearTimeout(done);
|
|
59
|
+
}, [entering]);
|
|
50
60
|
const playPause = () => {
|
|
51
61
|
if (ended) onSeek?.(0);
|
|
52
62
|
onPlayPause?.();
|
|
53
63
|
};
|
|
54
64
|
return /* @__PURE__ */ jsxs(View, {
|
|
55
65
|
accessibilityLabel: label,
|
|
56
|
-
className: cn("aspect-video w-full overflow-hidden rounded-
|
|
66
|
+
className: cn("aspect-video w-full overflow-hidden rounded-[12px] bg-media-surface shadow-lg", className),
|
|
57
67
|
...props,
|
|
58
68
|
children: [
|
|
59
69
|
/* @__PURE__ */ jsx(View, {
|
|
60
70
|
pointerEvents: "none",
|
|
61
71
|
className: "absolute inset-0",
|
|
62
|
-
children
|
|
72
|
+
children: isValidElement(children) && children.props.nativeControls !== void 0 ? cloneElement(children, { nativeControls: nativeControls || children.props.nativeControls }) : children
|
|
63
73
|
}),
|
|
64
74
|
/* @__PURE__ */ jsx(Pressable, {
|
|
65
75
|
accessible: false,
|
|
@@ -118,7 +128,7 @@ function VideoPlayer({ className, children, label = "Vídeo", playing = false, p
|
|
|
118
128
|
}), onToggleFullscreen && /* @__PURE__ */ jsx(Pressable, {
|
|
119
129
|
accessibilityRole: "button",
|
|
120
130
|
accessibilityLabel: fullscreen ? "Sair da tela cheia" : "Tela cheia",
|
|
121
|
-
onPress: onToggleFullscreen,
|
|
131
|
+
onPress: () => fullscreen ? onToggleFullscreen() : setEntering(true),
|
|
122
132
|
className: button,
|
|
123
133
|
children: fullscreen ? /* @__PURE__ */ jsx(MinimizeIcon, { color: onSurface }) : /* @__PURE__ */ jsx(MaximizeIcon, { color: onSurface })
|
|
124
134
|
})]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"video-player.js","names":[],"sources":["../../../src/components/video-player/video-player.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { type ReactNode, useCallback, useEffect, useRef, useState } from 'react'\nimport { AccessibilityInfo, Pressable, Text, View, type ViewProps } from 'react-native'\nimport { cn } from '../../utils/cn'\nimport { formatTime, useSeek } from '../_shared/media'\nimport { MaximizeIcon, MinimizeIcon, PauseIcon, PlayIcon } from '../_shared/media-icons'\nimport { useToken } from '../_shared/use-token'\n\nexport type VideoPlayerProps = Omit<ViewProps, 'style'> & {\n className?: string\n /** A superfície de vídeo (ex.: o `VideoView` do `expo-video`), que preenche o player. */\n children: ReactNode\n /** Nome acessível do player. */\n label?: string\n /** Tocando agora: mostra o botão de pausar e esconde o play central. */\n playing?: boolean\n /** Posição atual, em segundos. */\n position?: number\n /** Duração total, em segundos. */\n duration?: number\n /** Em tela cheia: mostra o ícone de sair. Só vale com `onToggleFullscreen`. */\n fullscreen?: boolean\n onPlayPause?: () => void\n /** Chamado com a posição (em segundos) tocada ou arrastada na barra. */\n onSeek?: (seconds: number) => void\n /** Sem ele, o botão de tela cheia não aparece. */\n onToggleFullscreen?: () => void\n /** Tempo sem interação, em ms, até esconder os controles durante a reprodução. Padrão: 2500. */\n hideControlsAfter?: number\n}\n\n// 44px: o mínimo de área de toque recomendado no iOS (Apple HIG) e perto dos 48dp do Android.\nconst button =\n 'size-11 shrink-0 items-center justify-center rounded-full active:bg-media-on-surface/15'\n\n/**\n * Casca de player de vídeo: play/pause central, posição, tempo e tela cheia sobre o seu vídeo.\n * Não toca nada: quem usa guarda o estado (por exemplo com `expo-video`) e o passa por props,\n * ligando os `on…` ao player de verdade. Tocar no vídeo mostra ou esconde os controles, que também\n * somem sozinhos depois de um tempo sem toques enquanto toca (com leitor de tela ligado, ficam).\n */\nexport function VideoPlayer({\n className,\n children,\n label = 'Vídeo',\n playing = false,\n position = 0,\n duration = 0,\n fullscreen = false,\n onPlayPause,\n onSeek,\n onToggleFullscreen,\n hideControlsAfter = 2500,\n ...props\n}: VideoPlayerProps) {\n const [active, setActive] = useState(true)\n const [screenReader, setScreenReader] = useState(false)\n const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)\n const onSurface = useToken('--color-media-on-surface')\n const accent = useToken('--color-interactive-primary')\n const seek = useSeek({ position, duration, label: 'Posição do vídeo', onSeek })\n const visible = active || !playing || screenReader\n const progress = duration ? Math.min(position / duration, 1) * 100 : 0\n const ended = !playing && duration > 0 && position >= duration - 0.1\n\n useEffect(() => {\n AccessibilityInfo.isScreenReaderEnabled().then(setScreenReader)\n const subscription = AccessibilityInfo.addEventListener('screenReaderChanged', setScreenReader)\n return () => subscription.remove()\n }, [])\n\n const hide = () => {\n clearTimeout(timer.current)\n setActive(false)\n }\n\n const show = useCallback(() => {\n setActive(true)\n clearTimeout(timer.current)\n timer.current = setTimeout(() => setActive(false), hideControlsAfter)\n }, [hideControlsAfter])\n\n useEffect(() => {\n if (playing) show()\n else setActive(true)\n return () => clearTimeout(timer.current)\n }, [playing, show])\n\n // No expo-video, `play()` no fim do vídeo não faz nada: volta ao começo antes.\n const playPause = () => {\n if (ended) onSeek?.(0)\n onPlayPause?.()\n }\n\n return (\n <View\n accessibilityLabel={label}\n className={cn(\n 'aspect-video w-full overflow-hidden rounded-lg bg-media-surface shadow-lg',\n className,\n )}\n {...props}\n >\n <View pointerEvents=\"none\" className=\"absolute inset-0\">\n {children}\n </View>\n {/* Tocar no vídeo só mostra/esconde os controles: pausar sem querer é o erro mais comum no celular. */}\n <Pressable\n accessible={false}\n onPress={() => {\n if (!playing) return\n if (visible) hide()\n else show()\n }}\n className=\"absolute inset-0\"\n >\n {visible && <View pointerEvents=\"none\" className=\"absolute inset-0 bg-media-surface/40\" />}\n </Pressable>\n <View\n pointerEvents={visible ? 'box-none' : 'none'}\n importantForAccessibility={visible ? 'auto' : 'no-hide-descendants'}\n accessibilityElementsHidden={!visible}\n className={cn('absolute inset-0', !visible && 'opacity-0')}\n >\n <View pointerEvents=\"box-none\" className=\"absolute inset-0 items-center justify-center\">\n <Pressable\n accessibilityRole=\"button\"\n accessibilityLabel={playing ? 'Pausar' : 'Reproduzir'}\n onPress={() => {\n playPause()\n show()\n }}\n className=\"size-14 items-center justify-center rounded-full bg-media-on-surface shadow-lg active:opacity-80\"\n >\n {playing ? (\n <PauseIcon filled size={24} color={accent} />\n ) : (\n <PlayIcon filled size={24} color={accent} />\n )}\n </Pressable>\n </View>\n <View pointerEvents=\"box-none\" className=\"absolute inset-x-0 bottom-0\">\n <View\n pointerEvents=\"box-none\"\n className=\"h-11 flex-row items-center justify-between pl-4 pr-1\"\n >\n <Text\n accessible={false}\n className=\"text-xs font-medium leading-4 text-media-on-surface\"\n style={{ fontVariant: ['tabular-nums'] }}\n >\n {formatTime(position)} / {formatTime(duration)}\n </Text>\n {onToggleFullscreen && (\n <Pressable\n accessibilityRole=\"button\"\n accessibilityLabel={fullscreen ? 'Sair da tela cheia' : 'Tela cheia'}\n onPress={onToggleFullscreen}\n className={button}\n >\n {fullscreen ? (\n <MinimizeIcon color={onSurface} />\n ) : (\n <MaximizeIcon color={onSurface} />\n )}\n </Pressable>\n )}\n </View>\n {/* 32px de altura para o dedo acertar a barra de 4px; sobe 8px sobre a folga do botão acima. */}\n <View {...seek.props} className=\"-mt-2 mx-4 h-8 justify-center\">\n {/* Filhos ignoram toques para que `locationX` seja sempre relativo à barra. */}\n <View pointerEvents=\"none\" className=\"h-1 w-full rounded-full bg-media-on-surface/30\">\n <View\n className=\"h-full rounded-full bg-media-accent\"\n style={{ width: `${progress}%` }}\n />\n <View\n className=\"absolute -top-1 size-3 rounded-full bg-media-accent\"\n style={{ left: `${progress}%`, marginLeft: -6 }}\n />\n </View>\n </View>\n </View>\n </View>\n </View>\n )\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"video-player.js","names":[],"sources":["../../../src/components/video-player/video-player.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport {\n cloneElement,\n isValidElement,\n type ReactNode,\n useCallback,\n useEffect,\n useRef,\n useState,\n} from 'react'\nimport { AccessibilityInfo, Pressable, Text, View, type ViewProps } from 'react-native'\nimport { cn } from '../../utils/cn'\nimport { formatTime, useSeek } from '../_shared/media'\nimport { MaximizeIcon, MinimizeIcon, PauseIcon, PlayIcon } from '../_shared/media-icons'\nimport { useToken } from '../_shared/use-token'\n\nexport type VideoPlayerProps = Omit<ViewProps, 'style'> & {\n className?: string\n /** A superfície de vídeo (ex.: o `VideoView` do `expo-video`), que preenche o player. */\n children: ReactNode\n /** Nome acessível do player. */\n label?: string\n /** Tocando agora: mostra o botão de pausar e esconde o play central. */\n playing?: boolean\n /** Posição atual, em segundos. */\n position?: number\n /** Duração total, em segundos. */\n duration?: number\n /**\n * Em tela cheia: mostra o ícone de sair. Só vale com `onToggleFullscreen`. Ligue ao estado do\n * vídeo (`onFullscreenEnter`/`onFullscreenExit` do `VideoView`): no iOS, é com ele que o\n * `VideoPlayer` liga os controles do sistema na tela cheia.\n */\n fullscreen?: boolean\n onPlayPause?: () => void\n /** Chamado com a posição (em segundos) tocada ou arrastada na barra. */\n onSeek?: (seconds: number) => void\n /** Sem ele, o botão de tela cheia não aparece. */\n onToggleFullscreen?: () => void\n /** Tempo sem interação, em ms, até esconder os controles durante a reprodução. Padrão: 2500. */\n hideControlsAfter?: number\n}\n\n// 44px: o mínimo de área de toque recomendado no iOS (Apple HIG) e perto dos 48dp do Android.\nconst button =\n 'size-11 shrink-0 items-center justify-center rounded-full active:bg-media-on-surface/15'\n\n/**\n * Casca de player de vídeo: play/pause central, posição, tempo e tela cheia sobre o seu vídeo.\n * Não toca nada: quem usa guarda o estado (por exemplo com `expo-video`) e o passa por props,\n * ligando os `on…` ao player de verdade. Tocar no vídeo mostra ou esconde os controles, que também\n * somem sozinhos depois de um tempo sem toques enquanto toca (com leitor de tela ligado, ficam).\n */\nexport function VideoPlayer({\n className,\n children,\n label = 'Vídeo',\n playing = false,\n position = 0,\n duration = 0,\n fullscreen = false,\n onPlayPause,\n onSeek,\n onToggleFullscreen,\n hideControlsAfter = 2500,\n ...props\n}: VideoPlayerProps) {\n const [active, setActive] = useState(true)\n const [screenReader, setScreenReader] = useState(false)\n const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)\n const onSurface = useToken('--color-media-on-surface')\n const accent = useToken('--color-interactive-primary')\n const seek = useSeek({ position, duration, label: 'Posição do vídeo', onSeek })\n const visible = active || !playing || screenReader\n const progress = duration ? Math.min(position / duration, 1) * 100 : 0\n const ended = !playing && duration > 0 && position >= duration - 0.1\n // Em tela cheia o vídeo sai do player e os nossos controles não vão junto: ligamos os do sistema.\n // No iOS eles precisam estar ligados antes de abrir, então `entering` vem antes do `onToggleFullscreen`.\n const [entering, setEntering] = useState(false)\n const toggleFullscreen = useRef(onToggleFullscreen)\n toggleFullscreen.current = onToggleFullscreen\n const nativeControls = entering || fullscreen\n\n useEffect(() => {\n AccessibilityInfo.isScreenReaderEnabled().then(setScreenReader)\n const subscription = AccessibilityInfo.addEventListener('screenReaderChanged', setScreenReader)\n return () => subscription.remove()\n }, [])\n\n const hide = () => {\n clearTimeout(timer.current)\n setActive(false)\n }\n\n const show = useCallback(() => {\n setActive(true)\n clearTimeout(timer.current)\n timer.current = setTimeout(() => setActive(false), hideControlsAfter)\n }, [hideControlsAfter])\n\n useEffect(() => {\n if (playing) show()\n else setActive(true)\n return () => clearTimeout(timer.current)\n }, [playing, show])\n\n useEffect(() => {\n if (!entering) return\n toggleFullscreen.current?.()\n // ponytail: se `fullscreen` nunca for ligado ao estado, os controles do sistema voltam a sumir após 2s\n const done = setTimeout(() => setEntering(false), 2000)\n return () => clearTimeout(done)\n }, [entering])\n\n // No expo-video, `play()` no fim do vídeo não faz nada: volta ao começo antes.\n const playPause = () => {\n if (ended) onSeek?.(0)\n onPlayPause?.()\n }\n\n return (\n <View\n accessibilityLabel={label}\n className={cn(\n 'aspect-video w-full overflow-hidden rounded-[12px] bg-media-surface shadow-lg',\n className,\n )}\n {...props}\n >\n <View pointerEvents=\"none\" className=\"absolute inset-0\">\n {isValidElement<{ nativeControls?: boolean }>(children) &&\n children.props.nativeControls !== undefined\n ? cloneElement(children, {\n nativeControls: nativeControls || children.props.nativeControls,\n })\n : children}\n </View>\n {/* Tocar no vídeo só mostra/esconde os controles: pausar sem querer é o erro mais comum no celular. */}\n <Pressable\n accessible={false}\n onPress={() => {\n if (!playing) return\n if (visible) hide()\n else show()\n }}\n className=\"absolute inset-0\"\n >\n {visible && <View pointerEvents=\"none\" className=\"absolute inset-0 bg-media-surface/40\" />}\n </Pressable>\n <View\n pointerEvents={visible ? 'box-none' : 'none'}\n importantForAccessibility={visible ? 'auto' : 'no-hide-descendants'}\n accessibilityElementsHidden={!visible}\n className={cn('absolute inset-0', !visible && 'opacity-0')}\n >\n <View pointerEvents=\"box-none\" className=\"absolute inset-0 items-center justify-center\">\n <Pressable\n accessibilityRole=\"button\"\n accessibilityLabel={playing ? 'Pausar' : 'Reproduzir'}\n onPress={() => {\n playPause()\n show()\n }}\n className=\"size-14 items-center justify-center rounded-full bg-media-on-surface shadow-lg active:opacity-80\"\n >\n {playing ? (\n <PauseIcon filled size={24} color={accent} />\n ) : (\n <PlayIcon filled size={24} color={accent} />\n )}\n </Pressable>\n </View>\n <View pointerEvents=\"box-none\" className=\"absolute inset-x-0 bottom-0\">\n <View\n pointerEvents=\"box-none\"\n className=\"h-11 flex-row items-center justify-between pl-4 pr-1\"\n >\n <Text\n accessible={false}\n className=\"text-xs font-medium leading-4 text-media-on-surface\"\n style={{ fontVariant: ['tabular-nums'] }}\n >\n {formatTime(position)} / {formatTime(duration)}\n </Text>\n {onToggleFullscreen && (\n <Pressable\n accessibilityRole=\"button\"\n accessibilityLabel={fullscreen ? 'Sair da tela cheia' : 'Tela cheia'}\n onPress={() => (fullscreen ? onToggleFullscreen() : setEntering(true))}\n className={button}\n >\n {fullscreen ? (\n <MinimizeIcon color={onSurface} />\n ) : (\n <MaximizeIcon color={onSurface} />\n )}\n </Pressable>\n )}\n </View>\n {/* 32px de altura para o dedo acertar a barra de 4px; sobe 8px sobre a folga do botão acima. */}\n <View {...seek.props} className=\"-mt-2 mx-4 h-8 justify-center\">\n {/* Filhos ignoram toques para que `locationX` seja sempre relativo à barra. */}\n <View pointerEvents=\"none\" className=\"h-1 w-full rounded-full bg-media-on-surface/30\">\n <View\n className=\"h-full rounded-full bg-media-accent\"\n style={{ width: `${progress}%` }}\n />\n <View\n className=\"absolute -top-1 size-3 rounded-full bg-media-accent\"\n style={{ left: `${progress}%`, marginLeft: -6 }}\n />\n </View>\n </View>\n </View>\n </View>\n </View>\n )\n}\n"],"mappings":"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"}
|