@innovui/react-native 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/README.md +1 -0
  2. package/dist/components/_shared/media-icons.js +125 -0
  3. package/dist/components/_shared/media-icons.js.map +1 -0
  4. package/dist/components/_shared/media.js +114 -0
  5. package/dist/components/_shared/media.js.map +1 -0
  6. package/dist/components/audio-player/audio-player.d.ts +34 -0
  7. package/dist/components/audio-player/audio-player.js +96 -0
  8. package/dist/components/audio-player/audio-player.js.map +1 -0
  9. package/dist/components/audio-player/index.d.ts +2 -0
  10. package/dist/components/carousel/carousel.d.ts +34 -0
  11. package/dist/components/carousel/carousel.js +110 -0
  12. package/dist/components/carousel/carousel.js.map +1 -0
  13. package/dist/components/carousel/index.d.ts +2 -0
  14. package/dist/components/gallery/gallery.d.ts +21 -0
  15. package/dist/components/gallery/gallery.js +54 -0
  16. package/dist/components/gallery/gallery.js.map +1 -0
  17. package/dist/components/gallery/index.d.ts +2 -0
  18. package/dist/components/lightbox/lightbox.d.ts +30 -0
  19. package/dist/components/lightbox/lightbox.js +147 -0
  20. package/dist/components/lightbox/lightbox.js.map +1 -0
  21. package/dist/components/video-player/index.d.ts +2 -0
  22. package/dist/components/video-player/video-player.d.ts +37 -0
  23. package/dist/components/video-player/video-player.js +178 -0
  24. package/dist/components/video-player/video-player.js.map +1 -0
  25. package/dist/index.d.ts +10 -1
  26. package/dist/index.js +6 -1
  27. package/dist/tokens/generated.d.ts +8 -0
  28. package/dist/tokens/generated.js +10 -2
  29. package/dist/tokens/generated.js.map +1 -1
  30. package/package.json +1 -1
  31. package/theme.css +8 -0
