orn-ui 0.2.7 → 0.2.8
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/AGENTS.md +21 -3
- package/README.md +7 -6
- package/lib/commonjs/atoms/Gradient.js +111 -0
- package/lib/commonjs/atoms/Gradient.js.map +1 -0
- package/lib/commonjs/atoms/index.js +7 -0
- package/lib/commonjs/atoms/index.js.map +1 -1
- package/lib/commonjs/organisms/Slides.js +406 -0
- package/lib/commonjs/organisms/Slides.js.map +1 -0
- package/lib/commonjs/organisms/index.js +7 -0
- package/lib/commonjs/organisms/index.js.map +1 -1
- package/lib/commonjs/organisms/useSlides.js +186 -0
- package/lib/commonjs/organisms/useSlides.js.map +1 -0
- package/lib/module/atoms/Gradient.js +106 -0
- package/lib/module/atoms/Gradient.js.map +1 -0
- package/lib/module/atoms/index.js +1 -0
- package/lib/module/atoms/index.js.map +1 -1
- package/lib/module/organisms/Slides.js +401 -0
- package/lib/module/organisms/Slides.js.map +1 -0
- package/lib/module/organisms/index.js +1 -0
- package/lib/module/organisms/index.js.map +1 -1
- package/lib/module/organisms/useSlides.js +182 -0
- package/lib/module/organisms/useSlides.js.map +1 -0
- package/lib/typescript/atoms/Gradient.d.ts +37 -0
- package/lib/typescript/atoms/Gradient.d.ts.map +1 -0
- package/lib/typescript/atoms/index.d.ts +2 -0
- package/lib/typescript/atoms/index.d.ts.map +1 -1
- package/lib/typescript/organisms/Slides.d.ts +69 -0
- package/lib/typescript/organisms/Slides.d.ts.map +1 -0
- package/lib/typescript/organisms/index.d.ts +2 -0
- package/lib/typescript/organisms/index.d.ts.map +1 -1
- package/lib/typescript/organisms/useSlides.d.ts +52 -0
- package/lib/typescript/organisms/useSlides.d.ts.map +1 -0
- package/llms.txt +6 -5
- package/package.json +12 -2
- package/registry/gradient.json +18 -0
- package/registry/index.json +26 -0
- package/registry/manifest.json +14 -0
- package/registry/slides.json +24 -0
- package/src/atoms/Gradient.tsx +99 -0
- package/src/atoms/index.ts +2 -0
- package/src/organisms/Slides.tsx +413 -0
- package/src/organisms/index.ts +8 -0
- package/src/organisms/useSlides.ts +220 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Gradient.d.ts","sourceRoot":"","sources":["../../../src/atoms/Gradient.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAwB,MAAM,OAAO,CAAC;AAC7C,OAAO,EAAoB,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAEhF,MAAM,MAAM,iBAAiB,GAAG,UAAU,GAAG,YAAY,GAAG,UAAU,GAAG,kBAAkB,CAAC;AAE5F,MAAM,WAAW,aAAa;IAC5B,0EAA0E;IAC1E,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,iBAAiB,CAAC;IAC9B;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAaD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,QAAQ,oFACkE,aAAa,uBA+CnG,CAAC"}
|
|
@@ -14,6 +14,8 @@ export { Card } from './Card';
|
|
|
14
14
|
export type { CardProps } from './Card';
|
|
15
15
|
export { Divider } from './Divider';
|
|
16
16
|
export type { DividerProps } from './Divider';
|
|
17
|
+
export { Gradient } from './Gradient';
|
|
18
|
+
export type { GradientProps, GradientDirection } from './Gradient';
|
|
17
19
|
export { Avatar } from './Avatar';
|
|
18
20
|
export type { AvatarProps } from './Avatar';
|
|
19
21
|
export { Image, prefetchImage } from './Image';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/atoms/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACxD,YAAY,EAAE,eAAe,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACvE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,YAAY,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACxD,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9B,YAAY,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,YAAY,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC/C,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC9D,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AACjE,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC3D,YAAY,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACtE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,YAAY,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAC5B,YAAY,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACtC,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,YAAY,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/atoms/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACxD,YAAY,EAAE,eAAe,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACvE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,YAAY,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACxD,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9B,YAAY,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,YAAY,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACnE,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,YAAY,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC/C,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC9D,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AACjE,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC3D,YAAY,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACtE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,YAAY,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAC5B,YAAY,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACtC,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,YAAY,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import { type GradientDirection } from '../atoms/Gradient';
|
|
4
|
+
import { type SlidesOrientation } from './useSlides';
|
|
5
|
+
export type { SlidesOrientation } from './useSlides';
|
|
6
|
+
export type SlidesIndicators = 'dots' | 'numbers' | 'none';
|
|
7
|
+
export type SlidesIndicatorPlacement = 'overlay' | 'outside';
|
|
8
|
+
/** Un color sólido, o dos o más para un degradado (ver `Gradient`). */
|
|
9
|
+
export type SlideBackground = string | string[];
|
|
10
|
+
export interface SlidesProps<T> {
|
|
11
|
+
data: T[];
|
|
12
|
+
keyExtractor: (item: T, index: number) => string;
|
|
13
|
+
/** Contenido de la slide: imágenes, botones, lo que sea. Se dibuja sobre `background`. */
|
|
14
|
+
renderItem: (item: T, index: number) => React.ReactNode;
|
|
15
|
+
/** Fondo por slide: `'#101014'` pinta plano, `['#004cef', '#00cae1']` degrada. */
|
|
16
|
+
background?: (item: T, index: number) => SlideBackground | undefined;
|
|
17
|
+
/** Dirección del degradado cuando `background` devuelve varios colores. @default 'vertical' */
|
|
18
|
+
gradientDirection?: GradientDirection;
|
|
19
|
+
/** @default 'horizontal' */
|
|
20
|
+
orientation?: SlidesOrientation;
|
|
21
|
+
/** Círculos, círculos numerados, o ninguno. @default 'dots' */
|
|
22
|
+
indicators?: SlidesIndicators;
|
|
23
|
+
/**
|
|
24
|
+
* 'overlay' los apoya sobre la slide, en blanco; 'outside' los saca fuera
|
|
25
|
+
* del área deslizable, con los colores del tema.
|
|
26
|
+
* @default 'overlay'
|
|
27
|
+
*/
|
|
28
|
+
indicatorPlacement?: SlidesIndicatorPlacement;
|
|
29
|
+
/** Después de la última vuelve a la primera (y al revés). @default false */
|
|
30
|
+
loop?: boolean;
|
|
31
|
+
/** Avanza solo. Se pausa mientras el dedo arrastra. @default false */
|
|
32
|
+
autoPlay?: boolean;
|
|
33
|
+
/** ms entre avances de `autoPlay`. @default 4000 */
|
|
34
|
+
interval?: number;
|
|
35
|
+
/**
|
|
36
|
+
* Botón para frenar el avance automático. Sólo aparece con `autoPlay`, y va
|
|
37
|
+
* en `true` porque contenido que se mueve solo necesita una forma de
|
|
38
|
+
* pausarlo (WCAG 2.2.2).
|
|
39
|
+
* @default true
|
|
40
|
+
*/
|
|
41
|
+
showAutoPlayToggle?: boolean;
|
|
42
|
+
/** Hueco entre slides, en px. En 0 quedan pegadas. @default 0 */
|
|
43
|
+
spacing?: number;
|
|
44
|
+
/** Índice controlado. Sin él, el componente maneja el suyo desde `defaultIndex`. */
|
|
45
|
+
index?: number;
|
|
46
|
+
/** @default 0 */
|
|
47
|
+
defaultIndex?: number;
|
|
48
|
+
onIndexChange?: (index: number) => void;
|
|
49
|
+
/** Alto del área deslizable. @default 240 */
|
|
50
|
+
height?: number;
|
|
51
|
+
/** @default true */
|
|
52
|
+
swipeEnabled?: boolean;
|
|
53
|
+
/** Estilos de la caja visible de cada slide — padding, alineación, radio. */
|
|
54
|
+
slideStyle?: StyleProp<ViewStyle>;
|
|
55
|
+
style?: StyleProp<ViewStyle>;
|
|
56
|
+
testID?: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Slides
|
|
60
|
+
* Carrusel con fondo propio (color plano o degradado) y contenido libre:
|
|
61
|
+
* `renderItem` decide si adentro va una imagen, texto o botones. El estado
|
|
62
|
+
* vive en `useSlides`; acá sólo está el árbol.
|
|
63
|
+
*
|
|
64
|
+
* El deslizamiento es un `ScrollView` con `pagingEnabled` en vez de gestos a
|
|
65
|
+
* mano: es lo único que da snap nativo en iOS y Android sin
|
|
66
|
+
* `react-native-gesture-handler`, en todo el rango soportado (Expo SDK 54–57).
|
|
67
|
+
*/
|
|
68
|
+
export declare function Slides<T>({ data, keyExtractor, renderItem, background, gradientDirection, orientation, indicators, indicatorPlacement, loop, autoPlay, interval, showAutoPlayToggle, spacing, index, defaultIndex, onIndexChange, height, swipeEnabled, slideStyle, style, testID, }: SlidesProps<T>): React.JSX.Element;
|
|
69
|
+
//# sourceMappingURL=Slides.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Slides.d.ts","sourceRoot":"","sources":["../../../src/organisms/Slides.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAkC,MAAM,OAAO,CAAC;AACvD,OAAO,EAQL,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAItB,OAAO,EAAY,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,EAAa,KAAK,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEhE,YAAY,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACrD,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC;AAC3D,MAAM,MAAM,wBAAwB,GAAG,SAAS,GAAG,SAAS,CAAC;AAE7D,uEAAuE;AACvE,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,MAAM,EAAE,CAAC;AAEhD,MAAM,WAAW,WAAW,CAAC,CAAC;IAC5B,IAAI,EAAE,CAAC,EAAE,CAAC;IACV,YAAY,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACjD,0FAA0F;IAC1F,UAAU,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,CAAC,SAAS,CAAC;IACxD,kFAAkF;IAClF,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,eAAe,GAAG,SAAS,CAAC;IACrE,+FAA+F;IAC/F,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IACtC,4BAA4B;IAC5B,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAChC,+DAA+D;IAC/D,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,wBAAwB,CAAC;IAC9C,4EAA4E;IAC5E,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,sEAAsE;IACtE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,oDAAoD;IACpD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,iEAAiE;IACjE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oFAAoF;IACpF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iBAAiB;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,6CAA6C;IAC7C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oBAAoB;IACpB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAClC,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAuHD;;;;;;;;;GASG;AACH,wBAAgB,MAAM,CAAC,CAAC,EAAE,EACxB,IAAI,EACJ,YAAY,EACZ,UAAU,EACV,UAAU,EACV,iBAA8B,EAC9B,WAA0B,EAC1B,UAAmB,EACnB,kBAA8B,EAC9B,IAAY,EACZ,QAAgB,EAChB,QAA2B,EAC3B,kBAAyB,EACzB,OAAW,EACX,KAAK,EACL,YAAgB,EAChB,aAAa,EACb,MAAuB,EACvB,YAAmB,EACnB,UAAU,EACV,KAAK,EACL,MAAM,GACP,EAAE,WAAW,CAAC,CAAC,CAAC,qBA6LhB"}
|
|
@@ -30,4 +30,6 @@ export { NavigationBar } from './NavigationBar';
|
|
|
30
30
|
export type { NavigationBarProps, NavigationBarItem } from './NavigationBar';
|
|
31
31
|
export { ReorderableList } from './ReorderableList';
|
|
32
32
|
export type { ReorderableListProps } from './ReorderableList';
|
|
33
|
+
export { Slides } from './Slides';
|
|
34
|
+
export type { SlidesProps, SlidesOrientation, SlidesIndicators, SlidesIndicatorPlacement, SlideBackground, } from './Slides';
|
|
33
35
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/organisms/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACvE,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC1D,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAClE,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC7F,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACpE,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACxD,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9B,YAAY,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,YAAY,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACxD,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAC/F,YAAY,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACxE,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACtG,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC/D,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,YAAY,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAC7E,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,YAAY,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/organisms/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACvE,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC1D,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAClE,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC7F,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACpE,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACxD,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9B,YAAY,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,YAAY,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACxD,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAC/F,YAAY,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACxE,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACtG,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC/D,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,YAAY,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAC7E,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,YAAY,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAC9D,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,YAAY,EACV,WAAW,EACX,iBAAiB,EACjB,gBAAgB,EAChB,wBAAwB,EACxB,eAAe,GAChB,MAAM,UAAU,CAAC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent, ScrollView } from 'react-native';
|
|
2
|
+
export type SlidesOrientation = 'horizontal' | 'vertical';
|
|
3
|
+
export interface UseSlidesOptions {
|
|
4
|
+
total: number;
|
|
5
|
+
orientation: SlidesOrientation;
|
|
6
|
+
loop: boolean;
|
|
7
|
+
autoPlay: boolean;
|
|
8
|
+
interval: number;
|
|
9
|
+
/** Índice controlado; sin él manda el estado interno. */
|
|
10
|
+
index?: number;
|
|
11
|
+
defaultIndex: number;
|
|
12
|
+
onIndexChange?: (index: number) => void;
|
|
13
|
+
}
|
|
14
|
+
/** Una página del scroll. Los clones del modo infinito repiten el `real` de un vecino. */
|
|
15
|
+
export interface SlidesPage {
|
|
16
|
+
real: number;
|
|
17
|
+
clone: 'head' | 'tail' | null;
|
|
18
|
+
}
|
|
19
|
+
export interface UseSlidesResult {
|
|
20
|
+
active: number;
|
|
21
|
+
pages: SlidesPage[];
|
|
22
|
+
isHorizontal: boolean;
|
|
23
|
+
isLoop: boolean;
|
|
24
|
+
size: {
|
|
25
|
+
width: number;
|
|
26
|
+
height: number;
|
|
27
|
+
};
|
|
28
|
+
/** Largo de una página sobre el eje del scroll. En 0 todavía no se midió. */
|
|
29
|
+
pageSize: number;
|
|
30
|
+
scrollRef: React.RefObject<ScrollView | null>;
|
|
31
|
+
autoPlayPaused: boolean;
|
|
32
|
+
/** El sistema pidió reducir movimiento: no hay autoPlay ni transiciones. */
|
|
33
|
+
motionReduced: boolean;
|
|
34
|
+
goTo: (index: number) => void;
|
|
35
|
+
toggleAutoPlay: () => void;
|
|
36
|
+
onLayout: (event: LayoutChangeEvent) => void;
|
|
37
|
+
onMomentumScrollEnd: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
|
|
38
|
+
onScrollBeginDrag: () => void;
|
|
39
|
+
onScrollEndDrag: () => void;
|
|
40
|
+
}
|
|
41
|
+
export declare function clampIndex(value: number, total: number): number;
|
|
42
|
+
/**
|
|
43
|
+
* useSlides
|
|
44
|
+
* Todo lo que en `Slides` no se ve: qué página está encuadrada, cuándo mover el
|
|
45
|
+
* scroll, cómo se mapean los clones del modo infinito a índices reales, y el
|
|
46
|
+
* reloj del avance automático.
|
|
47
|
+
*
|
|
48
|
+
* Trabaja sólo con índices, nunca con los ítems: así no necesita el genérico
|
|
49
|
+
* `T` del componente, y el UI resuelve `pages[i].real` contra su propio `data`.
|
|
50
|
+
*/
|
|
51
|
+
export declare function useSlides({ total, orientation, loop, autoPlay, interval, index, defaultIndex, onIndexChange, }: UseSlidesOptions): UseSlidesResult;
|
|
52
|
+
//# sourceMappingURL=useSlides.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useSlides.d.ts","sourceRoot":"","sources":["../../../src/organisms/useSlides.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAG3G,MAAM,MAAM,iBAAiB,GAAG,YAAY,GAAG,UAAU,CAAC;AAE1D,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,iBAAiB,CAAC;IAC/B,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AAED,0FAA0F;AAC1F,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,YAAY,EAAE,OAAO,CAAC;IACtB,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACxC,6EAA6E;IAC7E,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IAC9C,cAAc,EAAE,OAAO,CAAC;IACxB,4EAA4E;IAC5E,aAAa,EAAE,OAAO,CAAC;IACvB,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9B,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,QAAQ,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAC7C,mBAAmB,EAAE,CAAC,KAAK,EAAE,oBAAoB,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAC;IAC9E,iBAAiB,EAAE,MAAM,IAAI,CAAC;IAC9B,eAAe,EAAE,MAAM,IAAI,CAAC;CAC7B;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAK/D;AAED;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,WAAW,EACX,IAAI,EACJ,QAAQ,EACR,QAAQ,EACR,KAAK,EACL,YAAY,EACZ,aAAa,GACd,EAAE,gBAAgB,GAAG,eAAe,CAsJpC"}
|
package/llms.txt
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# orn-ui
|
|
2
2
|
|
|
3
|
-
> React Native / Expo component library:
|
|
4
|
-
> organisms), light/dark theming, zero runtime dependencies (
|
|
5
|
-
> `react-native`
|
|
3
|
+
> React Native / Expo component library: 46 typed components (atoms, molecules,
|
|
4
|
+
> organisms), light/dark theming, zero runtime dependencies (`react`,
|
|
5
|
+
> `react-native` and `react-native-safe-area-context` are the only peers).
|
|
6
|
+
> Verified in CI against Expo SDK 54, 55, 56 and 57.
|
|
6
7
|
> Built to be used by AI coding agents: one component replaces dozens of lines
|
|
7
8
|
> of hand-written `View`/`Text`/`StyleSheet`, so a screen costs a fraction of
|
|
8
9
|
> the tokens and ships production-ready instead of prototype-only.
|
|
@@ -27,7 +28,7 @@ Wrap the app once: `<SafeAreaUIProvider defaultMode="system">` from `orn-ui/safe
|
|
|
27
28
|
|
|
28
29
|
## Components
|
|
29
30
|
|
|
30
|
-
- Atoms: Title, Subtitle, Body, Caption, Button, IconButton, Input, Checkbox, Badge, Card, Divider, Avatar, Image, Spinner, Skeleton, Transition, EmptyState, KeyValueRow, Fab, PressableScale
|
|
31
|
+
- Atoms: Title, Subtitle, Body, Caption, Button, IconButton, Input, Checkbox, Badge, Card, Divider, Gradient, Avatar, Image, Spinner, Skeleton, Transition, EmptyState, KeyValueRow, Fab, PressableScale
|
|
31
32
|
- Molecules: Stepper, OptionCard, InfoRow, FormActions, AvatarHeader, SegmentedControl, Steps, SymmetricGrid
|
|
32
|
-
- Organisms: Screen, Modal, BottomSheet, Select, List, SearchList, ReorderableList, Alert, AlertProvider, Toast, ToastProvider, DatePicker, DateField, Wizard, ThemeToggle, NavigationBar
|
|
33
|
+
- Organisms: Screen, Modal, BottomSheet, Select, List, SearchList, ReorderableList, Slides, Alert, AlertProvider, Toast, ToastProvider, DatePicker, DateField, Wizard, ThemeToggle, NavigationBar
|
|
33
34
|
- Theme: UIProvider, SafeAreaUIProvider, createTheme, createStyles, useTheme, useColors, useThemeMode, useIcons, useInsets, useLabels
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orn-ui",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "React Native / Expo component library:
|
|
3
|
+
"version": "0.2.8",
|
|
4
|
+
"description": "React Native / Expo component library: 46 typed components, light/dark theming, zero runtime dependencies. Verified on Expo SDK 54-57. Agent-friendly: one component instead of hundreds of lines of hand-written UI.",
|
|
5
5
|
"author": "David Trujillo <davot098@gmail.com>",
|
|
6
6
|
"main": "lib/commonjs/index.js",
|
|
7
7
|
"module": "lib/module/index.js",
|
|
@@ -83,6 +83,11 @@
|
|
|
83
83
|
"react-native": "./src/atoms/Divider.tsx",
|
|
84
84
|
"default": "./lib/module/atoms/Divider.js"
|
|
85
85
|
},
|
|
86
|
+
"./gradient": {
|
|
87
|
+
"types": "./lib/typescript/atoms/Gradient.d.ts",
|
|
88
|
+
"react-native": "./src/atoms/Gradient.tsx",
|
|
89
|
+
"default": "./lib/module/atoms/Gradient.js"
|
|
90
|
+
},
|
|
86
91
|
"./avatar": {
|
|
87
92
|
"types": "./lib/typescript/atoms/Avatar.d.ts",
|
|
88
93
|
"react-native": "./src/atoms/Avatar.tsx",
|
|
@@ -318,6 +323,11 @@
|
|
|
318
323
|
"react-native": "./src/organisms/ReorderableList.tsx",
|
|
319
324
|
"default": "./lib/module/organisms/ReorderableList.js"
|
|
320
325
|
},
|
|
326
|
+
"./slides": {
|
|
327
|
+
"types": "./lib/typescript/organisms/Slides.d.ts",
|
|
328
|
+
"react-native": "./src/organisms/Slides.tsx",
|
|
329
|
+
"default": "./lib/module/organisms/Slides.js"
|
|
330
|
+
},
|
|
321
331
|
"./theme": {
|
|
322
332
|
"types": "./lib/typescript/theme/index.d.ts",
|
|
323
333
|
"react-native": "./src/theme/index.ts",
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Gradient",
|
|
3
|
+
"slug": "gradient",
|
|
4
|
+
"type": "component",
|
|
5
|
+
"category": "atoms",
|
|
6
|
+
"exportedNames": [
|
|
7
|
+
"Gradient"
|
|
8
|
+
],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "atoms/Gradient.tsx",
|
|
12
|
+
"content": "import React, { memo, useMemo } from 'react';\nimport { StyleSheet, View, type StyleProp, type ViewStyle } from 'react-native';\n\nexport type GradientDirection = 'vertical' | 'horizontal' | 'diagonal' | 'diagonal-reverse';\n\nexport interface GradientProps {\n /** Paradas del degradado. Uno solo pinta plano; ninguno no pinta nada. */\n colors: string[];\n /**\n * 'vertical' va de arriba a abajo, 'horizontal' de izquierda a derecha,\n * 'diagonal' de arriba-izquierda a abajo-derecha y 'diagonal-reverse' de\n * arriba-derecha a abajo-izquierda.\n * @default 'vertical'\n */\n direction?: GradientDirection;\n /**\n * Bandas por tramo (un tramo = un par de colores consecutivos). Más bandas\n * = transición más suave y más vistas en el árbol.\n * @default 24\n */\n steps?: number;\n /** Contenido por encima del degradado. */\n children?: React.ReactNode;\n style?: StyleProp<ViewStyle>;\n testID?: string;\n}\n\nconst DEFAULT_STEPS = 24;\n\nconst styles = StyleSheet.create({\n // Recorta el plano rotado de las diagonales y el degradado al borderRadius.\n root: { overflow: 'hidden' },\n segment: { flex: 1 },\n band: { flex: 1 },\n // Plano √2 centrado: rotado 45° cubre las esquinas sin medir el contenedor.\n diagonalPlane: { position: 'absolute', top: '-21%', left: '-21%', width: '142%', height: '142%' },\n});\n\n/**\n * Gradient\n * Degradado lineal sin dependencias: React Native no trae uno en el rango\n * soportado (Expo SDK 54–57) y `experimental_backgroundImage` no está en toda\n * esa ventana, así que se compone con bandas.\n *\n * Cada tramo pinta el color inicial y encima N bandas del final con opacidad\n * de 0 a 1. El compositor mezcla alpha linealmente: el resultado es la\n * interpolación sRGB entre paradas sin parsear el color, y por eso vale\n * cualquier formato que acepte RN (hex, `rgba()`, nombre).\n */\nexport const Gradient = memo(\n ({ colors, direction = 'vertical', steps = DEFAULT_STEPS, children, style, testID }: GradientProps) => {\n const isRow = direction === 'horizontal';\n const isDiagonal = direction === 'diagonal' || direction === 'diagonal-reverse';\n // rotate positivo gira en horario (Y crece hacia abajo): -45° manda el eje\n // del degradado hacia abajo-derecha.\n const rotate = direction === 'diagonal' ? '-45deg' : '45deg';\n const bandCount = Math.max(2, Math.floor(steps));\n\n const painted = useMemo(() => {\n if (colors.length === 0) return null;\n if (colors.length === 1) {\n return <View style={[StyleSheet.absoluteFill, { backgroundColor: colors[0] }]} />;\n }\n\n const segments = colors.slice(0, -1).map((from, index) => {\n const to = colors[index + 1] as string;\n return (\n <View\n key={`${from}-${to}-${index}`}\n style={[styles.segment, { backgroundColor: from, flexDirection: isRow ? 'row' : 'column' }]}\n >\n {Array.from({ length: bandCount }, (_, band) => (\n <View key={band} style={[styles.band, { backgroundColor: to, opacity: band / (bandCount - 1) }]} />\n ))}\n </View>\n );\n });\n\n const stack = (\n <View style={[StyleSheet.absoluteFill, { flexDirection: isRow ? 'row' : 'column' }]}>{segments}</View>\n );\n\n if (!isDiagonal) return stack;\n return <View style={[styles.diagonalPlane, { transform: [{ rotate }] }]}>{segments}</View>;\n }, [colors, isRow, isDiagonal, rotate, bandCount]);\n\n return (\n <View testID={testID} style={[styles.root, style]}>\n {/* pointerEvents va en el pintado, no en la raíz: el degradado no debe\n robarle los toques a lo que el consumidor ponga encima. */}\n <View style={StyleSheet.absoluteFill} pointerEvents=\"none\">\n {painted}\n </View>\n {children}\n </View>\n );\n }\n);\nGradient.displayName = 'Gradient';\n"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [
|
|
16
|
+
"core"
|
|
17
|
+
]
|
|
18
|
+
}
|
package/registry/index.json
CHANGED
|
@@ -103,6 +103,18 @@
|
|
|
103
103
|
],
|
|
104
104
|
"bytes": 489
|
|
105
105
|
},
|
|
106
|
+
{
|
|
107
|
+
"name": "Gradient",
|
|
108
|
+
"slug": "gradient",
|
|
109
|
+
"category": "atoms",
|
|
110
|
+
"exportedNames": [
|
|
111
|
+
"Gradient"
|
|
112
|
+
],
|
|
113
|
+
"registryDependencies": [
|
|
114
|
+
"core"
|
|
115
|
+
],
|
|
116
|
+
"bytes": 3852
|
|
117
|
+
},
|
|
106
118
|
{
|
|
107
119
|
"name": "Avatar",
|
|
108
120
|
"slug": "avatar",
|
|
@@ -553,6 +565,20 @@
|
|
|
553
565
|
"core"
|
|
554
566
|
],
|
|
555
567
|
"bytes": 7324
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
"name": "Slides",
|
|
571
|
+
"slug": "slides",
|
|
572
|
+
"category": "organisms",
|
|
573
|
+
"exportedNames": [
|
|
574
|
+
"Slides"
|
|
575
|
+
],
|
|
576
|
+
"registryDependencies": [
|
|
577
|
+
"core",
|
|
578
|
+
"transition",
|
|
579
|
+
"gradient"
|
|
580
|
+
],
|
|
581
|
+
"bytes": 21434
|
|
556
582
|
}
|
|
557
583
|
]
|
|
558
584
|
}
|
package/registry/manifest.json
CHANGED
|
@@ -60,6 +60,13 @@
|
|
|
60
60
|
"file": "atoms/Divider.tsx",
|
|
61
61
|
"slug": "divider"
|
|
62
62
|
},
|
|
63
|
+
{
|
|
64
|
+
"name": "Gradient",
|
|
65
|
+
"siblingNames": [],
|
|
66
|
+
"category": "atoms",
|
|
67
|
+
"file": "atoms/Gradient.tsx",
|
|
68
|
+
"slug": "gradient"
|
|
69
|
+
},
|
|
63
70
|
{
|
|
64
71
|
"name": "Avatar",
|
|
65
72
|
"siblingNames": [],
|
|
@@ -310,6 +317,13 @@
|
|
|
310
317
|
"category": "organisms",
|
|
311
318
|
"file": "organisms/ReorderableList.tsx",
|
|
312
319
|
"slug": "reorderable-list"
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
"name": "Slides",
|
|
323
|
+
"siblingNames": [],
|
|
324
|
+
"category": "organisms",
|
|
325
|
+
"file": "organisms/Slides.tsx",
|
|
326
|
+
"slug": "slides"
|
|
313
327
|
}
|
|
314
328
|
]
|
|
315
329
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Slides",
|
|
3
|
+
"slug": "slides",
|
|
4
|
+
"type": "component",
|
|
5
|
+
"category": "organisms",
|
|
6
|
+
"exportedNames": [
|
|
7
|
+
"Slides"
|
|
8
|
+
],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "organisms/Slides.tsx",
|
|
12
|
+
"content": "import React, { memo, useEffect, useRef } from 'react';\nimport {\n Animated,\n Easing,\n ScrollView,\n StyleSheet,\n Text,\n TouchableOpacity,\n View,\n type StyleProp,\n type ViewStyle,\n} from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { useAllowFontScaling, useColors, useTheme } from '../theme/UIProvider';\nimport { useReduceMotion } from '../atoms/Transition';\nimport { Gradient, type GradientDirection } from '../atoms/Gradient';\nimport { useSlides, type SlidesOrientation } from './useSlides';\n\nexport type { SlidesOrientation } from './useSlides';\nexport type SlidesIndicators = 'dots' | 'numbers' | 'none';\nexport type SlidesIndicatorPlacement = 'overlay' | 'outside';\n\n/** Un color sólido, o dos o más para un degradado (ver `Gradient`). */\nexport type SlideBackground = string | string[];\n\nexport interface SlidesProps<T> {\n data: T[];\n keyExtractor: (item: T, index: number) => string;\n /** Contenido de la slide: imágenes, botones, lo que sea. Se dibuja sobre `background`. */\n renderItem: (item: T, index: number) => React.ReactNode;\n /** Fondo por slide: `'#101014'` pinta plano, `['#004cef', '#00cae1']` degrada. */\n background?: (item: T, index: number) => SlideBackground | undefined;\n /** Dirección del degradado cuando `background` devuelve varios colores. @default 'vertical' */\n gradientDirection?: GradientDirection;\n /** @default 'horizontal' */\n orientation?: SlidesOrientation;\n /** Círculos, círculos numerados, o ninguno. @default 'dots' */\n indicators?: SlidesIndicators;\n /**\n * 'overlay' los apoya sobre la slide, en blanco; 'outside' los saca fuera\n * del área deslizable, con los colores del tema.\n * @default 'overlay'\n */\n indicatorPlacement?: SlidesIndicatorPlacement;\n /** Después de la última vuelve a la primera (y al revés). @default false */\n loop?: boolean;\n /** Avanza solo. Se pausa mientras el dedo arrastra. @default false */\n autoPlay?: boolean;\n /** ms entre avances de `autoPlay`. @default 4000 */\n interval?: number;\n /**\n * Botón para frenar el avance automático. Sólo aparece con `autoPlay`, y va\n * en `true` porque contenido que se mueve solo necesita una forma de\n * pausarlo (WCAG 2.2.2).\n * @default true\n */\n showAutoPlayToggle?: boolean;\n /** Hueco entre slides, en px. En 0 quedan pegadas. @default 0 */\n spacing?: number;\n /** Índice controlado. Sin él, el componente maneja el suyo desde `defaultIndex`. */\n index?: number;\n /** @default 0 */\n defaultIndex?: number;\n onIndexChange?: (index: number) => void;\n /** Alto del área deslizable. @default 240 */\n height?: number;\n /** @default true */\n swipeEnabled?: boolean;\n /** Estilos de la caja visible de cada slide — padding, alineación, radio. */\n slideStyle?: StyleProp<ViewStyle>;\n style?: StyleProp<ViewStyle>;\n testID?: string;\n}\n\nconst DOT = 8;\nconst DOT_ACTIVE = 20;\nconst NUMBER_CIRCLE = 24;\nconst TOGGLE = 32;\nconst DEFAULT_HEIGHT = 240;\nconst DEFAULT_INTERVAL = 4000;\n\nconst useStyles = createStyles((theme) => ({\n // Deja redondear el bloque por `style` sin que el degradado se salga.\n root: { overflow: 'hidden' },\n viewport: { flex: 1 },\n // La slide visible es la caja de adentro: con `spacing` la página es más\n // ancha que ella, y el fondo tiene que recortarse acá y no en la página, o\n // el degradado pinta también el hueco.\n slide: { flex: 1, overflow: 'hidden' },\n // Con indicadores fuera y orientación vertical el contenedor es una fila:\n // sin flex la ventana no tiene ancho propio y colapsa a 0.\n viewportInRow: { flex: 1 },\n slideContent: { flex: 1 },\n\n indicatorsRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center' },\n indicatorsColumn: { flexDirection: 'column', alignItems: 'center', justifyContent: 'center' },\n indicatorsOverlayHorizontal: { position: 'absolute', left: 0, right: 0, bottom: theme.tokens.spacing.md },\n indicatorsOverlayVertical: { position: 'absolute', top: 0, bottom: 0, right: theme.tokens.spacing.md },\n indicatorsOutsideHorizontal: { paddingTop: theme.tokens.spacing.md },\n indicatorsOutsideVertical: { paddingLeft: theme.tokens.spacing.md },\n indicatorSlot: { padding: theme.tokens.spacing.xs },\n\n dot: { width: DOT, height: DOT, borderRadius: DOT / 2 },\n\n autoPlayToggle: {\n position: 'absolute',\n top: theme.tokens.spacing.md,\n right: theme.tokens.spacing.md,\n width: TOGGLE,\n height: TOGGLE,\n borderRadius: TOGGLE / 2,\n alignItems: 'center',\n justifyContent: 'center',\n flexDirection: 'row',\n gap: 3,\n },\n pauseBar: { width: 3, height: 12, borderRadius: 1 },\n // Triángulo con bordes: un \"play\" sin SVG ni set de íconos.\n playTriangle: {\n width: 0,\n height: 0,\n marginLeft: 2,\n borderTopWidth: 6,\n borderBottomWidth: 6,\n borderLeftWidth: 10,\n borderTopColor: 'transparent',\n borderBottomColor: 'transparent',\n },\n\n numberCircle: {\n minWidth: NUMBER_CIRCLE,\n height: NUMBER_CIRCLE,\n borderRadius: NUMBER_CIRCLE / 2,\n borderWidth: 1,\n paddingHorizontal: theme.tokens.spacing.xs,\n alignItems: 'center',\n justifyContent: 'center',\n },\n numberText: { fontSize: theme.tokens.fontSize.xs, fontWeight: theme.tokens.fontWeight.semibold },\n\n outsideHorizontal: { flexDirection: 'column' },\n outsideVertical: { flexDirection: 'row' },\n}));\n\ninterface DotProps {\n selected: boolean;\n isHorizontal: boolean;\n /** En overlay el contraste lo da la opacidad; afuera, el color. */\n overlay: boolean;\n activeColor: string;\n inactiveColor: string;\n testID?: string;\n}\n\n/**\n * El punto activo se estira en vez de saltar: el cambio de golpe se lee como\n * \"apareció otro punto\", no como \"el mismo se movió\". Ancho y color son props\n * de layout/pintura, así que van por el driver de JS.\n */\nconst Dot = memo(({ selected, isHorizontal, overlay, activeColor, inactiveColor, testID }: DotProps) => {\n const styles = useStyles();\n const theme = useTheme();\n const reduceMotion = useReduceMotion();\n const progress = useRef(new Animated.Value(selected ? 1 : 0)).current;\n\n useEffect(() => {\n Animated.timing(progress, {\n toValue: selected ? 1 : 0,\n duration: reduceMotion ? 0 : theme.tokens.duration.base,\n easing: Easing.out(Easing.quad),\n useNativeDriver: false,\n }).start();\n }, [progress, reduceMotion, selected, theme]);\n\n const length = progress.interpolate({ inputRange: [0, 1], outputRange: [DOT, DOT_ACTIVE] });\n const backgroundColor = progress.interpolate({ inputRange: [0, 1], outputRange: [inactiveColor, activeColor] });\n\n return (\n <Animated.View\n testID={testID}\n style={[\n styles.dot,\n isHorizontal ? { width: length } : { height: length },\n { backgroundColor },\n overlay && { opacity: progress.interpolate({ inputRange: [0, 1], outputRange: [0.45, 1] }) },\n ]}\n />\n );\n});\nDot.displayName = 'SlidesDot';\n\n/**\n * Slides\n * Carrusel con fondo propio (color plano o degradado) y contenido libre:\n * `renderItem` decide si adentro va una imagen, texto o botones. El estado\n * vive en `useSlides`; acá sólo está el árbol.\n *\n * El deslizamiento es un `ScrollView` con `pagingEnabled` en vez de gestos a\n * mano: es lo único que da snap nativo en iOS y Android sin\n * `react-native-gesture-handler`, en todo el rango soportado (Expo SDK 54–57).\n */\nexport function Slides<T>({\n data,\n keyExtractor,\n renderItem,\n background,\n gradientDirection = 'vertical',\n orientation = 'horizontal',\n indicators = 'dots',\n indicatorPlacement = 'overlay',\n loop = false,\n autoPlay = false,\n interval = DEFAULT_INTERVAL,\n showAutoPlayToggle = true,\n spacing = 0,\n index,\n defaultIndex = 0,\n onIndexChange,\n height = DEFAULT_HEIGHT,\n swipeEnabled = true,\n slideStyle,\n style,\n testID,\n}: SlidesProps<T>) {\n const colors = useColors();\n const styles = useStyles();\n const allowFontScaling = useAllowFontScaling();\n\n const total = data.length;\n const {\n active,\n pages,\n isHorizontal,\n size,\n pageSize,\n scrollRef,\n autoPlayPaused,\n motionReduced,\n goTo,\n toggleAutoPlay,\n onLayout,\n onMomentumScrollEnd,\n onScrollBeginDrag,\n onScrollEndDrag,\n } = useSlides({ total, orientation, loop, autoPlay, interval, index, defaultIndex, onIndexChange });\n\n const isOverlay = indicatorPlacement === 'overlay';\n\n const renderBackground = (item: T, real: number) => {\n const value = background?.(item, real);\n if (!value) return null;\n if (Array.isArray(value)) {\n return <Gradient colors={value} direction={gradientDirection} style={StyleSheet.absoluteFill} />;\n }\n return <View style={[StyleSheet.absoluteFill, { backgroundColor: value }]} pointerEvents=\"none\" />;\n };\n\n const renderIndicator = (position: number) => {\n const selected = position === active;\n\n if (indicators === 'numbers') {\n const numberColors = isOverlay\n ? {\n background: selected ? colors.white : 'transparent',\n border: colors.white,\n text: selected ? colors.black : colors.white,\n }\n : {\n background: selected ? colors.primary : 'transparent',\n border: selected ? colors.primary : colors.border,\n text: selected ? colors.onPrimary : colors.textLight,\n };\n return (\n <View\n style={[\n styles.numberCircle,\n { backgroundColor: numberColors.background, borderColor: numberColors.border },\n ]}\n >\n <Text allowFontScaling={allowFontScaling} style={[styles.numberText, { color: numberColors.text }]}>\n {position + 1}\n </Text>\n </View>\n );\n }\n\n return (\n <Dot\n testID={`${testID ?? 'slides'}-dot-${position}`}\n selected={selected}\n isHorizontal={isHorizontal}\n overlay={isOverlay}\n activeColor={isOverlay ? colors.white : colors.primary}\n inactiveColor={isOverlay ? colors.white : colors.border}\n />\n );\n };\n\n // Con reduce-motion el autoPlay ya está apagado: no hay nada que pausar.\n const renderAutoPlayToggle = () => {\n if (!autoPlay || !showAutoPlayToggle || motionReduced || total < 2) return null;\n return (\n <TouchableOpacity\n testID={`${testID ?? 'slides'}-autoplay-toggle`}\n style={[styles.autoPlayToggle, { backgroundColor: colors.black, opacity: 0.55 }]}\n activeOpacity={0.7}\n onPress={toggleAutoPlay}\n accessibilityRole=\"button\"\n accessibilityLabel={autoPlayPaused ? 'Resume automatic slides' : 'Pause automatic slides'}\n accessibilityState={{ selected: !autoPlayPaused }}\n >\n {autoPlayPaused ? (\n <View style={[styles.playTriangle, { borderLeftColor: colors.white }]} />\n ) : (\n <>\n <View style={[styles.pauseBar, { backgroundColor: colors.white }]} />\n <View style={[styles.pauseBar, { backgroundColor: colors.white }]} />\n </>\n )}\n </TouchableOpacity>\n );\n };\n\n const renderIndicators = () => {\n if (indicators === 'none' || total < 2) return null;\n return (\n <View\n testID={`${testID ?? 'slides'}-indicators`}\n style={[\n isHorizontal ? styles.indicatorsRow : styles.indicatorsColumn,\n isOverlay\n ? isHorizontal\n ? styles.indicatorsOverlayHorizontal\n : styles.indicatorsOverlayVertical\n : isHorizontal\n ? styles.indicatorsOutsideHorizontal\n : styles.indicatorsOutsideVertical,\n ]}\n pointerEvents=\"box-none\"\n >\n {data.map((item, position) => (\n <TouchableOpacity\n key={`indicator-${keyExtractor(item, position)}`}\n testID={`${testID ?? 'slides'}-indicator-${position}`}\n style={styles.indicatorSlot}\n activeOpacity={0.7}\n onPress={() => goTo(position)}\n accessibilityRole=\"button\"\n accessibilityLabel={`Slide ${position + 1} of ${total}`}\n accessibilityState={{ selected: position === active }}\n >\n {renderIndicator(position)}\n </TouchableOpacity>\n ))}\n </View>\n );\n };\n\n const viewport = (\n <View\n testID={testID}\n style={[styles.root, { height }, !isOverlay && !isHorizontal && styles.viewportInRow, isOverlay && style]}\n onLayout={onLayout}\n >\n <ScrollView\n ref={scrollRef}\n testID={`${testID ?? 'slides'}-scroll`}\n style={styles.viewport}\n horizontal={isHorizontal}\n pagingEnabled\n scrollEnabled={swipeEnabled && total > 1}\n showsHorizontalScrollIndicator={false}\n showsVerticalScrollIndicator={false}\n onMomentumScrollEnd={onMomentumScrollEnd}\n onScrollBeginDrag={onScrollBeginDrag}\n onScrollEndDrag={onScrollEndDrag}\n >\n {pageSize > 0 &&\n pages.map(({ real, clone }) => {\n const item = data[real] as T;\n return (\n // El hueco se reparte a los dos lados de cada página: así la\n // separación entre dos slides es `spacing` entero y la página\n // sigue midiendo exactamente lo que pagina el ScrollView.\n <View\n key={clone ? `slides-clone-${clone}` : keyExtractor(item, real)}\n style={[\n { width: size.width, height: size.height },\n isHorizontal ? { paddingHorizontal: spacing / 2 } : { paddingVertical: spacing / 2 },\n ]}\n >\n <View style={[styles.slide, slideStyle]}>\n {renderBackground(item, real)}\n <View style={styles.slideContent}>{renderItem(item, real)}</View>\n </View>\n </View>\n );\n })}\n </ScrollView>\n {renderAutoPlayToggle()}\n {isOverlay && renderIndicators()}\n </View>\n );\n\n if (isOverlay) return viewport;\n\n return (\n <View style={[isHorizontal ? styles.outsideHorizontal : styles.outsideVertical, style]}>\n {viewport}\n {renderIndicators()}\n </View>\n );\n}\n"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"path": "organisms/useSlides.ts",
|
|
16
|
+
"content": "import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport type { LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent, ScrollView } from 'react-native';\nimport { useReduceMotion } from '../atoms/Transition';\n\nexport type SlidesOrientation = 'horizontal' | 'vertical';\n\nexport interface UseSlidesOptions {\n total: number;\n orientation: SlidesOrientation;\n loop: boolean;\n autoPlay: boolean;\n interval: number;\n /** Índice controlado; sin él manda el estado interno. */\n index?: number;\n defaultIndex: number;\n onIndexChange?: (index: number) => void;\n}\n\n/** Una página del scroll. Los clones del modo infinito repiten el `real` de un vecino. */\nexport interface SlidesPage {\n real: number;\n clone: 'head' | 'tail' | null;\n}\n\nexport interface UseSlidesResult {\n active: number;\n pages: SlidesPage[];\n isHorizontal: boolean;\n isLoop: boolean;\n size: { width: number; height: number };\n /** Largo de una página sobre el eje del scroll. En 0 todavía no se midió. */\n pageSize: number;\n scrollRef: React.RefObject<ScrollView | null>;\n autoPlayPaused: boolean;\n /** El sistema pidió reducir movimiento: no hay autoPlay ni transiciones. */\n motionReduced: boolean;\n goTo: (index: number) => void;\n toggleAutoPlay: () => void;\n onLayout: (event: LayoutChangeEvent) => void;\n onMomentumScrollEnd: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;\n onScrollBeginDrag: () => void;\n onScrollEndDrag: () => void;\n}\n\nexport function clampIndex(value: number, total: number): number {\n if (total === 0) return 0;\n if (value < 0) return 0;\n if (value > total - 1) return total - 1;\n return value;\n}\n\n/**\n * useSlides\n * Todo lo que en `Slides` no se ve: qué página está encuadrada, cuándo mover el\n * scroll, cómo se mapean los clones del modo infinito a índices reales, y el\n * reloj del avance automático.\n *\n * Trabaja sólo con índices, nunca con los ítems: así no necesita el genérico\n * `T` del componente, y el UI resuelve `pages[i].real` contra su propio `data`.\n */\nexport function useSlides({\n total,\n orientation,\n loop,\n autoPlay,\n interval,\n index,\n defaultIndex,\n onIndexChange,\n}: UseSlidesOptions): UseSlidesResult {\n const motionReduced = useReduceMotion();\n const isHorizontal = orientation === 'horizontal';\n // Un solo elemento no tiene a dónde ciclar: quedaría deslizable entre tres\n // copias de lo mismo.\n const isLoop = loop && total > 1;\n\n const [uncontrolled, setUncontrolled] = useState(() => clampIndex(defaultIndex, total));\n const active = clampIndex(index ?? uncontrolled, total);\n const activeRef = useRef(active);\n activeRef.current = active;\n\n const [autoPlayPaused, setAutoPlayPaused] = useState(false);\n const [size, setSize] = useState({ width: 0, height: 0 });\n const pageSize = isHorizontal ? size.width : size.height;\n\n const scrollRef = useRef<ScrollView>(null);\n /** Página realmente visible (índice dentro de `pages`, clones incluidos). */\n const offsetPageRef = useRef(isLoop ? 1 : 0);\n const didInitRef = useRef(false);\n const lastPageSizeRef = useRef(0);\n const draggingRef = useRef(false);\n\n const pages = useMemo<SlidesPage[]>(() => {\n const real = Array.from({ length: total }, (_, i) => ({ real: i, clone: null }));\n if (!isLoop) return real;\n return [{ real: total - 1, clone: 'head' }, ...real, { real: 0, clone: 'tail' }];\n }, [isLoop, total]);\n\n const scrollToPage = useCallback(\n (page: number, animated: boolean) => {\n const offset = page * pageSize;\n scrollRef.current?.scrollTo({\n x: isHorizontal ? offset : 0,\n y: isHorizontal ? 0 : offset,\n animated,\n });\n },\n [isHorizontal, pageSize]\n );\n\n const goTo = useCallback(\n (next: number) => {\n if (next === activeRef.current) return;\n activeRef.current = next;\n if (index == null) setUncontrolled(next);\n onIndexChange?.(next);\n },\n [index, onIndexChange]\n );\n\n // Sólo actúa cuando el índice y la página visible discrepan: un cambio que\n // vino del propio gesto no dispara un segundo scroll encima del que frenó.\n useEffect(() => {\n if (pageSize === 0 || total === 0) return;\n const target = isLoop ? active + 1 : active;\n const resized = lastPageSizeRef.current !== pageSize;\n if (offsetPageRef.current === target && didInitRef.current && !resized) return;\n // Ni el primer posicionamiento ni el reacomodo por rotación son un cambio\n // de slide: animarlos sería un deslizamiento que nadie pidió.\n const animated = didInitRef.current && !resized;\n didInitRef.current = true;\n lastPageSizeRef.current = pageSize;\n offsetPageRef.current = target;\n scrollToPage(target, animated);\n }, [active, isLoop, pageSize, scrollToPage, total]);\n\n const onLayout = useCallback((event: LayoutChangeEvent) => {\n const { width, height } = event.nativeEvent.layout;\n setSize((prev) => (prev.width === width && prev.height === height ? prev : { width, height }));\n }, []);\n\n const onMomentumScrollEnd = useCallback(\n (event: NativeSyntheticEvent<NativeScrollEvent>) => {\n draggingRef.current = false;\n if (pageSize === 0) return;\n const { contentOffset } = event.nativeEvent;\n const page = Math.round((isHorizontal ? contentOffset.x : contentOffset.y) / pageSize);\n\n if (isLoop) {\n if (page === 0) {\n offsetPageRef.current = total;\n scrollToPage(total, false);\n goTo(total - 1);\n return;\n }\n if (page === total + 1) {\n offsetPageRef.current = 1;\n scrollToPage(1, false);\n goTo(0);\n return;\n }\n offsetPageRef.current = page;\n goTo(page - 1);\n return;\n }\n\n offsetPageRef.current = page;\n goTo(clampIndex(page, total));\n },\n [goTo, isHorizontal, isLoop, pageSize, scrollToPage, total]\n );\n\n const onScrollBeginDrag = useCallback(() => {\n draggingRef.current = true;\n }, []);\n\n const onScrollEndDrag = useCallback(() => {\n // Un arrastre corto puede soltarse sin inercia y no emitir\n // momentumScrollEnd: sin esto el autoPlay quedaría pausado para siempre.\n draggingRef.current = false;\n }, []);\n\n const toggleAutoPlay = useCallback(() => setAutoPlayPaused((paused) => !paused), []);\n\n useEffect(() => {\n if (!autoPlay || autoPlayPaused || motionReduced || total < 2 || pageSize === 0) return;\n const timer = setInterval(() => {\n if (draggingRef.current) return;\n const from = activeRef.current;\n if (from < total - 1) {\n goTo(from + 1);\n return;\n }\n if (!isLoop) return;\n // Última en modo infinito: avanza al clon de la primera en vez de\n // rebobinar; el salto al original lo cierra momentumScrollEnd.\n offsetPageRef.current = total + 1;\n scrollToPage(total + 1, true);\n }, interval);\n return () => clearInterval(timer);\n }, [autoPlay, autoPlayPaused, goTo, interval, isLoop, motionReduced, pageSize, scrollToPage, total]);\n\n return {\n active,\n pages,\n isHorizontal,\n isLoop,\n size,\n pageSize,\n scrollRef,\n autoPlayPaused,\n motionReduced,\n goTo,\n toggleAutoPlay,\n onLayout,\n onMomentumScrollEnd,\n onScrollBeginDrag,\n onScrollEndDrag,\n };\n}\n"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"registryDependencies": [
|
|
20
|
+
"core",
|
|
21
|
+
"transition",
|
|
22
|
+
"gradient"
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import React, { memo, useMemo } from 'react';
|
|
2
|
+
import { StyleSheet, View, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export type GradientDirection = 'vertical' | 'horizontal' | 'diagonal' | 'diagonal-reverse';
|
|
5
|
+
|
|
6
|
+
export interface GradientProps {
|
|
7
|
+
/** Paradas del degradado. Uno solo pinta plano; ninguno no pinta nada. */
|
|
8
|
+
colors: string[];
|
|
9
|
+
/**
|
|
10
|
+
* 'vertical' va de arriba a abajo, 'horizontal' de izquierda a derecha,
|
|
11
|
+
* 'diagonal' de arriba-izquierda a abajo-derecha y 'diagonal-reverse' de
|
|
12
|
+
* arriba-derecha a abajo-izquierda.
|
|
13
|
+
* @default 'vertical'
|
|
14
|
+
*/
|
|
15
|
+
direction?: GradientDirection;
|
|
16
|
+
/**
|
|
17
|
+
* Bandas por tramo (un tramo = un par de colores consecutivos). Más bandas
|
|
18
|
+
* = transición más suave y más vistas en el árbol.
|
|
19
|
+
* @default 24
|
|
20
|
+
*/
|
|
21
|
+
steps?: number;
|
|
22
|
+
/** Contenido por encima del degradado. */
|
|
23
|
+
children?: React.ReactNode;
|
|
24
|
+
style?: StyleProp<ViewStyle>;
|
|
25
|
+
testID?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const DEFAULT_STEPS = 24;
|
|
29
|
+
|
|
30
|
+
const styles = StyleSheet.create({
|
|
31
|
+
// Recorta el plano rotado de las diagonales y el degradado al borderRadius.
|
|
32
|
+
root: { overflow: 'hidden' },
|
|
33
|
+
segment: { flex: 1 },
|
|
34
|
+
band: { flex: 1 },
|
|
35
|
+
// Plano √2 centrado: rotado 45° cubre las esquinas sin medir el contenedor.
|
|
36
|
+
diagonalPlane: { position: 'absolute', top: '-21%', left: '-21%', width: '142%', height: '142%' },
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Gradient
|
|
41
|
+
* Degradado lineal sin dependencias: React Native no trae uno en el rango
|
|
42
|
+
* soportado (Expo SDK 54–57) y `experimental_backgroundImage` no está en toda
|
|
43
|
+
* esa ventana, así que se compone con bandas.
|
|
44
|
+
*
|
|
45
|
+
* Cada tramo pinta el color inicial y encima N bandas del final con opacidad
|
|
46
|
+
* de 0 a 1. El compositor mezcla alpha linealmente: el resultado es la
|
|
47
|
+
* interpolación sRGB entre paradas sin parsear el color, y por eso vale
|
|
48
|
+
* cualquier formato que acepte RN (hex, `rgba()`, nombre).
|
|
49
|
+
*/
|
|
50
|
+
export const Gradient = memo(
|
|
51
|
+
({ colors, direction = 'vertical', steps = DEFAULT_STEPS, children, style, testID }: GradientProps) => {
|
|
52
|
+
const isRow = direction === 'horizontal';
|
|
53
|
+
const isDiagonal = direction === 'diagonal' || direction === 'diagonal-reverse';
|
|
54
|
+
// rotate positivo gira en horario (Y crece hacia abajo): -45° manda el eje
|
|
55
|
+
// del degradado hacia abajo-derecha.
|
|
56
|
+
const rotate = direction === 'diagonal' ? '-45deg' : '45deg';
|
|
57
|
+
const bandCount = Math.max(2, Math.floor(steps));
|
|
58
|
+
|
|
59
|
+
const painted = useMemo(() => {
|
|
60
|
+
if (colors.length === 0) return null;
|
|
61
|
+
if (colors.length === 1) {
|
|
62
|
+
return <View style={[StyleSheet.absoluteFill, { backgroundColor: colors[0] }]} />;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const segments = colors.slice(0, -1).map((from, index) => {
|
|
66
|
+
const to = colors[index + 1] as string;
|
|
67
|
+
return (
|
|
68
|
+
<View
|
|
69
|
+
key={`${from}-${to}-${index}`}
|
|
70
|
+
style={[styles.segment, { backgroundColor: from, flexDirection: isRow ? 'row' : 'column' }]}
|
|
71
|
+
>
|
|
72
|
+
{Array.from({ length: bandCount }, (_, band) => (
|
|
73
|
+
<View key={band} style={[styles.band, { backgroundColor: to, opacity: band / (bandCount - 1) }]} />
|
|
74
|
+
))}
|
|
75
|
+
</View>
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const stack = (
|
|
80
|
+
<View style={[StyleSheet.absoluteFill, { flexDirection: isRow ? 'row' : 'column' }]}>{segments}</View>
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
if (!isDiagonal) return stack;
|
|
84
|
+
return <View style={[styles.diagonalPlane, { transform: [{ rotate }] }]}>{segments}</View>;
|
|
85
|
+
}, [colors, isRow, isDiagonal, rotate, bandCount]);
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<View testID={testID} style={[styles.root, style]}>
|
|
89
|
+
{/* pointerEvents va en el pintado, no en la raíz: el degradado no debe
|
|
90
|
+
robarle los toques a lo que el consumidor ponga encima. */}
|
|
91
|
+
<View style={StyleSheet.absoluteFill} pointerEvents="none">
|
|
92
|
+
{painted}
|
|
93
|
+
</View>
|
|
94
|
+
{children}
|
|
95
|
+
</View>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
Gradient.displayName = 'Gradient';
|
package/src/atoms/index.ts
CHANGED
|
@@ -14,6 +14,8 @@ export { Card } from './Card';
|
|
|
14
14
|
export type { CardProps } from './Card';
|
|
15
15
|
export { Divider } from './Divider';
|
|
16
16
|
export type { DividerProps } from './Divider';
|
|
17
|
+
export { Gradient } from './Gradient';
|
|
18
|
+
export type { GradientProps, GradientDirection } from './Gradient';
|
|
17
19
|
export { Avatar } from './Avatar';
|
|
18
20
|
export type { AvatarProps } from './Avatar';
|
|
19
21
|
export { Image, prefetchImage } from './Image';
|