package/README.md CHANGED
@@ -39,6 +39,7 @@ export function App() {
39
39
  - **Sem `asChild`:** gatilhos de Modal, Drawer, Popover, Menu e Tooltip recebem o elemento clonado com `onPress` (o Tooltip abre no `onLongPress`).
40
40
  - **Overlays:** Modal, Drawer, Popover, Menu, Select e Combobox usam o `Modal` nativo do RN. Sem blur no backdrop. Envolva o app em `ToastProvider` para usar `useToast()`.
41
41
  - **Upload:** o RN não tem `<input type="file">`. O `Upload` é controlado: o consumidor abre o seletor (ex. `expo-document-picker`) em `onPick` e passa a lista em `files`.
42
+ - **AudioPlayer e VideoPlayer:** o React Native não tem player de mídia embutido, então são cascas controladas: você guarda o estado (por exemplo com `expo-audio` e `expo-video`) e o passa por props (`playing`, `position`, `duration`…), ligando `onPlayPause`, `onSeek` etc. ao player de verdade. A onda do AudioPlayer vem de `waveform` (ou da onda padrão): no native não há extração automática. Gallery, Lightbox e Carousel são completos.
42
43
  - **Grid e Table:** feitos com Views e flexbox (sem CSS grid). Breakpoints vêm de `useWindowDimensions`.
43
44
  - **Sombras no Android:** `shadow-*` não gera `elevation`.
44
45
 
@@ -0,0 +1,125 @@
1
+ import Svg, { Path, Rect } from "react-native-svg";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ //#region src/components/_shared/media-icons.tsx
4
+ const strokeProps = {
5
+ fill: "none",
6
+ strokeWidth: 2.4,
7
+ strokeLinecap: "round",
8
+ strokeLinejoin: "round"
9
+ };
10
+ const playPath = "M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z";
11
+ const speakerPath = "M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z";
12
+ function Icon({ color, size = 20, children, ...props }) {
13
+ return /* @__PURE__ */ jsx(Svg, {
14
+ width: size,
15
+ height: size,
16
+ viewBox: "0 0 24 24",
17
+ stroke: color,
18
+ ...strokeProps,
19
+ ...props,
20
+ children
21
+ });
22
+ }
23
+ /** Triângulo de reproduzir; `filled` preenche com `color` (botão central do vídeo). */
24
+ function PlayIcon({ filled, color, ...props }) {
25
+ return /* @__PURE__ */ jsx(Icon, {
26
+ color,
27
+ fill: filled ? color : "none",
28
+ ...props,
29
+ children: /* @__PURE__ */ jsx(Path, { d: playPath })
30
+ });
31
+ }
32
+ function PauseIcon(props) {
33
+ return /* @__PURE__ */ jsxs(Icon, {
34
+ ...props,
35
+ children: [/* @__PURE__ */ jsx(Rect, {
36
+ x: "14",
37
+ y: "3",
38
+ width: "5",
39
+ height: "18",
40
+ rx: "1"
41
+ }), /* @__PURE__ */ jsx(Rect, {
42
+ x: "5",
43
+ y: "3",
44
+ width: "5",
45
+ height: "18",
46
+ rx: "1"
47
+ })]
48
+ });
49
+ }
50
+ function VolumeIcon(props) {
51
+ return /* @__PURE__ */ jsxs(Icon, {
52
+ ...props,
53
+ children: [
54
+ /* @__PURE__ */ jsx(Path, { d: speakerPath }),
55
+ /* @__PURE__ */ jsx(Path, { d: "M16 9a5 5 0 0 1 0 6" }),
56
+ /* @__PURE__ */ jsx(Path, { d: "M19.364 18.364a9 9 0 0 0 0-12.728" })
57
+ ]
58
+ });
59
+ }
60
+ function VolumeMutedIcon(props) {
61
+ return /* @__PURE__ */ jsxs(Icon, {
62
+ ...props,
63
+ children: [
64
+ /* @__PURE__ */ jsx(Path, { d: speakerPath }),
65
+ /* @__PURE__ */ jsx(Path, { d: "M22 9l-6 6" }),
66
+ /* @__PURE__ */ jsx(Path, { d: "M16 9l6 6" })
67
+ ]
68
+ });
69
+ }
70
+ function MaximizeIcon(props) {
71
+ return /* @__PURE__ */ jsxs(Icon, {
72
+ ...props,
73
+ children: [
74
+ /* @__PURE__ */ jsx(Path, { d: "M8 3H5a2 2 0 0 0-2 2v3" }),
75
+ /* @__PURE__ */ jsx(Path, { d: "M21 8V5a2 2 0 0 0-2-2h-3" }),
76
+ /* @__PURE__ */ jsx(Path, { d: "M3 16v3a2 2 0 0 0 2 2h3" }),
77
+ /* @__PURE__ */ jsx(Path, { d: "M16 21h3a2 2 0 0 0 2-2v-3" })
78
+ ]
79
+ });
80
+ }
81
+ function MinimizeIcon(props) {
82
+ return /* @__PURE__ */ jsxs(Icon, {
83
+ ...props,
84
+ children: [
85
+ /* @__PURE__ */ jsx(Path, { d: "M8 3v3a2 2 0 0 1-2 2H3" }),
86
+ /* @__PURE__ */ jsx(Path, { d: "M21 8h-3a2 2 0 0 1-2-2V3" }),
87
+ /* @__PURE__ */ jsx(Path, { d: "M3 16h3a2 2 0 0 1 2 2v3" }),
88
+ /* @__PURE__ */ jsx(Path, { d: "M16 21v-3a2 2 0 0 1 2-2h3" })
89
+ ]
90
+ });
91
+ }
92
+ /** Quatro setas para fora: o ícone de ampliar sobre a miniatura. */
93
+ function ExpandIcon(props) {
94
+ return /* @__PURE__ */ jsxs(Icon, {
95
+ ...props,
96
+ children: [
97
+ /* @__PURE__ */ jsx(Path, { d: "m15 15 6 6" }),
98
+ /* @__PURE__ */ jsx(Path, { d: "m15 9 6-6" }),
99
+ /* @__PURE__ */ jsx(Path, { d: "M21 16v5h-5" }),
100
+ /* @__PURE__ */ jsx(Path, { d: "M21 8V3h-5" }),
101
+ /* @__PURE__ */ jsx(Path, { d: "M3 16v5h5" }),
102
+ /* @__PURE__ */ jsx(Path, { d: "m3 21 6-6" }),
103
+ /* @__PURE__ */ jsx(Path, { d: "M3 8V3h5" }),
104
+ /* @__PURE__ */ jsx(Path, { d: "M9 9 3 3" })
105
+ ]
106
+ });
107
+ }
108
+ /** ‹ de 20px (setas do carrossel e do lightbox). */
109
+ function PreviousIcon(props) {
110
+ return /* @__PURE__ */ jsx(Icon, {
111
+ ...props,
112
+ children: /* @__PURE__ */ jsx(Path, { d: "m15 18-6-6 6-6" })
113
+ });
114
+ }
115
+ /** › de 20px (setas do carrossel e do lightbox). */
116
+ function NextIcon(props) {
117
+ return /* @__PURE__ */ jsx(Icon, {
118
+ ...props,
119
+ children: /* @__PURE__ */ jsx(Path, { d: "m9 18 6-6-6-6" })
120
+ });
121
+ }
122
+ //#endregion
123
+ export { ExpandIcon, MaximizeIcon, MinimizeIcon, NextIcon, PauseIcon, PlayIcon, PreviousIcon, VolumeIcon, VolumeMutedIcon };
124
+
125
+ //# sourceMappingURL=media-icons.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-icons.js","names":[],"sources":["../../../src/components/_shared/media-icons.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport Svg, { Path, Rect, type SvgProps } from 'react-native-svg'\n\ntype IconProps = Omit<SvgProps, 'stroke' | 'fill' | 'width' | 'height'> & {\n color?: string\n /** Lado em px. Padrão: 20. */\n size?: number\n}\n\n// Ícones do Lucide (viewBox 24); com traço 2.4 saem com 2px aos 20px, como no Figma.\nconst strokeProps = {\n fill: 'none',\n strokeWidth: 2.4,\n strokeLinecap: 'round',\n strokeLinejoin: 'round',\n} as const\n\nconst playPath =\n 'M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z'\nconst speakerPath =\n 'M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z'\n\nfunction Icon({\n color,\n size = 20,\n children,\n ...props\n}: IconProps & { fill?: string; children: React.ReactNode }) {\n return (\n <Svg width={size} height={size} viewBox=\"0 0 24 24\" stroke={color} {...strokeProps} {...props}>\n {children}\n </Svg>\n )\n}\n\n/** Triângulo de reproduzir; `filled` preenche com `color` (botão central do vídeo). */\nexport function PlayIcon({ filled, color, ...props }: IconProps & { filled?: boolean }) {\n return (\n <Icon color={color} fill={filled ? color : 'none'} {...props}>\n <Path d={playPath} />\n </Icon>\n )\n}\n\nexport function PauseIcon(props: IconProps) {\n return (\n <Icon {...props}>\n <Rect x=\"14\" y=\"3\" width=\"5\" height=\"18\" rx=\"1\" />\n <Rect x=\"5\" y=\"3\" width=\"5\" height=\"18\" rx=\"1\" />\n </Icon>\n )\n}\n\nexport function VolumeIcon(props: IconProps) {\n return (\n <Icon {...props}>\n <Path d={speakerPath} />\n <Path d=\"M16 9a5 5 0 0 1 0 6\" />\n <Path d=\"M19.364 18.364a9 9 0 0 0 0-12.728\" />\n </Icon>\n )\n}\n\nexport function VolumeMutedIcon(props: IconProps) {\n return (\n <Icon {...props}>\n <Path d={speakerPath} />\n <Path d=\"M22 9l-6 6\" />\n <Path d=\"M16 9l6 6\" />\n </Icon>\n )\n}\n\nexport function MaximizeIcon(props: IconProps) {\n return (\n <Icon {...props}>\n <Path d=\"M8 3H5a2 2 0 0 0-2 2v3\" />\n <Path d=\"M21 8V5a2 2 0 0 0-2-2h-3\" />\n <Path d=\"M3 16v3a2 2 0 0 0 2 2h3\" />\n <Path d=\"M16 21h3a2 2 0 0 0 2-2v-3\" />\n </Icon>\n )\n}\n\nexport function MinimizeIcon(props: IconProps) {\n return (\n <Icon {...props}>\n <Path d=\"M8 3v3a2 2 0 0 1-2 2H3\" />\n <Path d=\"M21 8h-3a2 2 0 0 1-2-2V3\" />\n <Path d=\"M3 16h3a2 2 0 0 1 2 2v3\" />\n <Path d=\"M16 21v-3a2 2 0 0 1 2-2h3\" />\n </Icon>\n )\n}\n\n/** Quatro setas para fora: o ícone de ampliar sobre a miniatura. */\nexport function ExpandIcon(props: IconProps) {\n return (\n <Icon {...props}>\n <Path d=\"m15 15 6 6\" />\n <Path d=\"m15 9 6-6\" />\n <Path d=\"M21 16v5h-5\" />\n <Path d=\"M21 8V3h-5\" />\n <Path d=\"M3 16v5h5\" />\n <Path d=\"m3 21 6-6\" />\n <Path d=\"M3 8V3h5\" />\n <Path d=\"M9 9 3 3\" />\n </Icon>\n )\n}\n\n/** ‹ de 20px (setas do carrossel e do lightbox). */\nexport function PreviousIcon(props: IconProps) {\n return (\n <Icon {...props}>\n <Path d=\"m15 18-6-6 6-6\" />\n </Icon>\n )\n}\n\n/** › de 20px (setas do carrossel e do lightbox). */\nexport function NextIcon(props: IconProps) {\n return (\n <Icon {...props}>\n <Path d=\"m9 18 6-6-6-6\" />\n </Icon>\n )\n}\n"],"mappings":"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"}
@@ -0,0 +1,114 @@
1
+ import { useState } from "react";
2
+ //#region src/components/_shared/media.ts
3
+ /** `83` → `1:23`; com hora, `3725` → `1:02:05`. Valores inválidos viram `0:00`. */
4
+ function formatTime(seconds) {
5
+ const total = Number.isFinite(seconds) ? Math.max(0, Math.floor(seconds)) : 0;
6
+ const hours = Math.floor(total / 3600);
7
+ const minutes = Math.floor(total % 3600 / 60);
8
+ const secs = String(total % 60).padStart(2, "0");
9
+ return hours ? `${hours}:${String(minutes).padStart(2, "0")}:${secs}` : `${minutes}:${secs}`;
10
+ }
11
+ /** Alturas das barras do Figma (6–34px), como picos de 0 a 1. É a onda padrão do AudioPlayer. */
12
+ const DEFAULT_WAVEFORM = [
13
+ 6,
14
+ 18,
15
+ 34,
16
+ 20,
17
+ 12,
18
+ 22,
19
+ 16,
20
+ 26,
21
+ 20,
22
+ 14,
23
+ 28,
24
+ 18,
25
+ 34,
26
+ 22,
27
+ 16,
28
+ 24,
29
+ 20,
30
+ 14,
31
+ 18,
32
+ 26,
33
+ 16,
34
+ 30,
35
+ 22,
36
+ 12,
37
+ 34,
38
+ 18,
39
+ 26,
40
+ 16,
41
+ 10,
42
+ 22,
43
+ 14,
44
+ 20,
45
+ 24,
46
+ 16,
47
+ 28,
48
+ 12,
49
+ 34,
50
+ 18,
51
+ 26,
52
+ 16,
53
+ 10,
54
+ 20,
55
+ 14,
56
+ 18,
57
+ 24,
58
+ 30,
59
+ 22,
60
+ 28,
61
+ 34,
62
+ 16,
63
+ 24,
64
+ 18
65
+ ].map((height) => (height - 6) / 28);
66
+ /** Leva uma lista de picos para `bars` barras (pega o maior de cada trecho, ou repete se faltar). */
67
+ function resamplePeaks(peaks, bars) {
68
+ if (peaks.length === bars || peaks.length === 0) return peaks;
69
+ return Array.from({ length: bars }, (_, i) => {
70
+ const start = Math.floor(i * peaks.length / bars);
71
+ const end = Math.max(Math.floor((i + 1) * peaks.length / bars), start + 1);
72
+ return Math.max(...peaks.slice(start, end));
73
+ });
74
+ }
75
+ const SEEK_STEP = 5;
76
+ /**
77
+ * Props de uma barra de posição: tocar e arrastar posicionam, e o leitor de tela ajusta de 5 em
78
+ * 5 segundos (ações increment/decrement, como no Slider). Espalhe numa `View` cujos filhos têm
79
+ * `pointerEvents="none"`, para o `locationX` ser sempre relativo a ela.
80
+ */
81
+ function useSeek({ position, duration, label, onSeek }) {
82
+ const [width, setWidth] = useState(0);
83
+ const clamp = (value) => Math.min(Math.max(value, 0), duration);
84
+ const at = (x) => width ? clamp(x / width * duration) : 0;
85
+ return {
86
+ width,
87
+ props: {
88
+ onLayout: (event) => setWidth(event.nativeEvent.layout.width),
89
+ onStartShouldSetResponder: () => duration > 0,
90
+ onMoveShouldSetResponder: () => duration > 0,
91
+ onResponderTerminationRequest: () => false,
92
+ onResponderGrant: (event) => onSeek?.(at(event.nativeEvent.locationX)),
93
+ onResponderMove: (event) => onSeek?.(at(event.nativeEvent.locationX)),
94
+ accessible: true,
95
+ accessibilityRole: "adjustable",
96
+ accessibilityLabel: label,
97
+ accessibilityValue: {
98
+ min: 0,
99
+ max: Math.round(duration),
100
+ now: Math.round(position),
101
+ text: `${formatTime(position)} de ${formatTime(duration)}`
102
+ },
103
+ accessibilityActions: [{ name: "increment" }, { name: "decrement" }],
104
+ onAccessibilityAction: (event) => {
105
+ const direction = event.nativeEvent.actionName === "increment" ? 1 : -1;
106
+ onSeek?.(clamp(position + direction * SEEK_STEP));
107
+ }
108
+ }
109
+ };
110
+ }
111
+ //#endregion
112
+ export { DEFAULT_WAVEFORM, formatTime, resamplePeaks, useSeek };
113
+
114
+ //# sourceMappingURL=media.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media.js","names":[],"sources":["../../../src/components/_shared/media.ts"],"sourcesContent":["import { useState } from 'react'\nimport type {\n AccessibilityActionEvent,\n GestureResponderEvent,\n LayoutChangeEvent,\n} from 'react-native'\n\n/** `83` → `1:23`; com hora, `3725` → `1:02:05`. Valores inválidos viram `0:00`. */\nexport function formatTime(seconds: number) {\n const total = Number.isFinite(seconds) ? Math.max(0, Math.floor(seconds)) : 0\n const hours = Math.floor(total / 3600)\n const minutes = Math.floor((total % 3600) / 60)\n const secs = String(total % 60).padStart(2, '0')\n return hours ? `${hours}:${String(minutes).padStart(2, '0')}:${secs}` : `${minutes}:${secs}`\n}\n\n/** Alturas das barras do Figma (6–34px), como picos de 0 a 1. É a onda padrão do AudioPlayer. */\nexport const DEFAULT_WAVEFORM = [\n 6, 18, 34, 20, 12, 22, 16, 26, 20, 14, 28, 18, 34, 22, 16, 24, 20, 14, 18, 26, 16, 30, 22, 12, 34,\n 18, 26, 16, 10, 22, 14, 20, 24, 16, 28, 12, 34, 18, 26, 16, 10, 20, 14, 18, 24, 30, 22, 28, 34,\n 16, 24, 18,\n].map((height) => (height - 6) / 28)\n\n/** Leva uma lista de picos para `bars` barras (pega o maior de cada trecho, ou repete se faltar). */\nexport function resamplePeaks(peaks: number[], bars: number) {\n if (peaks.length === bars || peaks.length === 0) return peaks\n return Array.from({ length: bars }, (_, i) => {\n const start = Math.floor((i * peaks.length) / bars)\n const end = Math.max(Math.floor(((i + 1) * peaks.length) / bars), start + 1)\n return Math.max(...peaks.slice(start, end))\n })\n}\n\nconst SEEK_STEP = 5\n\n/**\n * Props de uma barra de posição: tocar e arrastar posicionam, e o leitor de tela ajusta de 5 em\n * 5 segundos (ações increment/decrement, como no Slider). Espalhe numa `View` cujos filhos têm\n * `pointerEvents=\"none\"`, para o `locationX` ser sempre relativo a ela.\n */\nexport function useSeek({\n position,\n duration,\n label,\n onSeek,\n}: {\n position: number\n duration: number\n label: string\n onSeek?: (seconds: number) => void\n}) {\n const [width, setWidth] = useState(0)\n const clamp = (value: number) => Math.min(Math.max(value, 0), duration)\n const at = (x: number) => (width ? clamp((x / width) * duration) : 0)\n\n return {\n width,\n props: {\n onLayout: (event: LayoutChangeEvent) => setWidth(event.nativeEvent.layout.width),\n onStartShouldSetResponder: () => duration > 0,\n onMoveShouldSetResponder: () => duration > 0,\n onResponderTerminationRequest: () => false,\n onResponderGrant: (event: GestureResponderEvent) => onSeek?.(at(event.nativeEvent.locationX)),\n onResponderMove: (event: GestureResponderEvent) => onSeek?.(at(event.nativeEvent.locationX)),\n accessible: true,\n accessibilityRole: 'adjustable' as const,\n accessibilityLabel: label,\n accessibilityValue: {\n min: 0,\n max: Math.round(duration),\n now: Math.round(position),\n text: `${formatTime(position)} de ${formatTime(duration)}`,\n },\n accessibilityActions: [{ name: 'increment' }, { name: 'decrement' }],\n onAccessibilityAction: (event: AccessibilityActionEvent) => {\n const direction = event.nativeEvent.actionName === 'increment' ? 1 : -1\n onSeek?.(clamp(position + direction * SEEK_STEP))\n },\n },\n }\n}\n"],"mappings":";;;AAQA,SAAgB,WAAW,SAAiB;CAC1C,MAAM,QAAQ,OAAO,SAAS,OAAO,IAAI,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,CAAC,IAAI;CAC5E,MAAM,QAAQ,KAAK,MAAM,QAAQ,IAAI;CACrC,MAAM,UAAU,KAAK,MAAO,QAAQ,OAAQ,EAAE;CAC9C,MAAM,OAAO,OAAO,QAAQ,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG;CAC/C,OAAO,QAAQ,GAAG,MAAM,GAAG,OAAO,OAAO,CAAC,CAAC,SAAS,GAAG,GAAG,EAAE,GAAG,SAAS,GAAG,QAAQ,GAAG;AACxF;;AAGA,MAAa,mBAAmB;CAC9B;CAAG;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAC/F;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;CAC5F;CAAI;CAAI;AACV,CAAC,CAAC,KAAK,YAAY,SAAS,KAAK,EAAE;;AAGnC,SAAgB,cAAc,OAAiB,MAAc;CAC3D,IAAI,MAAM,WAAW,QAAQ,MAAM,WAAW,GAAG,OAAO;CACxD,OAAO,MAAM,KAAK,EAAE,QAAQ,KAAK,IAAI,GAAG,MAAM;EAC5C,MAAM,QAAQ,KAAK,MAAO,IAAI,MAAM,SAAU,IAAI;EAClD,MAAM,MAAM,KAAK,IAAI,KAAK,OAAQ,IAAI,KAAK,MAAM,SAAU,IAAI,GAAG,QAAQ,CAAC;EAC3E,OAAO,KAAK,IAAI,GAAG,MAAM,MAAM,OAAO,GAAG,CAAC;CAC5C,CAAC;AACH;AAEA,MAAM,YAAY;;;;;;AAOlB,SAAgB,QAAQ,EACtB,UACA,UACA,OACA,UAMC;CACD,MAAM,CAAC,OAAO,YAAY,SAAS,CAAC;CACpC,MAAM,SAAS,UAAkB,KAAK,IAAI,KAAK,IAAI,OAAO,CAAC,GAAG,QAAQ;CACtE,MAAM,MAAM,MAAe,QAAQ,MAAO,IAAI,QAAS,QAAQ,IAAI;CAEnE,OAAO;EACL;EACA,OAAO;GACL,WAAW,UAA6B,SAAS,MAAM,YAAY,OAAO,KAAK;GAC/E,iCAAiC,WAAW;GAC5C,gCAAgC,WAAW;GAC3C,qCAAqC;GACrC,mBAAmB,UAAiC,SAAS,GAAG,MAAM,YAAY,SAAS,CAAC;GAC5F,kBAAkB,UAAiC,SAAS,GAAG,MAAM,YAAY,SAAS,CAAC;GAC3F,YAAY;GACZ,mBAAmB;GACnB,oBAAoB;GACpB,oBAAoB;IAClB,KAAK;IACL,KAAK,KAAK,MAAM,QAAQ;IACxB,KAAK,KAAK,MAAM,QAAQ;IACxB,MAAM,GAAG,WAAW,QAAQ,EAAE,MAAM,WAAW,QAAQ;GACzD;GACA,sBAAsB,CAAC,EAAE,MAAM,YAAY,GAAG,EAAE,MAAM,YAAY,CAAC;GACnE,wBAAwB,UAAoC;IAC1D,MAAM,YAAY,MAAM,YAAY,eAAe,cAAc,IAAI;IACrE,SAAS,MAAM,WAAW,YAAY,SAAS,CAAC;GAClD;EACF;CACF;AACF"}
@@ -0,0 +1,34 @@
1
+ import { ViewProps } from "react-native";
2
+ //#region src/components/audio-player/audio-player.d.ts
3
+ export type AudioPlayerProps = Omit<ViewProps, 'children' | 'style'> & {
4
+ className?: string;
5
+ /** Nome acessível do player. */
6
+ label?: string;
7
+ /** Tocando agora: mostra o botão de pausar. */
8
+ playing?: boolean;
9
+ /** Posição atual, em segundos. */
10
+ position?: number;
11
+ /** Duração total, em segundos. */
12
+ duration?: number;
13
+ /** Velocidade atual (controlado). Sem ela, o player guarda a escolhida. */
14
+ rate?: number;
15
+ /** Velocidades do botão (cada toque passa para a próxima). Padrão: 1x, 1.5x e 2x. */
16
+ speeds?: number[];
17
+ /** Picos da onda, de 0 a 1. Sem eles, usa uma onda padrão. */
18
+ waveform?: number[];
19
+ /** Quantidade de barras da onda. Padrão: 52. */
20
+ bars?: number;
21
+ onPlayPause?: () => void;
22
+ /** Chamado com a posição (em segundos) tocada ou arrastada na onda. */
23
+ onSeek?: (seconds: number) => void;
24
+ /** Chamado com a nova velocidade quando o botão é tocado. */
25
+ onRateChange?: (rate: number) => void;
26
+ };
27
+ /**
28
+ * Casca de player de áudio: botão, onda, tempo e velocidade. Não toca nada: quem usa guarda o
29
+ * estado (por exemplo com `expo-audio`) e o passa por props, ligando `onPlayPause`, `onSeek` e
30
+ * `onRateChange` ao player de verdade.
31
+ */
32
+ export declare function AudioPlayer({ className, label, playing, position, duration, rate, speeds, waveform, bars, onPlayPause, onSeek, onRateChange, onLayout, ...props }: AudioPlayerProps): import("react").JSX.Element;
33
+ //#endregion
34
+ //# sourceMappingURL=audio-player.d.ts.map
@@ -0,0 +1,96 @@
1
+ import { cn } from "../../utils/cn.js";
2
+ import { useToken } from "../_shared/use-token.js";
3
+ import { DEFAULT_WAVEFORM, formatTime, resamplePeaks, useSeek } from "../_shared/media.js";
4
+ import { PauseIcon, PlayIcon } from "../_shared/media-icons.js";
5
+ import { useState } from "react";
6
+ import { Pressable, Text, View } from "react-native";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ //#region src/components/audio-player/audio-player.tsx
9
+ const BAR_MIN = 6;
10
+ const BAR_RANGE = 28;
11
+ /**
12
+ * Casca de player de áudio: botão, onda, tempo e velocidade. Não toca nada: quem usa guarda o
13
+ * estado (por exemplo com `expo-audio`) e o passa por props, ligando `onPlayPause`, `onSeek` e
14
+ * `onRateChange` ao player de verdade.
15
+ */
16
+ function AudioPlayer({ className, label = "Áudio", playing = false, position = 0, duration = 0, rate, speeds = [
17
+ 1,
18
+ 1.5,
19
+ 2
20
+ ], waveform, bars = 52, onPlayPause, onSeek, onRateChange, onLayout, ...props }) {
21
+ const [chosen, setChosen] = useState(speeds[0] ?? 1);
22
+ const [cardWidth, setCardWidth] = useState(0);
23
+ const stacked = cardWidth > 0 && cardWidth < 416;
24
+ const speed = rate ?? chosen;
25
+ const iconColor = useToken("--color-media-on-surface");
26
+ const seek = useSeek({
27
+ position,
28
+ duration,
29
+ label: "Posição do áudio",
30
+ onSeek
31
+ });
32
+ const shown = seek.width ? Math.min(bars, Math.max(1, Math.floor((seek.width + 2) / 4))) : bars;
33
+ const peaks = resamplePeaks(waveform ?? DEFAULT_WAVEFORM, shown);
34
+ const played = duration ? Math.round(position / duration * shown) : 0;
35
+ function nextSpeed() {
36
+ const next = speeds[(speeds.indexOf(speed) + 1) % speeds.length] ?? 1;
37
+ setChosen(next);
38
+ onRateChange?.(next);
39
+ }
40
+ return /* @__PURE__ */ jsxs(View, {
41
+ accessibilityLabel: label,
42
+ onLayout: (event) => {
43
+ setCardWidth(event.nativeEvent.layout.width);
44
+ onLayout?.(event);
45
+ },
46
+ className: cn("w-full max-w-[480px] flex-row items-center gap-3 rounded-lg border border-border-default bg-surface-elevated p-4 shadow-sm", className),
47
+ ...props,
48
+ children: [
49
+ /* @__PURE__ */ jsx(Pressable, {
50
+ accessibilityRole: "button",
51
+ accessibilityLabel: playing ? "Pausar" : "Reproduzir",
52
+ onPress: onPlayPause,
53
+ className: "size-11 shrink-0 items-center justify-center rounded-full bg-button-primary-bg active:bg-button-primary-bg-active",
54
+ children: playing ? /* @__PURE__ */ jsx(PauseIcon, { color: iconColor }) : /* @__PURE__ */ jsx(PlayIcon, { color: iconColor })
55
+ }),
56
+ /* @__PURE__ */ jsxs(View, {
57
+ className: cn("min-w-0 flex-1", stacked ? "flex-col items-stretch gap-1" : "flex-row items-center gap-2"),
58
+ children: [/* @__PURE__ */ jsx(View, {
59
+ ...seek.props,
60
+ className: cn("h-[34px] min-w-0 justify-center", stacked ? "w-full" : "flex-1"),
61
+ children: /* @__PURE__ */ jsx(View, {
62
+ pointerEvents: "none",
63
+ className: "h-full flex-row items-center gap-0.5",
64
+ children: peaks.map((peak, i) => /* @__PURE__ */ jsx(View, {
65
+ className: cn("min-w-0.5 flex-1 rounded-full", i < played ? "bg-button-primary-bg" : "bg-media-wave-rest"),
66
+ style: { height: BAR_MIN + peak * BAR_RANGE }
67
+ }, i))
68
+ })
69
+ }), /* @__PURE__ */ jsxs(Text, {
70
+ accessible: false,
71
+ className: "shrink-0 text-xs font-medium leading-4 text-text-secondary",
72
+ style: { fontVariant: ["tabular-nums"] },
73
+ children: [
74
+ formatTime(position),
75
+ " / ",
76
+ formatTime(duration)
77
+ ]
78
+ })]
79
+ }),
80
+ /* @__PURE__ */ jsx(Pressable, {
81
+ accessibilityRole: "button",
82
+ accessibilityLabel: `Velocidade de reprodução: ${speed}x`,
83
+ onPress: nextSpeed,
84
+ className: "h-8 w-16 shrink-0 items-center justify-center rounded-full border border-border-default bg-surface-elevated active:bg-bg-tertiary",
85
+ children: /* @__PURE__ */ jsxs(Text, {
86
+ className: "text-sm font-medium leading-5 text-text-secondary",
87
+ children: [speed, "x"]
88
+ })
89
+ })
90
+ ]
91
+ });
92
+ }
93
+ //#endregion
94
+ export { AudioPlayer };
95
+
96
+ //# sourceMappingURL=audio-player.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio-player.js","names":[],"sources":["../../../src/components/audio-player/audio-player.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { useState } from 'react'\nimport { Pressable, Text, View, type ViewProps } from 'react-native'\nimport { cn } from '../../utils/cn'\nimport { DEFAULT_WAVEFORM, formatTime, resamplePeaks, useSeek } from '../_shared/media'\nimport { PauseIcon, PlayIcon } from '../_shared/media-icons'\nimport { useToken } from '../_shared/use-token'\n\n// Alturas das barras no Figma: de 6px (silêncio) a 34px (pico).\nconst BAR_MIN = 6\nconst BAR_RANGE = 28\n\nexport type AudioPlayerProps = Omit<ViewProps, 'children' | 'style'> & {\n className?: string\n /** Nome acessível do player. */\n label?: string\n /** Tocando agora: mostra o botão de pausar. */\n playing?: boolean\n /** Posição atual, em segundos. */\n position?: number\n /** Duração total, em segundos. */\n duration?: number\n /** Velocidade atual (controlado). Sem ela, o player guarda a escolhida. */\n rate?: number\n /** Velocidades do botão (cada toque passa para a próxima). Padrão: 1x, 1.5x e 2x. */\n speeds?: number[]\n /** Picos da onda, de 0 a 1. Sem eles, usa uma onda padrão. */\n waveform?: number[]\n /** Quantidade de barras da onda. Padrão: 52. */\n bars?: number\n onPlayPause?: () => void\n /** Chamado com a posição (em segundos) tocada ou arrastada na onda. */\n onSeek?: (seconds: number) => void\n /** Chamado com a nova velocidade quando o botão é tocado. */\n onRateChange?: (rate: number) => void\n}\n\n/**\n * Casca de player de áudio: botão, onda, tempo e velocidade. Não toca nada: quem usa guarda o\n * estado (por exemplo com `expo-audio`) e o passa por props, ligando `onPlayPause`, `onSeek` e\n * `onRateChange` ao player de verdade.\n */\nexport function AudioPlayer({\n className,\n label = 'Áudio',\n playing = false,\n position = 0,\n duration = 0,\n rate,\n speeds = [1, 1.5, 2],\n waveform,\n bars = 52,\n onPlayPause,\n onSeek,\n onRateChange,\n 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-lg border border-border-default bg-surface-elevated p-4 shadow-sm',\n className,\n )}\n {...props}\n >\n <Pressable\n accessibilityRole=\"button\"\n accessibilityLabel={playing ? 'Pausar' : 'Reproduzir'}\n onPress={onPlayPause}\n className=\"size-11 shrink-0 items-center justify-center rounded-full bg-button-primary-bg active:bg-button-primary-bg-active\"\n >\n {playing ? <PauseIcon color={iconColor} /> : <PlayIcon color={iconColor} />}\n </Pressable>\n <View\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 pointerEvents=\"none\" className=\"h-full flex-row items-center gap-0.5\">\n {peaks.map((peak, i) => (\n <View\n // biome-ignore lint/suspicious/noArrayIndexKey: as barras são posicionais e fixas\n key={i}\n className={cn(\n 'min-w-0.5 flex-1 rounded-full',\n i < played ? 'bg-button-primary-bg' : 'bg-media-wave-rest',\n )}\n style={{ height: BAR_MIN + peak * BAR_RANGE }}\n />\n ))}\n </View>\n </View>\n <Text\n accessible={false}\n className=\"shrink-0 text-xs font-medium leading-4 text-text-secondary\"\n style={{ fontVariant: ['tabular-nums'] }}\n >\n {formatTime(position)} / {formatTime(duration)}\n </Text>\n </View>\n <Pressable\n accessibilityRole=\"button\"\n accessibilityLabel={`Velocidade de reprodução: ${speed}x`}\n onPress={nextSpeed}\n className=\"h-8 w-16 shrink-0 items-center justify-center rounded-full border border-border-default bg-surface-elevated active:bg-bg-tertiary\"\n >\n <Text className=\"text-sm font-medium leading-5 text-text-secondary\">{speed}x</Text>\n </Pressable>\n </View>\n )\n}\n"],"mappings":";;;;;;;;AAUA,MAAM,UAAU;AAChB,MAAM,YAAY;;;;;;AAgClB,SAAgB,YAAY,EAC1B,WACA,QAAQ,SACR,UAAU,OACV,WAAW,GACX,WAAW,GACX,MACA,SAAS;CAAC;CAAG;CAAK;AAAC,GACnB,UACA,OAAO,IACP,aACA,QACA,cACA,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,8HACA,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;MAAM,eAAc;MAAO,WAAU;MAClC,UAAA,MAAM,KAAK,MAAM,MAChB,oBAAC,MAAD;OAGE,WAAW,GACT,iCACA,IAAI,SAAS,yBAAyB,oBACxC;OACA,OAAO,EAAE,QAAQ,UAAU,OAAO,UAAU;MAC7C,GANM,CAMN,CACF;KACG,CAAA;IACF,CAAA,GACN,qBAAC,MAAD;KACE,YAAY;KACZ,WAAU;KACV,OAAO,EAAE,aAAa,CAAC,cAAc,EAAE;KAHzC,UAAA;MAKG,WAAW,QAAQ;MAAE;MAAI,WAAW,QAAQ;KACzC;IACF,CAAA,CAAA;;GACN,oBAAC,WAAD;IACE,mBAAkB;IAClB,oBAAoB,6BAA6B,MAAM;IACvD,SAAS;IACT,WAAU;IAEV,UAAA,qBAAC,MAAD;KAAM,WAAU;KAAhB,UAAA,CAAqE,OAAM,GAAO;;GACzE,CAAA;EACP;;AAEV"}
@@ -0,0 +1,2 @@
1
+ import { AudioPlayer, AudioPlayerProps } from "./audio-player.js";
2
+ export { AudioPlayer, type AudioPlayerProps };
@@ -0,0 +1,34 @@
1
+ import { ReactNode } from "react";
2
+ import { ViewProps } from "react-native";
3
+ //#region src/components/carousel/carousel.d.ts
4
+ export type CarouselProps = Omit<ViewProps, 'children' | 'style'> & {
5
+ className?: string;
6
+ /** Nome acessível do carrossel (ex.: "Modelos de mensagem"). */
7
+ label: string;
8
+ /** Os itens: use `CarouselItem`. */
9
+ children: ReactNode;
10
+ /** Mostra os pontos de posição. Padrão: `true`. */
11
+ showDots?: boolean;
12
+ /** Mostra as setas anterior/próximo. Padrão: `true`. */
13
+ showArrows?: boolean;
14
+ /** Largura de cada item, de 0 a 1 da largura do carrossel. Padrão: 0.78 no celular e 0.52 a partir de 768px de tela. */
15
+ itemRatio?: number;
16
+ /** Chamado com o índice do item em destaque sempre que ele muda. */
17
+ onIndexChange?: (index: number) => void;
18
+ previousLabel?: string;
19
+ nextLabel?: string;
20
+ /** Monta o nome acessível de cada ponto. Padrão: "Ir para o item N". */
21
+ dotLabel?: (index: number) => string;
22
+ };
23
+ /**
24
+ * Fileira de itens que desliza com o dedo e encaixa item a item, com pontos e setas. Mostra um
25
+ * pedaço do próximo item. O último item "chega ao fim" mesmo que não caiba alinhado à esquerda:
26
+ * o ponto e a seta refletem isso.
27
+ */
28
+ export declare function Carousel({ className, label, children, showDots, showArrows, itemRatio, onIndexChange, previousLabel, nextLabel, dotLabel, ...props }: CarouselProps): import("react").JSX.Element;
29
+ /** Um item do `Carousel`. A largura vem do carrossel (`itemRatio`); o item preenche toda a altura. */
30
+ export declare function CarouselItem({ className, ...props }: Omit<ViewProps, 'style'> & {
31
+ className?: string;
32
+ }): import("react").JSX.Element;
33
+ //#endregion
34
+ //# sourceMappingURL=carousel.d.ts.map
@@ -0,0 +1,110 @@
1
+ import { cn } from "../../utils/cn.js";
2
+ import { useToken } from "../_shared/use-token.js";
3
+ import { IconButton } from "../icon-button/icon-button.js";
4
+ import { NextIcon, PreviousIcon } from "../_shared/media-icons.js";
5
+ import { Children, useRef, useState } from "react";
6
+ import { Pressable, ScrollView, View, useWindowDimensions } from "react-native";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ //#region src/components/carousel/carousel.tsx
9
+ const GAP = 16;
10
+ const TABLET = 768;
11
+ /**
12
+ * Fileira de itens que desliza com o dedo e encaixa item a item, com pontos e setas. Mostra um
13
+ * pedaço do próximo item. O último item "chega ao fim" mesmo que não caiba alinhado à esquerda:
14
+ * o ponto e a seta refletem isso.
15
+ */
16
+ function Carousel({ className, label, children, showDots = true, showArrows = true, itemRatio, onIndexChange, previousLabel = "Item anterior", nextLabel = "Próximo item", dotLabel = (index) => `Ir para o item ${index + 1}`, ...props }) {
17
+ const items = Children.toArray(children);
18
+ const count = items.length;
19
+ const scroll = useRef(null);
20
+ const { width: screen } = useWindowDimensions();
21
+ const [width, setWidth] = useState(0);
22
+ const [active, setActive] = useState(0);
23
+ const arrowColor = useToken("--color-text-primary");
24
+ const arrowDisabled = useToken("--color-button-disabled-text");
25
+ const itemWidth = width * (itemRatio ?? (screen >= TABLET ? .52 : .78));
26
+ const step = itemWidth + GAP;
27
+ const max = Math.max(count * step - GAP - width, 0);
28
+ const offsets = Array.from({ length: count }, (_, i) => Math.min(i * step, max));
29
+ function indexAt(x) {
30
+ if (max > 0 && x >= max - 1) return count - 1;
31
+ let best = 0;
32
+ offsets.forEach((offset, i) => {
33
+ if (Math.abs(offset - x) < Math.abs((offsets[best] ?? 0) - x)) best = i;
34
+ });
35
+ return best;
36
+ }
37
+ function onScroll(event) {
38
+ const next = indexAt(event.nativeEvent.contentOffset.x);
39
+ if (next === active) return;
40
+ setActive(next);
41
+ onIndexChange?.(next);
42
+ }
43
+ function goTo(index) {
44
+ const target = Math.min(Math.max(index, 0), count - 1);
45
+ scroll.current?.scrollTo({
46
+ x: offsets[target] ?? 0,
47
+ animated: true
48
+ });
49
+ }
50
+ return /* @__PURE__ */ jsxs(View, {
51
+ className: cn("gap-6", className),
52
+ ...props,
53
+ children: [/* @__PURE__ */ jsx(ScrollView, {
54
+ ref: scroll,
55
+ horizontal: true,
56
+ accessibilityLabel: label,
57
+ showsHorizontalScrollIndicator: false,
58
+ decelerationRate: "fast",
59
+ snapToOffsets: offsets,
60
+ scrollEventThrottle: 16,
61
+ onLayout: (event) => setWidth(event.nativeEvent.layout.width),
62
+ onScroll,
63
+ contentContainerClassName: "gap-4",
64
+ children: items.map((child, i) => /* @__PURE__ */ jsx(View, {
65
+ accessibilityLabel: `${i + 1} de ${count}`,
66
+ style: { width: itemWidth },
67
+ children: child
68
+ }, i))
69
+ }), (showDots || showArrows) && count > 0 && /* @__PURE__ */ jsxs(View, {
70
+ className: "flex-row items-center justify-between",
71
+ children: [showDots ? /* @__PURE__ */ jsx(View, {
72
+ className: "flex-row items-center gap-2",
73
+ children: items.map((_, i) => /* @__PURE__ */ jsx(Pressable, {
74
+ accessibilityRole: "button",
75
+ accessibilityLabel: dotLabel(i),
76
+ accessibilityState: { selected: i === active },
77
+ onPress: () => goTo(i),
78
+ className: "h-11 items-center justify-center",
79
+ children: /* @__PURE__ */ jsx(View, { className: cn("h-2 rounded-full", i === active ? "w-6 bg-interactive-primary" : "w-2 bg-border-strong") })
80
+ }, i))
81
+ }) : /* @__PURE__ */ jsx(View, {}), showArrows && /* @__PURE__ */ jsxs(View, {
82
+ className: "flex-row gap-2",
83
+ children: [/* @__PURE__ */ jsx(IconButton, {
84
+ accessibilityLabel: previousLabel,
85
+ variant: "tertiary",
86
+ disabled: active <= 0,
87
+ onPress: () => goTo(active - 1),
88
+ children: /* @__PURE__ */ jsx(PreviousIcon, { color: active <= 0 ? arrowDisabled : arrowColor })
89
+ }), /* @__PURE__ */ jsx(IconButton, {
90
+ accessibilityLabel: nextLabel,
91
+ variant: "tertiary",
92
+ disabled: active >= count - 1,
93
+ onPress: () => goTo(active + 1),
94
+ children: /* @__PURE__ */ jsx(NextIcon, { color: active >= count - 1 ? arrowDisabled : arrowColor })
95
+ })]
96
+ })]
97
+ })]
98
+ });
99
+ }
100
+ /** Um item do `Carousel`. A largura vem do carrossel (`itemRatio`); o item preenche toda a altura. */
101
+ function CarouselItem({ className, ...props }) {
102
+ return /* @__PURE__ */ jsx(View, {
103
+ className: cn("flex-1", className),
104
+ ...props
105
+ });
106
+ }
107
+ //#endregion
108
+ export { Carousel, CarouselItem };
109
+
110
+ //# sourceMappingURL=carousel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"carousel.js","names":[],"sources":["../../../src/components/carousel/carousel.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { Children, type ReactNode, useRef, useState } from 'react'\nimport {\n type LayoutChangeEvent,\n type NativeScrollEvent,\n type NativeSyntheticEvent,\n Pressable,\n ScrollView,\n useWindowDimensions,\n View,\n type ViewProps,\n} from 'react-native'\nimport { cn } from '../../utils/cn'\nimport { NextIcon, PreviousIcon } from '../_shared/media-icons'\nimport { useToken } from '../_shared/use-token'\nimport { IconButton } from '../icon-button'\n\nconst GAP = 16\nconst TABLET = 768\n\nexport type CarouselProps = Omit<ViewProps, 'children' | 'style'> & {\n className?: string\n /** Nome acessível do carrossel (ex.: \"Modelos de mensagem\"). */\n label: string\n /** Os itens: use `CarouselItem`. */\n children: ReactNode\n /** Mostra os pontos de posição. Padrão: `true`. */\n showDots?: boolean\n /** Mostra as setas anterior/próximo. Padrão: `true`. */\n showArrows?: boolean\n /** Largura de cada item, de 0 a 1 da largura do carrossel. Padrão: 0.78 no celular e 0.52 a partir de 768px de tela. */\n itemRatio?: number\n /** Chamado com o índice do item em destaque sempre que ele muda. */\n onIndexChange?: (index: number) => void\n previousLabel?: string\n nextLabel?: string\n /** Monta o nome acessível de cada ponto. Padrão: \"Ir para o item N\". */\n dotLabel?: (index: number) => string\n}\n\n/**\n * Fileira de itens que desliza com o dedo e encaixa item a item, com pontos e setas. Mostra um\n * pedaço do próximo item. O último item \"chega ao fim\" mesmo que não caiba alinhado à esquerda:\n * o ponto e a seta refletem isso.\n */\nexport function Carousel({\n className,\n label,\n children,\n showDots = true,\n showArrows = true,\n itemRatio,\n onIndexChange,\n previousLabel = 'Item anterior',\n nextLabel = 'Próximo item',\n dotLabel = (index) => `Ir para o item ${index + 1}`,\n ...props\n}: CarouselProps) {\n const items = Children.toArray(children)\n const count = items.length\n const scroll = useRef<ScrollView>(null)\n const { width: screen } = useWindowDimensions()\n const [width, setWidth] = useState(0)\n const [active, setActive] = useState(0)\n const arrowColor = useToken('--color-text-primary')\n const arrowDisabled = useToken('--color-button-disabled-text')\n\n const itemWidth = width * (itemRatio ?? (screen >= TABLET ? 0.52 : 0.78))\n const step = itemWidth + GAP\n const max = Math.max(count * step - GAP - width, 0)\n // o último ponto de encaixe é o fim da rolagem, mesmo que o último item não chegue à esquerda\n const offsets = Array.from({ length: count }, (_, i) => Math.min(i * step, max))\n\n function indexAt(x: number) {\n if (max > 0 && x >= max - 1) return count - 1\n let best = 0\n offsets.forEach((offset, i) => {\n if (Math.abs(offset - x) < Math.abs((offsets[best] ?? 0) - x)) best = i\n })\n return best\n }\n\n function onScroll(event: NativeSyntheticEvent<NativeScrollEvent>) {\n const next = indexAt(event.nativeEvent.contentOffset.x)\n if (next === active) return\n setActive(next)\n onIndexChange?.(next)\n }\n\n function goTo(index: number) {\n const target = Math.min(Math.max(index, 0), count - 1)\n scroll.current?.scrollTo({ x: offsets[target] ?? 0, animated: true })\n }\n\n return (\n <View className={cn('gap-6', className)} {...props}>\n <ScrollView\n ref={scroll}\n horizontal\n accessibilityLabel={label}\n showsHorizontalScrollIndicator={false}\n decelerationRate=\"fast\"\n snapToOffsets={offsets}\n scrollEventThrottle={16}\n onLayout={(event: LayoutChangeEvent) => setWidth(event.nativeEvent.layout.width)}\n onScroll={onScroll}\n contentContainerClassName=\"gap-4\"\n >\n {items.map((child, i) => (\n <View\n // biome-ignore lint/suspicious/noArrayIndexKey: os itens são posicionais\n key={i}\n accessibilityLabel={`${i + 1} de ${count}`}\n style={{ width: itemWidth }}\n >\n {child}\n </View>\n ))}\n </ScrollView>\n {(showDots || showArrows) && count > 0 && (\n <View className=\"flex-row items-center justify-between\">\n {showDots ? (\n <View className=\"flex-row items-center gap-2\">\n {items.map((_, i) => (\n <Pressable\n // biome-ignore lint/suspicious/noArrayIndexKey: os pontos são posicionais\n key={i}\n accessibilityRole=\"button\"\n accessibilityLabel={dotLabel(i)}\n accessibilityState={{ selected: i === active }}\n onPress={() => goTo(i)}\n className=\"h-11 items-center justify-center\"\n >\n <View\n className={cn(\n 'h-2 rounded-full',\n i === active ? 'w-6 bg-interactive-primary' : 'w-2 bg-border-strong',\n )}\n />\n </Pressable>\n ))}\n </View>\n ) : (\n <View />\n )}\n {showArrows && (\n <View className=\"flex-row gap-2\">\n <IconButton\n accessibilityLabel={previousLabel}\n variant=\"tertiary\"\n disabled={active <= 0}\n onPress={() => goTo(active - 1)}\n >\n <PreviousIcon color={active <= 0 ? arrowDisabled : arrowColor} />\n </IconButton>\n <IconButton\n accessibilityLabel={nextLabel}\n variant=\"tertiary\"\n disabled={active >= count - 1}\n onPress={() => goTo(active + 1)}\n >\n <NextIcon color={active >= count - 1 ? arrowDisabled : arrowColor} />\n </IconButton>\n </View>\n )}\n </View>\n )}\n </View>\n )\n}\n\n/** Um item do `Carousel`. A largura vem do carrossel (`itemRatio`); o item preenche toda a altura. */\nexport function CarouselItem({\n className,\n ...props\n}: Omit<ViewProps, 'style'> & { className?: string }) {\n return <View className={cn('flex-1', className)} {...props} />\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { Carousel, CarouselItem, CarouselProps } from "./carousel.js";
2
+ export { Carousel, CarouselItem, type CarouselProps };
@@ -0,0 +1,21 @@
1
+ import { LightboxItem } from "../lightbox/lightbox.js";
2
+ import { ViewProps } from "react-native";
3
+ //#region src/components/gallery/gallery.d.ts
4
+ export type GalleryItem = LightboxItem & {
5
+ /** Imagem menor para a grade; sem ela, a grade usa `src`. */
6
+ thumbSrc?: string;
7
+ };
8
+ export type GalleryProps = Omit<ViewProps, 'children' | 'style'> & {
9
+ className?: string;
10
+ items: GalleryItem[];
11
+ /** Colunas da grade. Padrão: 2 no celular e 3 a partir de 768px de tela. */
12
+ columns?: number;
13
+ /** Nome acessível do botão de cada miniatura. Padrão: "Ampliar foto N: descrição". */
14
+ openLabel?: (item: GalleryItem, index: number) => string;
15
+ /** Nome acessível do visualizador ampliado. */
16
+ lightboxLabel?: string;
17
+ };
18
+ /** Grade de miniaturas que abre a foto ampliada no `Lightbox`. */
19
+ export declare function Gallery({ className, items, columns, openLabel, lightboxLabel, ...props }: GalleryProps): import("react").JSX.Element;
20
+ //#endregion
21
+ //# sourceMappingURL=gallery.d.ts.map