orn-ui 0.2.2 → 0.2.4
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/lib/commonjs/molecules/Steps.js +14 -6
- package/lib/commonjs/molecules/Steps.js.map +1 -1
- package/lib/commonjs/molecules/SymmetricGrid.js +84 -0
- package/lib/commonjs/molecules/SymmetricGrid.js.map +1 -0
- package/lib/commonjs/molecules/index.js +7 -0
- package/lib/commonjs/molecules/index.js.map +1 -1
- package/lib/commonjs/organisms/AlertProvider.js +12 -4
- package/lib/commonjs/organisms/AlertProvider.js.map +1 -1
- package/lib/commonjs/organisms/BottomSheet.js +58 -50
- package/lib/commonjs/organisms/BottomSheet.js.map +1 -1
- package/lib/commonjs/organisms/Modal.js +44 -42
- package/lib/commonjs/organisms/Modal.js.map +1 -1
- package/lib/commonjs/organisms/NavigationBar.js +4 -1
- package/lib/commonjs/organisms/NavigationBar.js.map +1 -1
- package/lib/commonjs/organisms/ReorderableList.js +143 -0
- package/lib/commonjs/organisms/ReorderableList.js.map +1 -0
- package/lib/commonjs/organisms/Screen.js +1 -2
- package/lib/commonjs/organisms/Screen.js.map +1 -1
- package/lib/commonjs/organisms/index.js +7 -0
- package/lib/commonjs/organisms/index.js.map +1 -1
- package/lib/commonjs/safe-area/SafeAreaUIProvider.js +38 -30
- package/lib/commonjs/safe-area/SafeAreaUIProvider.js.map +1 -1
- package/lib/commonjs/theme/UIProvider.js +35 -8
- package/lib/commonjs/theme/UIProvider.js.map +1 -1
- package/lib/module/molecules/Steps.js +14 -6
- package/lib/module/molecules/Steps.js.map +1 -1
- package/lib/module/molecules/SymmetricGrid.js +79 -0
- package/lib/module/molecules/SymmetricGrid.js.map +1 -0
- package/lib/module/molecules/index.js +1 -0
- package/lib/module/molecules/index.js.map +1 -1
- package/lib/module/organisms/AlertProvider.js +12 -4
- package/lib/module/organisms/AlertProvider.js.map +1 -1
- package/lib/module/organisms/BottomSheet.js +59 -51
- package/lib/module/organisms/BottomSheet.js.map +1 -1
- package/lib/module/organisms/Modal.js +45 -43
- package/lib/module/organisms/Modal.js.map +1 -1
- package/lib/module/organisms/NavigationBar.js +4 -1
- package/lib/module/organisms/NavigationBar.js.map +1 -1
- package/lib/module/organisms/ReorderableList.js +137 -0
- package/lib/module/organisms/ReorderableList.js.map +1 -0
- package/lib/module/organisms/Screen.js +1 -2
- package/lib/module/organisms/Screen.js.map +1 -1
- package/lib/module/organisms/index.js +1 -0
- package/lib/module/organisms/index.js.map +1 -1
- package/lib/module/safe-area/SafeAreaUIProvider.js +39 -31
- package/lib/module/safe-area/SafeAreaUIProvider.js.map +1 -1
- package/lib/module/theme/UIProvider.js +36 -10
- package/lib/module/theme/UIProvider.js.map +1 -1
- package/lib/typescript/molecules/Steps.d.ts.map +1 -1
- package/lib/typescript/molecules/SymmetricGrid.d.ts +26 -0
- package/lib/typescript/molecules/SymmetricGrid.d.ts.map +1 -0
- package/lib/typescript/molecules/index.d.ts +2 -0
- package/lib/typescript/molecules/index.d.ts.map +1 -1
- package/lib/typescript/organisms/AlertProvider.d.ts.map +1 -1
- package/lib/typescript/organisms/BottomSheet.d.ts.map +1 -1
- package/lib/typescript/organisms/Modal.d.ts.map +1 -1
- package/lib/typescript/organisms/NavigationBar.d.ts.map +1 -1
- package/lib/typescript/organisms/ReorderableList.d.ts +29 -0
- package/lib/typescript/organisms/ReorderableList.d.ts.map +1 -0
- package/lib/typescript/organisms/Screen.d.ts.map +1 -1
- package/lib/typescript/organisms/index.d.ts +2 -0
- package/lib/typescript/organisms/index.d.ts.map +1 -1
- package/lib/typescript/safe-area/SafeAreaUIProvider.d.ts +0 -38
- package/lib/typescript/safe-area/SafeAreaUIProvider.d.ts.map +1 -1
- package/lib/typescript/theme/UIProvider.d.ts +21 -1
- package/lib/typescript/theme/UIProvider.d.ts.map +1 -1
- package/package.json +11 -1
- package/registry/alert-provider.json +1 -1
- package/registry/bottom-sheet.json +1 -1
- package/registry/core.json +1 -1
- package/registry/index.json +31 -7
- package/registry/manifest.json +14 -0
- package/registry/modal.json +1 -1
- package/registry/navigation-bar.json +1 -1
- package/registry/reorderable-list.json +18 -0
- package/registry/screen.json +1 -1
- package/registry/steps.json +1 -1
- package/registry/symmetric-grid.json +18 -0
- package/src/molecules/Steps.tsx +14 -8
- package/src/molecules/SymmetricGrid.tsx +82 -0
- package/src/molecules/index.ts +2 -0
- package/src/organisms/AlertProvider.tsx +13 -5
- package/src/organisms/BottomSheet.tsx +58 -44
- package/src/organisms/Modal.tsx +63 -55
- package/src/organisms/NavigationBar.tsx +4 -1
- package/src/organisms/ReorderableList.tsx +151 -0
- package/src/organisms/Screen.tsx +1 -2
- package/src/organisms/index.ts +2 -0
- package/src/safe-area/SafeAreaUIProvider.tsx +46 -41
- package/src/theme/UIProvider.tsx +50 -8
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
export interface ReorderableListProps<T> {
|
|
4
|
+
data: T[];
|
|
5
|
+
keyExtractor: (item: T, index: number) => string;
|
|
6
|
+
/** Alto fijo de cada fila: necesario para calcular el índice destino sin medir layout async. */
|
|
7
|
+
itemHeight: number;
|
|
8
|
+
renderItem: (item: T, index: number, dragging: boolean) => React.ReactElement;
|
|
9
|
+
/** Se llama al soltar, con el arreglo ya reordenado. No muta `data`. */
|
|
10
|
+
onReorder: (data: T[]) => void;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
style?: StyleProp<ViewStyle>;
|
|
13
|
+
}
|
|
14
|
+
/** Reordena moviendo el ítem de `from` a `to`, sin mutar `data`. Exportada aparte por ser testeable sin simular gestos. */
|
|
15
|
+
export declare function move<T>(data: T[], from: number, to: number): T[];
|
|
16
|
+
/**
|
|
17
|
+
* ReorderableList
|
|
18
|
+
* Ley de destino común (common fate): al arrastrar una fila, las filas que
|
|
19
|
+
* tiene que atravesar se desplazan juntas en el mismo sentido — eso es lo
|
|
20
|
+
* que comunica "van a intercambiar lugar" antes de soltar el dedo, no un
|
|
21
|
+
* simple resaltado del ítem activo.
|
|
22
|
+
*
|
|
23
|
+
* Sin dependencias de gestos/animación externas (reanimated, gesture-handler):
|
|
24
|
+
* PanResponder + Animated alcanzan porque el cálculo de índice destino
|
|
25
|
+
* necesita el valor de arrastre en JS en cada movimiento (no algo que
|
|
26
|
+
* useNativeDriver pueda resolver solo).
|
|
27
|
+
*/
|
|
28
|
+
export declare function ReorderableList<T>({ data, keyExtractor, itemHeight, renderItem, onReorder, disabled, style, }: ReorderableListProps<T>): React.JSX.Element;
|
|
29
|
+
//# sourceMappingURL=ReorderableList.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReorderableList.d.ts","sourceRoot":"","sources":["../../../src/organisms/ReorderableList.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA2B,MAAM,OAAO,CAAC;AAChD,OAAO,EAAgC,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAG5F,MAAM,WAAW,oBAAoB,CAAC,CAAC;IACrC,IAAI,EAAE,CAAC,EAAE,CAAC;IACV,YAAY,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACjD,gGAAgG;IAChG,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,KAAK,KAAK,CAAC,YAAY,CAAC;IAC9E,wEAAwE;IACxE,SAAS,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,IAAI,CAAC;IAC/B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B;AAED,2HAA2H;AAC3H,wBAAgB,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,CAAC,EAAE,CAKhE;AAMD;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,EACjC,IAAI,EACJ,YAAY,EACZ,UAAU,EACV,UAAU,EACV,SAAS,EACT,QAAgB,EAChB,KAAK,GACN,EAAE,oBAAoB,CAAC,CAAC,CAAC,qBAsGzB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Screen.d.ts","sourceRoot":"","sources":["../../../src/organisms/Screen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAe,MAAM,OAAO,CAAC;AACpC,OAAO,EAAoD,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAIhG,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,QAAQ,CAAC;AAE1C,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,qBAAqB,CAAC,EAAE,SAAS,CAAC;IAClC,kEAAkE;IAClE,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC;IACrB;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAQD;;;;GAIG;AACH,eAAO,MAAM,MAAM,+GAOhB,WAAW,
|
|
1
|
+
{"version":3,"file":"Screen.d.ts","sourceRoot":"","sources":["../../../src/organisms/Screen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAe,MAAM,OAAO,CAAC;AACpC,OAAO,EAAoD,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAIhG,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,QAAQ,CAAC;AAE1C,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,qBAAqB,CAAC,EAAE,SAAS,CAAC;IAClC,kEAAkE;IAClE,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC;IACrB;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAQD;;;;GAIG;AACH,eAAO,MAAM,MAAM,+GAOhB,WAAW,uBA8CZ,CAAC"}
|
|
@@ -28,4 +28,6 @@ export { ThemeToggle } from './ThemeToggle';
|
|
|
28
28
|
export type { ThemeToggleProps } from './ThemeToggle';
|
|
29
29
|
export { NavigationBar } from './NavigationBar';
|
|
30
30
|
export type { NavigationBarProps, NavigationBarItem } from './NavigationBar';
|
|
31
|
+
export { ReorderableList } from './ReorderableList';
|
|
32
|
+
export type { ReorderableListProps } from './ReorderableList';
|
|
31
33
|
//# 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"}
|
|
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,46 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { type UIProviderProps } from '../theme/UIProvider';
|
|
3
3
|
export interface SafeAreaUIProviderProps extends Omit<UIProviderProps, 'insets'> {
|
|
4
|
-
/**
|
|
5
|
-
* Insets explícitos. Ganan sobre los medidos — útil para tests, storybook o
|
|
6
|
-
* una pantalla embebida que no ocupa la ventana entera.
|
|
7
|
-
*/
|
|
8
4
|
insets?: UIProviderProps['insets'];
|
|
9
|
-
/**
|
|
10
|
-
* Montar también el `<SafeAreaProvider>`. Ponelo en false si tu app ya tiene
|
|
11
|
-
* uno arriba (React Navigation lo monta solo en algunas plantillas):
|
|
12
|
-
* un provider anidado mide el frame de *su* View, no el de la ventana, así
|
|
13
|
-
* que ahí adentro los insets darían cero.
|
|
14
|
-
* @default true
|
|
15
|
-
*/
|
|
16
5
|
mountSafeAreaProvider?: boolean;
|
|
17
6
|
}
|
|
18
|
-
/**
|
|
19
|
-
* SafeAreaUIProvider
|
|
20
|
-
* `UIProvider` con los insets ya cableados desde `react-native-safe-area-context`.
|
|
21
|
-
*
|
|
22
|
-
* Vive en su propio subpath (`orn-ui/safe-area`) a propósito: es el único
|
|
23
|
-
* archivo de la librería que importa un paquete de terceros. Nada del entry
|
|
24
|
-
* principal lo referencia, así que quien hace `import { Button } from 'orn-ui'`
|
|
25
|
-
* nunca lo resuelve y el bundle sigue sin dependencias de runtime.
|
|
26
|
-
*
|
|
27
|
-
* ```tsx
|
|
28
|
-
* import { SafeAreaUIProvider } from 'orn-ui/safe-area';
|
|
29
|
-
*
|
|
30
|
-
* export default function App() {
|
|
31
|
-
* return (
|
|
32
|
-
* <SafeAreaUIProvider defaultMode="system">
|
|
33
|
-
* <RootNavigator />
|
|
34
|
-
* </SafeAreaUIProvider>
|
|
35
|
-
* );
|
|
36
|
-
* }
|
|
37
|
-
* ```
|
|
38
|
-
*
|
|
39
|
-
* El equivalente a mano — pasarle `insets={useSafeAreaInsets()}` a `UIProvider`
|
|
40
|
-
* desde un componente que ya esté dentro de `<SafeAreaProvider>` — sigue
|
|
41
|
-
* siendo válido. Esto sólo evita el error silencioso de olvidarlo: `insets`
|
|
42
|
-
* por defecto es `{0,0,0,0}`, y con ceros el bottom sheet queda pegado contra
|
|
43
|
-
* la barra de gestos y el modal `full` se mete abajo del notch.
|
|
44
|
-
*/
|
|
45
7
|
export declare function SafeAreaUIProvider({ mountSafeAreaProvider, ...props }: SafeAreaUIProviderProps): React.JSX.Element;
|
|
46
8
|
//# sourceMappingURL=SafeAreaUIProvider.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SafeAreaUIProvider.d.ts","sourceRoot":"","sources":["../../../src/safe-area/SafeAreaUIProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"SafeAreaUIProvider.d.ts","sourceRoot":"","sources":["../../../src/safe-area/SafeAreaUIProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAGnD,OAAO,EAIL,KAAK,eAAe,EACrB,MAAM,qBAAqB,CAAC;AAG7B,MAAM,WAAW,uBAAwB,SAAQ,IAAI,CAAC,eAAe,EAAE,QAAQ,CAAC;IAC9E,MAAM,CAAC,EAAE,eAAe,CAAC,QAAQ,CAAC,CAAC;IACnC,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AA+CD,wBAAgB,kBAAkB,CAAC,EACjC,qBAA4B,EAC5B,GAAG,KAAK,EACT,EAAE,uBAAuB,qBAOzB"}
|
|
@@ -5,6 +5,18 @@ import { type EdgeInsets } from './insets';
|
|
|
5
5
|
import type { Theme, ThemePair } from './types';
|
|
6
6
|
import type { IconRenderer } from '../icons/types';
|
|
7
7
|
export type ThemeMode = 'system' | 'light' | 'dark';
|
|
8
|
+
/**
|
|
9
|
+
* Un `<Modal>`/`<BottomSheet>` nativo abre su propia ventana (en Android
|
|
10
|
+
* siempre; en iOS según `presentationStyle`), así que los insets medidos en
|
|
11
|
+
* la raíz de la app pueden no valer ahí adentro. Este boundary es el punto de
|
|
12
|
+
* inyección: el default sólo repite los insets ambientes (comportamiento de
|
|
13
|
+
* siempre, cero deps), pero `SafeAreaUIProvider` lo reemplaza por uno que
|
|
14
|
+
* monta un `<SafeAreaProvider>` nuevo y remide dentro de esa ventana.
|
|
15
|
+
*/
|
|
16
|
+
export interface ModalSafeAreaBoundaryProps {
|
|
17
|
+
children: (insets: EdgeInsets) => React.ReactNode;
|
|
18
|
+
}
|
|
19
|
+
export type ModalSafeAreaBoundaryComponent = React.ComponentType<ModalSafeAreaBoundaryProps>;
|
|
8
20
|
export interface UIProviderProps {
|
|
9
21
|
children: React.ReactNode;
|
|
10
22
|
/** Par light/dark. Por defecto el theme de marca de la librería (créalo con createTheme()). */
|
|
@@ -21,6 +33,12 @@ export interface UIProviderProps {
|
|
|
21
33
|
labels?: Partial<Labels>;
|
|
22
34
|
/** @default false, para paridad con el resto de la librería (accesibilidad primero). */
|
|
23
35
|
allowFontScaling?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Fuente de insets que usan `Modal`/`BottomSheet` para su propia ventana
|
|
38
|
+
* nativa. No la pases a mano — `SafeAreaUIProvider` la cablea sola.
|
|
39
|
+
* @default repite los insets ambientes (comportamiento sin safe-area-context)
|
|
40
|
+
*/
|
|
41
|
+
ModalSafeAreaBoundary?: ModalSafeAreaBoundaryComponent;
|
|
24
42
|
}
|
|
25
43
|
/**
|
|
26
44
|
* UIProvider
|
|
@@ -29,7 +47,7 @@ export interface UIProviderProps {
|
|
|
29
47
|
* árbol. No persiste el modo — si el consumidor quiere recordar la preferencia
|
|
30
48
|
* entre sesiones, controla `mode`/`onModeChange` y persiste donde prefiera.
|
|
31
49
|
*/
|
|
32
|
-
export declare function UIProvider({ children, theme, defaultMode, mode: controlledMode, onModeChange, icons, insets, labels: labelOverrides, allowFontScaling, }: UIProviderProps): React.JSX.Element;
|
|
50
|
+
export declare function UIProvider({ children, theme, defaultMode, mode: controlledMode, onModeChange, icons, insets, labels: labelOverrides, allowFontScaling, ModalSafeAreaBoundary, }: UIProviderProps): React.JSX.Element;
|
|
33
51
|
export declare function useTheme(): Theme;
|
|
34
52
|
export declare function useColors(): Theme['colors'];
|
|
35
53
|
export declare function useThemeMode(): {
|
|
@@ -40,6 +58,8 @@ export declare function useIcons(): IconRenderer;
|
|
|
40
58
|
export declare function useInsets(): EdgeInsets;
|
|
41
59
|
export declare function useLabels(): Labels;
|
|
42
60
|
export declare function useAllowFontScaling(): boolean;
|
|
61
|
+
/** Interno: lo consumen `Modal`/`BottomSheet` para remedir insets dentro de su propia ventana nativa. */
|
|
62
|
+
export declare function useModalSafeAreaBoundary(): ModalSafeAreaBoundaryComponent;
|
|
43
63
|
/** Re-exportado para que tests/consumidores puedan simular cambios de sistema sin RN real. */
|
|
44
64
|
export declare const __testUtils: {
|
|
45
65
|
Appearance: typeof Appearance;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UIProvider.d.ts","sourceRoot":"","sources":["../../../src/theme/UIProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"UIProvider.d.ts","sourceRoot":"","sources":["../../../src/theme/UIProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4E,MAAM,OAAO,CAAC;AACjG,OAAO,EAAE,UAAU,EAAkB,MAAM,cAAc,CAAC;AAE1D,OAAO,EAAiB,KAAK,MAAM,EAAE,MAAM,UAAU,CAAC;AACtD,OAAO,EAAc,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AACvD,OAAO,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEhD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAC;AAEpD;;;;;;;GAOG;AACH,MAAM,WAAW,0BAA0B;IACzC,QAAQ,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,KAAK,CAAC,SAAS,CAAC;CACnD;AACD,MAAM,MAAM,8BAA8B,GAAG,KAAK,CAAC,aAAa,CAAC,0BAA0B,CAAC,CAAC;AAoB7F,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,+FAA+F;IAC/F,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,2EAA2E;IAC3E,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB,+FAA+F;IAC/F,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAC;IACzC,yEAAyE;IACzE,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,2EAA2E;IAC3E,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IACzB,wFAAwF;IACxF,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,8BAA8B,CAAC;CACxD;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,EACzB,QAAQ,EACR,KAAoB,EACpB,WAAsB,EACtB,IAAI,EAAE,cAAc,EACpB,YAAY,EACZ,KAAyB,EACzB,MAAmB,EACnB,MAAM,EAAE,cAAc,EACtB,gBAAwB,EACxB,qBAAoD,GACrD,EAAE,eAAe,qBAwDjB;AAUD,wBAAgB,QAAQ,IAAI,KAAK,CAEhC;AAED,wBAAgB,SAAS,IAAI,KAAK,CAAC,QAAQ,CAAC,CAE3C;AAED,wBAAgB,YAAY;;oBA9HV,SAAS,KAAK,IAAI;EAiInC;AAED,wBAAgB,QAAQ,IAAI,YAAY,CAEvC;AAED,wBAAgB,SAAS,IAAI,UAAU,CAEtC;AAED,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,wBAAgB,mBAAmB,IAAI,OAAO,CAE7C;AAED,yGAAyG;AACzG,wBAAgB,wBAAwB,IAAI,8BAA8B,CAEzE;AAED,8FAA8F;AAC9F,eAAO,MAAM,WAAW;;CAAiB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orn-ui",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Fast, tree-shakeable, atomic-design component library for React Native. Zero runtime dependencies.",
|
|
5
5
|
"author": "David Trujillo <davot098@gmail.com>",
|
|
6
6
|
"main": "lib/commonjs/index.js",
|
|
@@ -176,6 +176,11 @@
|
|
|
176
176
|
"react-native": "./src/molecules/Steps.tsx",
|
|
177
177
|
"default": "./lib/module/molecules/Steps.js"
|
|
178
178
|
},
|
|
179
|
+
"./symmetric-grid": {
|
|
180
|
+
"types": "./lib/typescript/molecules/SymmetricGrid.d.ts",
|
|
181
|
+
"react-native": "./src/molecules/SymmetricGrid.tsx",
|
|
182
|
+
"default": "./lib/module/molecules/SymmetricGrid.js"
|
|
183
|
+
},
|
|
179
184
|
"./modal": {
|
|
180
185
|
"types": "./lib/typescript/organisms/Modal.d.ts",
|
|
181
186
|
"react-native": "./src/organisms/Modal.tsx",
|
|
@@ -306,6 +311,11 @@
|
|
|
306
311
|
"react-native": "./src/organisms/NavigationBar.tsx",
|
|
307
312
|
"default": "./lib/module/organisms/NavigationBar.js"
|
|
308
313
|
},
|
|
314
|
+
"./reorderable-list": {
|
|
315
|
+
"types": "./lib/typescript/organisms/ReorderableList.d.ts",
|
|
316
|
+
"react-native": "./src/organisms/ReorderableList.tsx",
|
|
317
|
+
"default": "./lib/module/organisms/ReorderableList.js"
|
|
318
|
+
},
|
|
309
319
|
"./theme": {
|
|
310
320
|
"types": "./lib/typescript/theme/index.d.ts",
|
|
311
321
|
"react-native": "./src/theme/index.ts",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"files": [
|
|
14
14
|
{
|
|
15
15
|
"path": "organisms/AlertProvider.tsx",
|
|
16
|
-
"content": "import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';\nimport { Alert, type AlertButton, type AlertType } from './Alert';\nimport { createImperativeBridge } from './imperativeBridge';\n\nexport interface AlertOptions {\n title: string;\n message?: string;\n type?: AlertType;\n confirmText?: string;\n cancelText?: string;\n}\n\nexport interface ConfirmOptions extends AlertOptions {\n /** Botón de confirmar en rojo/destructivo (para \"eliminar\", etc). */\n destructive?: boolean;\n}\n\ninterface AlertContextValue {\n /** Muestra un alert de un solo botón. Resuelve cuando se cierra. */\n alert: (options: AlertOptions) => Promise<void>;\n /** Muestra confirmar/cancelar. Resuelve true si se confirmó, false si se canceló. */\n confirm: (options: ConfirmOptions) => Promise<boolean>;\n hide: () => void;\n}\n\nconst AlertContext = createContext<AlertContextValue | undefined>(undefined);\nconst bridge = createImperativeBridge<AlertContextValue>('AlertProvider');\n\n/**\n * Abre un alert desde fuera del árbol de React —un servicio, un interceptor—\n * sin pasar por el hook. Sin <AlertProvider> montado resuelve de inmediato, así\n * que un `await` nunca queda colgado.\n */\nexport function showAlert(options: AlertOptions): Promise<void> {\n return bridge.get()?.alert(options) ?? Promise.resolve();\n}\n\n/** Igual que showAlert, pero resuelve true/false. Sin provider, false. */\nexport function showConfirm(options: ConfirmOptions): Promise<boolean> {\n return bridge.get()?.confirm(options) ?? Promise.resolve(false);\n}\n\nexport function hideAlert(): void {\n bridge.get()?.hide();\n}\n\ninterface InternalState {\n visible: boolean;\n title: string;\n message?: string;\n type: AlertType;\n buttons: AlertButton[];\n}\n\nconst HIDDEN: InternalState = { visible: false, title: '', type: 'info', buttons: [] };\n\n/**\n * AlertProvider\n * Monta un único <Alert> controlado imperativamente. Colocar una vez cerca\n * de la raíz (dentro de UIProvider). Los handlers async pueden hacer\n * `const ok = await confirm({ title: 'Eliminar', destructive: true })`.\n */\nexport function AlertProvider({ children }: { children: React.ReactNode }) {\n const [state, setState] = useState<InternalState>(HIDDEN);\n const resolverRef = useRef<((value: any) => void) | null>(null);\n\n const
|
|
16
|
+
"content": "import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';\nimport { Alert, type AlertButton, type AlertType } from './Alert';\nimport { createImperativeBridge } from './imperativeBridge';\n\nexport interface AlertOptions {\n title: string;\n message?: string;\n type?: AlertType;\n confirmText?: string;\n cancelText?: string;\n}\n\nexport interface ConfirmOptions extends AlertOptions {\n /** Botón de confirmar en rojo/destructivo (para \"eliminar\", etc). */\n destructive?: boolean;\n}\n\ninterface AlertContextValue {\n /** Muestra un alert de un solo botón. Resuelve cuando se cierra. */\n alert: (options: AlertOptions) => Promise<void>;\n /** Muestra confirmar/cancelar. Resuelve true si se confirmó, false si se canceló. */\n confirm: (options: ConfirmOptions) => Promise<boolean>;\n hide: () => void;\n}\n\nconst AlertContext = createContext<AlertContextValue | undefined>(undefined);\nconst bridge = createImperativeBridge<AlertContextValue>('AlertProvider');\n\n/**\n * Abre un alert desde fuera del árbol de React —un servicio, un interceptor—\n * sin pasar por el hook. Sin <AlertProvider> montado resuelve de inmediato, así\n * que un `await` nunca queda colgado.\n */\nexport function showAlert(options: AlertOptions): Promise<void> {\n return bridge.get()?.alert(options) ?? Promise.resolve();\n}\n\n/** Igual que showAlert, pero resuelve true/false. Sin provider, false. */\nexport function showConfirm(options: ConfirmOptions): Promise<boolean> {\n return bridge.get()?.confirm(options) ?? Promise.resolve(false);\n}\n\nexport function hideAlert(): void {\n bridge.get()?.hide();\n}\n\ninterface InternalState {\n visible: boolean;\n title: string;\n message?: string;\n type: AlertType;\n buttons: AlertButton[];\n}\n\nconst HIDDEN: InternalState = { visible: false, title: '', type: 'info', buttons: [] };\n\n/**\n * AlertProvider\n * Monta un único <Alert> controlado imperativamente. Colocar una vez cerca\n * de la raíz (dentro de UIProvider). Los handlers async pueden hacer\n * `const ok = await confirm({ title: 'Eliminar', destructive: true })`.\n */\nexport function AlertProvider({ children }: { children: React.ReactNode }) {\n const [state, setState] = useState<InternalState>(HIDDEN);\n const resolverRef = useRef<((value: any) => void) | null>(null);\n // Si alert()/confirm() se llaman de nuevo mientras ya hay uno abierto, el\n // pedido anterior se resuelve (no se descarta) antes de mostrar el nuevo:\n // evita que su promise quede colgada para siempre.\n const settlePending = useCallback(() => {\n resolverRef.current?.(undefined);\n resolverRef.current = null;\n }, []);\n\n const hide = useCallback(() => {\n setState(HIDDEN);\n settlePending();\n }, [settlePending]);\n\n const alertFn = useCallback((options: AlertOptions): Promise<void> => {\n settlePending();\n return new Promise((resolve) => {\n resolverRef.current = resolve;\n setState({\n visible: true,\n title: options.title,\n message: options.message,\n type: options.type ?? 'info',\n buttons: [\n {\n text: options.confirmText ?? 'OK',\n onPress: () => {\n setState(HIDDEN);\n resolve();\n resolverRef.current = null;\n },\n },\n ],\n });\n });\n }, [settlePending]);\n\n const confirmFn = useCallback((options: ConfirmOptions): Promise<boolean> => {\n settlePending();\n return new Promise((resolve) => {\n resolverRef.current = resolve;\n setState({\n visible: true,\n title: options.title,\n message: options.message,\n type: options.type ?? 'question',\n buttons: [\n {\n text: options.cancelText ?? 'Cancel',\n style: 'cancel',\n variant: 'outline',\n onPress: () => {\n setState(HIDDEN);\n resolve(false);\n resolverRef.current = null;\n },\n },\n {\n text: options.confirmText ?? 'OK',\n variant: options.destructive ? 'destructive' : 'primary',\n style: options.destructive ? 'destructive' : 'default',\n onPress: () => {\n setState(HIDDEN);\n resolve(true);\n resolverRef.current = null;\n },\n },\n ],\n });\n });\n }, [settlePending]);\n\n const value = useMemo<AlertContextValue>(() => ({ alert: alertFn, confirm: confirmFn, hide }), [alertFn, confirmFn, hide]);\n\n useEffect(() => {\n bridge.set(value);\n return () => bridge.set(null);\n }, [value]);\n\n return (\n <AlertContext.Provider value={value}>\n {children}\n <Alert visible={state.visible} title={state.title} message={state.message} type={state.type} buttons={state.buttons} onClose={hide} />\n </AlertContext.Provider>\n );\n}\n\nexport function useAlert(): AlertContextValue {\n const ctx = useContext(AlertContext);\n if (!ctx) {\n throw new Error('orn-ui: useAlert must be used within an <AlertProvider>.');\n }\n return ctx;\n}\n"
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
19
|
"path": "organisms/imperativeBridge.ts",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
{
|
|
11
11
|
"path": "organisms/BottomSheet.tsx",
|
|
12
|
-
"content": "import React, { memo, useEffect, useRef, useState } from 'react';\nimport {\n Animated,\n Dimensions,\n Keyboard,\n KeyboardAvoidingView,\n Modal,\n PanResponder,\n Platform,\n Pressable,\n ScrollView,\n View,\n type ViewStyle,\n} from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { useColors,
|
|
12
|
+
"content": "import React, { memo, useEffect, useRef, useState } from 'react';\nimport {\n Animated,\n Dimensions,\n Keyboard,\n KeyboardAvoidingView,\n Modal,\n PanResponder,\n Platform,\n Pressable,\n ScrollView,\n View,\n type ViewStyle,\n} from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { useColors, useModalSafeAreaBoundary } from '../theme/UIProvider';\nimport { Title } from '../atoms/Text';\nimport { IconButton } from '../atoms/IconButton';\nimport { Transition } from '../atoms/Transition';\n\nconst { height: SCREEN_HEIGHT } = Dimensions.get('window');\nconst DISMISS_THRESHOLD = 100;\n\nexport type FooterPlacement = 'fixed' | 'scroll' | 'hide-with-keyboard';\n\nexport interface BottomSheetProps {\n visible: boolean;\n onClose: () => void;\n children: React.ReactNode;\n /** Con título aparece el header, con su botón de cerrar. */\n title?: string;\n /** Acciones de la hoja. Dónde vive lo decide `footerPlacement`. */\n footer?: React.ReactNode;\n /**\n * 'fixed' lo ancla al pie de la hoja; 'scroll' lo deja al final del\n * contenido; 'hide-with-keyboard' lo esconde mientras se escribe, que en un\n * formulario largo le devuelve esa franja al campo enfocado. @default 'fixed'\n */\n footerPlacement?: FooterPlacement;\n /** El contenido scrollea cuando no entra. @default true */\n scrollable?: boolean;\n /** Arrastrar la hoja hacia abajo la cierra. @default true */\n draggable?: boolean;\n /** Fracción de la pantalla (0-1) o píxeles. @default 0.9 */\n maxHeight?: number;\n closeAccessibilityLabel?: string;\n containerStyle?: ViewStyle;\n contentStyle?: ViewStyle;\n testID?: string;\n}\n\nconst useStyles = createStyles((theme) => ({\n container: { flex: 1, justifyContent: 'flex-end' },\n backdrop: { backgroundColor: theme.colors.overlay },\n sheet: {\n backgroundColor: theme.colors.surface,\n borderTopLeftRadius: theme.tokens.radius.xxl,\n borderTopRightRadius: theme.tokens.radius.xxl,\n ...theme.tokens.shadow.lg,\n },\n handleArea: { paddingVertical: theme.tokens.spacing.md, alignItems: 'center' },\n handle: { width: 40, height: 5, borderRadius: 3, backgroundColor: theme.colors.border },\n header: {\n flexDirection: 'row',\n alignItems: 'center',\n gap: theme.tokens.spacing.md,\n paddingHorizontal: theme.tokens.spacing.xl,\n paddingBottom: theme.tokens.spacing.md,\n },\n headerSpaced: { paddingTop: theme.tokens.spacing.xl },\n title: { flex: 1, fontSize: theme.tokens.fontSize.xl, fontWeight: theme.tokens.fontWeight.bold },\n content: { paddingHorizontal: theme.tokens.spacing.xl },\n scroll: { flexShrink: 1 },\n footer: {\n paddingHorizontal: theme.tokens.spacing.xl,\n paddingTop: theme.tokens.spacing.md,\n borderTopWidth: 1,\n borderTopColor: theme.colors.border,\n },\n footerInline: { paddingTop: theme.tokens.spacing.xl },\n}));\n\nconst absoluteFill: ViewStyle = { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 };\n\n/**\n * BottomSheet\n * Hoja que entra desde abajo, con arrastre para cerrar (PanResponder, no un\n * gesture-handler externo: la librería es cero dependencias). Header, footer y\n * scroll son opcionales, así que cubre tanto la hoja desnuda como el formulario\n * completo.\n */\nexport const BottomSheet = memo(\n ({\n visible,\n onClose,\n children,\n title,\n footer,\n footerPlacement = 'fixed',\n scrollable = true,\n draggable = true,\n maxHeight = 0.9,\n closeAccessibilityLabel = 'Close',\n containerStyle,\n contentStyle,\n testID,\n }: BottomSheetProps) => {\n const styles = useStyles();\n const colors = useColors();\n const ModalSafeAreaBoundary = useModalSafeAreaBoundary();\n const [mounted, setMounted] = useState(visible);\n const [keyboardOpen, setKeyboardOpen] = useState(false);\n const progress = useRef(new Animated.Value(0)).current;\n // El PanResponder de más abajo se crea una sola vez; leer `onClose`\n // directo del closure daría siempre el de ese primer render. El ref\n // refleja la última prop sin importar cuándo se creó la closure.\n const onCloseRef = useRef(onClose);\n onCloseRef.current = onClose;\n\n useEffect(() => {\n const animation = visible\n ? Animated.timing(progress, { toValue: 1, duration: 250, useNativeDriver: true })\n : Animated.timing(progress, { toValue: 0, duration: 200, useNativeDriver: true });\n if (visible) setMounted(true);\n animation.start(({ finished }) => {\n if (!visible && finished) setMounted(false);\n });\n // Si el efecto vuelve a correr (o el componente se desmonta) antes de\n // que termine, se corta: si no, un `finished` tardío podría llamar\n // `setMounted` sobre un componente ya desmontado.\n return () => animation.stop();\n }, [visible, progress]);\n\n useEffect(() => {\n if (footerPlacement !== 'hide-with-keyboard') return;\n const [show, hide] =\n Platform.OS === 'ios' ? ['keyboardWillShow', 'keyboardWillHide'] : ['keyboardDidShow', 'keyboardDidHide'];\n const subs = [\n Keyboard.addListener(show as 'keyboardDidShow', () => setKeyboardOpen(true)),\n Keyboard.addListener(hide as 'keyboardDidHide', () => setKeyboardOpen(false)),\n ];\n return () => subs.forEach((sub) => sub.remove());\n }, [footerPlacement]);\n\n const panResponder = useRef(\n PanResponder.create({\n onMoveShouldSetPanResponder: (_, gesture) => gesture.dy > 5 && Math.abs(gesture.dy) > Math.abs(gesture.dx),\n onPanResponderMove: (_, gesture) => {\n if (gesture.dy > 0) progress.setValue(1 - Math.min(gesture.dy, SCREEN_HEIGHT) / SCREEN_HEIGHT);\n },\n onPanResponderRelease: (_, gesture) => {\n if (gesture.dy > DISMISS_THRESHOLD) {\n onCloseRef.current();\n } else {\n Animated.timing(progress, { toValue: 1, duration: 150, useNativeDriver: true }).start();\n }\n },\n })\n ).current;\n\n if (!mounted) return null;\n\n // Porcentaje y no píxeles: se mide contra el contenedor, que con el teclado\n // abierto es más bajo, así que la hoja entra siempre entera.\n const resolvedMaxHeight: number | `${number}%` = maxHeight <= 1 ? `${maxHeight * 100}%` : maxHeight;\n const hasHeader = !!title;\n\n const scrolledFooter = !!footer && footerPlacement === 'scroll' && (\n <View style={styles.footerInline}>{footer}</View>\n );\n const pinnedFooter =\n !!footer && footerPlacement !== 'scroll' && !(footerPlacement === 'hide-with-keyboard' && keyboardOpen) ? (\n <View style={styles.footer}>{footer}</View>\n ) : null;\n\n const body = scrollable ? (\n <ScrollView\n style={styles.scroll}\n contentContainerStyle={[styles.content, contentStyle]}\n keyboardShouldPersistTaps=\"handled\"\n showsVerticalScrollIndicator={false}\n >\n {children}\n {scrolledFooter}\n </ScrollView>\n ) : (\n <View style={[styles.content, contentStyle]}>\n {children}\n {scrolledFooter}\n </View>\n );\n\n return (\n <Modal visible transparent animationType=\"none\" statusBarTranslucent onRequestClose={onClose}>\n {/* La hoja está anclada abajo, así que el teclado la tapa entera: hay\n que empujarla, no ajustar insets adentro del scroll. Una sola\n compensación, la del contenedor. */}\n <KeyboardAvoidingView\n style={styles.container}\n behavior={Platform.OS === 'ios' ? 'padding' : 'height'}\n >\n <Transition value={progress} preset=\"fade\" style={[absoluteFill, styles.backdrop]}>\n {/* Con header, el botón de cerrar ya expone la acción: repetir el\n label acá deja dos elementos con el mismo nombre. */}\n <Pressable\n style={absoluteFill}\n onPress={onClose}\n testID=\"bottom-sheet-backdrop\"\n accessibilityRole={hasHeader ? undefined : 'button'}\n accessibilityLabel={hasHeader ? undefined : closeAccessibilityLabel}\n />\n </Transition>\n\n {/* Insets medidos dentro de esta ventana nativa (el Modal nativo que\n usa BottomSheet por debajo), no los de la raíz de la app. */}\n <ModalSafeAreaBoundary>\n {(insets) => (\n <Transition\n value={progress}\n preset=\"slide-up\"\n distance={SCREEN_HEIGHT}\n testID={testID}\n style={[\n styles.sheet,\n { maxHeight: resolvedMaxHeight, paddingBottom: Math.max(insets.bottom, 20) },\n containerStyle,\n ]}\n >\n {draggable && (\n <View style={styles.handleArea} testID=\"bottom-sheet-handle\" {...panResponder.panHandlers}>\n <View style={styles.handle} />\n </View>\n )}\n\n {hasHeader && (\n <View style={[styles.header, !draggable && styles.headerSpaced]}>\n <Title style={styles.title} numberOfLines={1}>\n {title}\n </Title>\n <IconButton\n iconName=\"close\"\n size={24}\n color={colors.text}\n onPress={onClose}\n accessibilityLabel={closeAccessibilityLabel}\n />\n </View>\n )}\n\n {body}\n\n {pinnedFooter}\n </Transition>\n )}\n </ModalSafeAreaBoundary>\n </KeyboardAvoidingView>\n </Modal>\n );\n }\n);\nBottomSheet.displayName = 'BottomSheet';\n"
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"registryDependencies": [
|
package/registry/core.json
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
"path": "theme/UIProvider.tsx",
|
|
24
|
-
"content": "import React, { createContext, useContext, useMemo, useState, useCallback } from 'react';\nimport { Appearance, useColorScheme } from 'react-native';\nimport { defaultTheme } from './createTheme';\nimport { defaultLabels, type Labels } from './labels';\nimport { zeroInsets, type EdgeInsets } from './insets';\nimport type { Theme, ThemePair } from './types';\nimport { renderDefaultIcon } from '../icons/defaultIcons';\nimport type { IconRenderer } from '../icons/types';\n\nexport type ThemeMode = 'system' | 'light' | 'dark';\n\ninterface UIContextValue {\n theme: Theme;\n mode: ThemeMode;\n setMode: (mode: ThemeMode) => void;\n icons: IconRenderer;\n insets: EdgeInsets;\n labels: Labels;\n allowFontScaling: boolean;\n}\n\nconst UIContext = createContext<UIContextValue | undefined>(undefined);\n\
|
|
24
|
+
"content": "import React, { createContext, useContext, useMemo, useState, useCallback, useRef } from 'react';\nimport { Appearance, useColorScheme } from 'react-native';\nimport { defaultTheme } from './createTheme';\nimport { defaultLabels, type Labels } from './labels';\nimport { zeroInsets, type EdgeInsets } from './insets';\nimport type { Theme, ThemePair } from './types';\nimport { renderDefaultIcon } from '../icons/defaultIcons';\nimport type { IconRenderer } from '../icons/types';\n\nexport type ThemeMode = 'system' | 'light' | 'dark';\n\n/**\n * Un `<Modal>`/`<BottomSheet>` nativo abre su propia ventana (en Android\n * siempre; en iOS según `presentationStyle`), así que los insets medidos en\n * la raíz de la app pueden no valer ahí adentro. Este boundary es el punto de\n * inyección: el default sólo repite los insets ambientes (comportamiento de\n * siempre, cero deps), pero `SafeAreaUIProvider` lo reemplaza por uno que\n * monta un `<SafeAreaProvider>` nuevo y remide dentro de esa ventana.\n */\nexport interface ModalSafeAreaBoundaryProps {\n children: (insets: EdgeInsets) => React.ReactNode;\n}\nexport type ModalSafeAreaBoundaryComponent = React.ComponentType<ModalSafeAreaBoundaryProps>;\n\nfunction DefaultModalSafeAreaBoundary({ children }: ModalSafeAreaBoundaryProps) {\n const insets = useInsets();\n return <>{children(insets)}</>;\n}\n\ninterface UIContextValue {\n theme: Theme;\n mode: ThemeMode;\n setMode: (mode: ThemeMode) => void;\n icons: IconRenderer;\n insets: EdgeInsets;\n labels: Labels;\n allowFontScaling: boolean;\n ModalSafeAreaBoundary: ModalSafeAreaBoundaryComponent;\n}\n\nconst UIContext = createContext<UIContextValue | undefined>(undefined);\n\nexport interface UIProviderProps {\n children: React.ReactNode;\n /** Par light/dark. Por defecto el theme de marca de la librería (créalo con createTheme()). */\n theme?: ThemePair;\n /** Modo inicial (no controlado) si no se pasa `mode`. @default 'system' */\n defaultMode?: ThemeMode;\n /** Modo controlado. Si se pasa junto a onModeChange, el provider no gestiona estado propio. */\n mode?: ThemeMode;\n onModeChange?: (mode: ThemeMode) => void;\n /** Renderer de íconos. Por defecto usa los glifos zero-dep incluidos. */\n icons?: IconRenderer;\n /** Insets de safe area (de useSafeAreaInsets()). Por defecto {0,0,0,0}. */\n insets?: EdgeInsets;\n labels?: Partial<Labels>;\n /** @default false, para paridad con el resto de la librería (accesibilidad primero). */\n allowFontScaling?: boolean;\n /**\n * Fuente de insets que usan `Modal`/`BottomSheet` para su propia ventana\n * nativa. No la pases a mano — `SafeAreaUIProvider` la cablea sola.\n * @default repite los insets ambientes (comportamiento sin safe-area-context)\n */\n ModalSafeAreaBoundary?: ModalSafeAreaBoundaryComponent;\n}\n\n/**\n * UIProvider\n * Único punto de entrada de la librería: resuelve el theme activo (sistema u\n * override manual), e inyecta íconos, insets y textos por defecto a todo el\n * árbol. No persiste el modo — si el consumidor quiere recordar la preferencia\n * entre sesiones, controla `mode`/`onModeChange` y persiste donde prefiera.\n */\nexport function UIProvider({\n children,\n theme = defaultTheme,\n defaultMode = 'system',\n mode: controlledMode,\n onModeChange,\n icons = renderDefaultIcon,\n insets = zeroInsets,\n labels: labelOverrides,\n allowFontScaling = false,\n ModalSafeAreaBoundary = DefaultModalSafeAreaBoundary,\n}: UIProviderProps) {\n const systemScheme = useColorScheme();\n const [uncontrolledMode, setUncontrolledMode] = useState<ThemeMode>(defaultMode);\n const isControlled = controlledMode !== undefined;\n const mode = isControlled ? controlledMode : uncontrolledMode;\n\n const setMode = useCallback(\n (next: ThemeMode) => {\n if (!isControlled) setUncontrolledMode(next);\n onModeChange?.(next);\n },\n [isControlled, onModeChange]\n );\n\n // Por instancia, no por módulo: dos `<UIProvider>` en el mismo árbol (tests,\n // un modal con su propio provider) avisan cada uno si le faltan insets, en\n // vez de que el segundo se calle porque el primero ya avisó una vez.\n const warnedAboutInsets = useRef(false);\n\n // Olvidar `insets` no rompe nada visible al montar: el default de ceros\n // recién se nota cuando abrís un Modal `full` (se mete abajo del notch) o un\n // BottomSheet (queda pegado a la barra de gestos). El aviso llega antes.\n if (__DEV__ && insets === zeroInsets && !warnedAboutInsets.current) {\n warnedAboutInsets.current = true;\n console.warn(\n 'orn-ui: <UIProvider> sin `insets`, se usan {top:0,bottom:0,left:0,right:0}. ' +\n 'Los modales y el Screen quedan sin safe area. Pasá `insets={useSafeAreaInsets()}` ' +\n \"o usá <SafeAreaUIProvider> de 'orn-ui/safe-area', que lo cablea solo.\"\n );\n }\n\n const scheme: 'light' | 'dark' =\n mode === 'system' ? (systemScheme === 'dark' ? 'dark' : 'light') : mode;\n\n const resolvedTheme = theme[scheme];\n\n const labels = useMemo(\n () => (labelOverrides ? { ...defaultLabels, ...labelOverrides } : defaultLabels),\n [labelOverrides]\n );\n\n const value = useMemo<UIContextValue>(\n () => ({\n theme: resolvedTheme,\n mode,\n setMode,\n icons,\n insets,\n labels,\n allowFontScaling,\n ModalSafeAreaBoundary,\n }),\n [resolvedTheme, mode, setMode, icons, insets, labels, allowFontScaling, ModalSafeAreaBoundary]\n );\n\n return <UIContext.Provider value={value}>{children}</UIContext.Provider>;\n}\n\nfunction useUIContext(): UIContextValue {\n const ctx = useContext(UIContext);\n if (!ctx) {\n throw new Error('orn-ui: this hook must be used within a <UIProvider>.');\n }\n return ctx;\n}\n\nexport function useTheme(): Theme {\n return useUIContext().theme;\n}\n\nexport function useColors(): Theme['colors'] {\n return useUIContext().theme.colors;\n}\n\nexport function useThemeMode() {\n const { mode, setMode } = useUIContext();\n return { mode, setMode };\n}\n\nexport function useIcons(): IconRenderer {\n return useUIContext().icons;\n}\n\nexport function useInsets(): EdgeInsets {\n return useUIContext().insets;\n}\n\nexport function useLabels(): Labels {\n return useUIContext().labels;\n}\n\nexport function useAllowFontScaling(): boolean {\n return useUIContext().allowFontScaling;\n}\n\n/** Interno: lo consumen `Modal`/`BottomSheet` para remedir insets dentro de su propia ventana nativa. */\nexport function useModalSafeAreaBoundary(): ModalSafeAreaBoundaryComponent {\n return useUIContext().ModalSafeAreaBoundary;\n}\n\n/** Re-exportado para que tests/consumidores puedan simular cambios de sistema sin RN real. */\nexport const __testUtils = { Appearance };\n"
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
"path": "theme/colors.ts",
|
package/registry/index.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"core": {
|
|
3
|
-
"bytes":
|
|
3
|
+
"bytes": 37338
|
|
4
4
|
},
|
|
5
5
|
"components": [
|
|
6
6
|
{
|
|
@@ -301,7 +301,19 @@
|
|
|
301
301
|
"registryDependencies": [
|
|
302
302
|
"core"
|
|
303
303
|
],
|
|
304
|
-
"bytes":
|
|
304
|
+
"bytes": 8623
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
"name": "SymmetricGrid",
|
|
308
|
+
"slug": "symmetric-grid",
|
|
309
|
+
"category": "molecules",
|
|
310
|
+
"exportedNames": [
|
|
311
|
+
"SymmetricGrid"
|
|
312
|
+
],
|
|
313
|
+
"registryDependencies": [
|
|
314
|
+
"core"
|
|
315
|
+
],
|
|
316
|
+
"bytes": 2789
|
|
305
317
|
},
|
|
306
318
|
{
|
|
307
319
|
"name": "Modal",
|
|
@@ -317,7 +329,7 @@
|
|
|
317
329
|
"pressable-scale",
|
|
318
330
|
"transition"
|
|
319
331
|
],
|
|
320
|
-
"bytes":
|
|
332
|
+
"bytes": 9294
|
|
321
333
|
},
|
|
322
334
|
{
|
|
323
335
|
"name": "BottomSheet",
|
|
@@ -333,7 +345,7 @@
|
|
|
333
345
|
"pressable-scale",
|
|
334
346
|
"transition"
|
|
335
347
|
],
|
|
336
|
-
"bytes":
|
|
348
|
+
"bytes": 9679
|
|
337
349
|
},
|
|
338
350
|
{
|
|
339
351
|
"name": "Select",
|
|
@@ -376,7 +388,7 @@
|
|
|
376
388
|
"alert",
|
|
377
389
|
"button"
|
|
378
390
|
],
|
|
379
|
-
"bytes":
|
|
391
|
+
"bytes": 5834
|
|
380
392
|
},
|
|
381
393
|
{
|
|
382
394
|
"name": "Screen",
|
|
@@ -388,7 +400,7 @@
|
|
|
388
400
|
"registryDependencies": [
|
|
389
401
|
"core"
|
|
390
402
|
],
|
|
391
|
-
"bytes":
|
|
403
|
+
"bytes": 3237
|
|
392
404
|
},
|
|
393
405
|
{
|
|
394
406
|
"name": "List",
|
|
@@ -528,7 +540,19 @@
|
|
|
528
540
|
"registryDependencies": [
|
|
529
541
|
"core"
|
|
530
542
|
],
|
|
531
|
-
"bytes":
|
|
543
|
+
"bytes": 7875
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
"name": "ReorderableList",
|
|
547
|
+
"slug": "reorderable-list",
|
|
548
|
+
"category": "organisms",
|
|
549
|
+
"exportedNames": [
|
|
550
|
+
"ReorderableList"
|
|
551
|
+
],
|
|
552
|
+
"registryDependencies": [
|
|
553
|
+
"core"
|
|
554
|
+
],
|
|
555
|
+
"bytes": 5899
|
|
532
556
|
}
|
|
533
557
|
]
|
|
534
558
|
}
|
package/registry/manifest.json
CHANGED
|
@@ -178,6 +178,13 @@
|
|
|
178
178
|
"file": "molecules/Steps.tsx",
|
|
179
179
|
"slug": "steps"
|
|
180
180
|
},
|
|
181
|
+
{
|
|
182
|
+
"name": "SymmetricGrid",
|
|
183
|
+
"siblingNames": [],
|
|
184
|
+
"category": "molecules",
|
|
185
|
+
"file": "molecules/SymmetricGrid.tsx",
|
|
186
|
+
"slug": "symmetric-grid"
|
|
187
|
+
},
|
|
181
188
|
{
|
|
182
189
|
"name": "Modal",
|
|
183
190
|
"siblingNames": [],
|
|
@@ -296,6 +303,13 @@
|
|
|
296
303
|
"category": "organisms",
|
|
297
304
|
"file": "organisms/NavigationBar.tsx",
|
|
298
305
|
"slug": "navigation-bar"
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
"name": "ReorderableList",
|
|
309
|
+
"siblingNames": [],
|
|
310
|
+
"category": "organisms",
|
|
311
|
+
"file": "organisms/ReorderableList.tsx",
|
|
312
|
+
"slug": "reorderable-list"
|
|
299
313
|
}
|
|
300
314
|
]
|
|
301
315
|
}
|
package/registry/modal.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
{
|
|
11
11
|
"path": "organisms/Modal.tsx",
|
|
12
|
-
"content": "import React, { memo, useEffect, useState } from 'react';\nimport {\n Dimensions,\n KeyboardAvoidingView,\n Modal as RNModal,\n Platform,\n Pressable,\n ScrollView,\n View,\n type ModalProps as RNModalProps,\n type ViewStyle,\n} from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { useColors,
|
|
12
|
+
"content": "import React, { memo, useEffect, useState } from 'react';\nimport {\n Dimensions,\n KeyboardAvoidingView,\n Modal as RNModal,\n Platform,\n Pressable,\n ScrollView,\n View,\n type ModalProps as RNModalProps,\n type ViewStyle,\n} from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { useColors, useModalSafeAreaBoundary } from '../theme/UIProvider';\nimport type { EdgeInsets } from '../theme/insets';\nimport { Title } from '../atoms/Text';\nimport { IconButton } from '../atoms/IconButton';\nimport { Transition, type TransitionPreset } from '../atoms/Transition';\n\nconst { height: SCREEN_HEIGHT } = Dimensions.get('window');\n\nexport type ModalVariant = 'full' | 'overlay' | 'fullScreen';\n\nexport interface ModalProps\n extends Pick<RNModalProps, 'statusBarTranslucent' | 'onRequestClose' | 'hardwareAccelerated'> {\n visible: boolean;\n onClose?: () => void;\n title?: string;\n closeAccessibilityLabel?: string;\n children: React.ReactNode;\n footer?: React.ReactNode;\n containerStyle?: ViewStyle;\n contentStyle?: ViewStyle;\n variant?: ModalVariant;\n scrollable?: boolean;\n}\n\nconst useStyles = createStyles((theme) => ({\n flex1: { flex: 1 },\n fullWrapper: { flex: 1, backgroundColor: theme.colors.background },\n overlayContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: theme.tokens.spacing.xxl - 4 },\n backdrop: { backgroundColor: theme.colors.overlay },\n overlayCard: {\n width: '100%',\n maxHeight: '85%',\n backgroundColor: theme.colors.surface,\n borderRadius: theme.tokens.radius.xxl,\n overflow: 'hidden',\n ...theme.tokens.shadow.lg,\n },\n header: {\n flexDirection: 'row',\n justifyContent: 'space-between',\n alignItems: 'center',\n padding: theme.tokens.spacing.xl,\n borderBottomWidth: 1,\n borderBottomColor: theme.colors.border,\n backgroundColor: theme.colors.surface,\n },\n title: { fontSize: theme.tokens.fontSize.xl, fontWeight: theme.tokens.fontWeight.bold, flex: 1 },\n closeButton: { marginLeft: theme.tokens.spacing.md },\n contentWrapper: { flexShrink: 1, backgroundColor: theme.colors.surface },\n scrollWrapper: { flexShrink: 1 },\n scrollContent: { padding: theme.tokens.spacing.xl, flexGrow: 1 },\n scrollContentFull: { padding: 0, flexGrow: 1 },\n footer: {\n padding: theme.tokens.spacing.xl,\n borderTopWidth: 1,\n borderTopColor: theme.colors.border,\n backgroundColor: theme.colors.surface,\n },\n}));\n\n/**\n * Modal\n * Un solo componente para todos los modales de la app: maneja scroll,\n * teclado, insets y layout según `variant`. Para una hoja desde abajo está\n * BottomSheet, que además trae arrastre para cerrar.\n */\nexport const Modal = memo(\n ({\n visible,\n onClose,\n title,\n closeAccessibilityLabel = 'Close',\n children,\n footer,\n containerStyle,\n contentStyle,\n variant = 'full',\n scrollable = true,\n statusBarTranslucent = true,\n onRequestClose,\n hardwareAccelerated,\n }: ModalProps) => {\n const isFull = variant === 'full';\n const isOverlay = variant === 'overlay';\n const isFullScreen = variant === 'fullScreen';\n const isAnimatedVariant = isOverlay || isFull;\n const ModalSafeAreaBoundary = useModalSafeAreaBoundary();\n const colors = useColors();\n const styles = useStyles();\n\n const [mounted, setMounted] = useState(!isAnimatedVariant && visible);\n\n useEffect(() => {\n if (!isAnimatedVariant) setMounted(visible);\n else if (visible) setMounted(true);\n }, [visible, isAnimatedVariant]);\n\n // La tarjeta nunca anima opacity: hacerlo dispara un bug de Android donde\n // los hijos con elevation parpadean. El fondo semitransparente sí.\n const cardPreset: TransitionPreset[] = isFull ? ['slide-up'] : ['slide-down', 'scale'];\n const cardDistance = isFull ? SCREEN_HEIGHT : 40;\n\n const behavior = Platform.OS === 'ios' ? (isFull ? undefined : 'padding') : 'height';\n const keyboardOffset = Platform.OS === 'ios' ? 0 : 20;\n\n const renderHeader = () => {\n if (!title && !onClose) return null;\n return (\n <View style={styles.header}>\n {!!title && (\n <Title style={styles.title} numberOfLines={1}>\n {title}\n </Title>\n )}\n {!title && <View style={styles.flex1} />}\n {!!onClose && (\n <IconButton iconName=\"close\" size={24} color={colors.text} onPress={onClose} accessibilityLabel={closeAccessibilityLabel} style={styles.closeButton} />\n )}\n </View>\n );\n };\n\n const renderMainContent = () => (\n <View style={[styles.contentWrapper, isFull && styles.flex1]}>\n {scrollable ? (\n <ScrollView\n style={[styles.scrollWrapper, isFull && styles.flex1]}\n contentContainerStyle={[!isFull ? styles.scrollContent : styles.scrollContentFull, contentStyle]}\n keyboardShouldPersistTaps=\"handled\"\n showsVerticalScrollIndicator={false}\n bounces={Platform.OS === 'ios'}\n automaticallyAdjustKeyboardInsets\n >\n {children}\n </ScrollView>\n ) : (\n <View style={[!isFull ? styles.scrollContent : styles.scrollContentFull, isFull && styles.flex1, contentStyle]}>\n {children}\n </View>\n )}\n </View>\n );\n\n const renderFooter = () => (footer ? <View style={styles.footer}>{footer}</View> : null);\n\n if (!mounted) return null;\n\n return (\n <RNModal\n visible\n onRequestClose={onRequestClose ?? onClose}\n animationType={isAnimatedVariant ? 'none' : 'slide'}\n // `full` anima su propia entrada (translateY) con animationType=\"none\"\n // y necesita `transparent` para que no se vea un fondo negro detrás\n // mientras desliza. RN sólo admite `transparent` junto a\n // 'overFullScreen' — con 'pageSheet' avisa que la combinación no está\n // soportada. Por eso `full` usa overFullScreen: la presentación nativa\n // de pageSheet sería redundante con nuestra animación de todos modos.\n presentationStyle={\n Platform.OS === 'ios' ? (isFullScreen ? 'fullScreen' : 'overFullScreen') : undefined\n }\n transparent={isOverlay || isFull}\n statusBarTranslucent={statusBarTranslucent}\n hardwareAccelerated={hardwareAccelerated}\n >\n <KeyboardAvoidingView behavior={behavior} keyboardVerticalOffset={keyboardOffset} style={styles.flex1}>\n {/* Insets medidos dentro de esta ventana nativa del Modal, no los de\n la raíz de la app — pueden diferir (Android siempre abre una\n ventana propia; iOS según `presentationStyle`). */}\n <ModalSafeAreaBoundary>\n {(insets: EdgeInsets) =>\n isOverlay ? (\n <View style={styles.overlayContainer}>\n {/* Sin accessibilityLabel propio: el header ya expone un botón \"Close\"\n accesible; duplicar el label aquí generaría dos elementos con el\n mismo nombre para lectores de pantalla. Tocar afuera es un atajo\n puramente táctil. */}\n <Transition\n visible={visible}\n preset=\"fade\"\n duration={200}\n keepMounted\n style={[{ ...StyleSheetAbsoluteFill }, styles.backdrop]}\n >\n <Pressable style={StyleSheetAbsoluteFill} onPress={onClose} testID=\"modal-backdrop\" />\n </Transition>\n\n <Transition\n visible={visible}\n preset={cardPreset}\n distance={cardDistance}\n duration={250}\n onExited={() => setMounted(false)}\n style={[styles.overlayCard, containerStyle]}\n >\n {renderHeader()}\n {renderMainContent()}\n {renderFooter()}\n </Transition>\n </View>\n ) : isFull ? (\n <Transition\n visible={visible}\n preset={cardPreset}\n distance={cardDistance}\n duration={250}\n onExited={() => setMounted(false)}\n style={[styles.fullWrapper, containerStyle]}\n >\n <View style={[styles.flex1, { paddingTop: insets.top, paddingBottom: insets.bottom }]}>\n {renderHeader()}\n {renderMainContent()}\n {renderFooter()}\n </View>\n </Transition>\n ) : (\n <View style={[styles.fullWrapper, containerStyle]}>\n <View style={[styles.flex1, { paddingTop: insets.top, paddingBottom: insets.bottom }]}>\n {renderHeader()}\n {renderMainContent()}\n {renderFooter()}\n </View>\n </View>\n )\n }\n </ModalSafeAreaBoundary>\n </KeyboardAvoidingView>\n </RNModal>\n );\n }\n);\nModal.displayName = 'Modal';\n\nconst StyleSheetAbsoluteFill: ViewStyle = { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 };\n"
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"registryDependencies": [
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
{
|
|
11
11
|
"path": "organisms/NavigationBar.tsx",
|
|
12
|
-
"content": "import React, { memo, useEffect, useRef, useState } from 'react';\nimport {\n Animated,\n StyleSheet,\n Text,\n TouchableOpacity,\n View,\n type LayoutChangeEvent,\n type StyleProp,\n type ViewStyle,\n} from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { useAllowFontScaling, useColors, useInsets, useTheme } from '../theme/UIProvider';\nimport { Icon } from '../icons/Icon';\nimport type { IconName } from '../icons/types';\n\nexport interface NavigationBarItem {\n key: string;\n label: string;\n iconName?: IconName;\n /** Contador sobre el ícono. `true` pinta sólo el punto. */\n badge?: number | string | true;\n}\n\nexport interface NavigationBarProps {\n items: NavigationBarItem[];\n activeKey: string;\n onChange: (key: string) => void;\n /** Borde de la pantalla donde vive la barra. @default 'bottom' */\n position?: 'bottom' | 'top';\n /** @default true */\n showLabels?: boolean;\n /** Suma el inset de safe area del borde donde vive la barra. @default true */\n safeArea?: boolean;\n style?: StyleProp<ViewStyle>;\n testID?: string;\n}\n\nconst useStyles = createStyles((theme) => ({\n bar: {\n flexDirection: 'row',\n backgroundColor: theme.colors.surface,\n borderColor: theme.colors.border,\n paddingHorizontal: theme.tokens.spacing.xs,\n paddingVertical: theme.tokens.spacing.sm,\n },\n barBottom: { borderTopWidth: StyleSheet.hairlineWidth },\n barTop: { borderBottomWidth: StyleSheet.hairlineWidth },\n indicator: {\n position: 'absolute',\n top: theme.tokens.spacing.xs,\n bottom: theme.tokens.spacing.xs,\n left: theme.tokens.spacing.sm,\n borderRadius: theme.tokens.radius.lg,\n backgroundColor: theme.colors.primarySoft,\n },\n item: { flex: 1, paddingVertical: theme.tokens.spacing.xs },\n itemContent: { alignItems: 'center', justifyContent: 'center', gap: 2 },\n label: { fontSize: theme.tokens.fontSize.xs, fontWeight: theme.tokens.fontWeight.medium },\n badge: {\n position: 'absolute',\n top: -4,\n left: '50%',\n marginLeft: 4,\n minWidth: 16,\n height: 16,\n paddingHorizontal: 4,\n borderRadius: theme.tokens.radius.full,\n backgroundColor: theme.colors.error,\n alignItems: 'center',\n justifyContent: 'center',\n },\n badgeDot: { minWidth: 10, height: 10, top: -2, paddingHorizontal: 0 },\n badgeText: {\n fontSize: 10,\n fontWeight: theme.tokens.fontWeight.bold,\n color: theme.colors.onError,\n },\n}));\n\nconst ICON_SIZE = 22;\n\n/**\n * NavigationBar\n * Barra de navegación por pestañas con la misma apariencia y las mismas\n * transiciones en iOS y Android: el indicador se desliza hasta la pestaña\n * activa y su contenido crece y toma el color de acento con un cross-fade.\n * No navega por su cuenta — reporta la pestaña tocada y el consumidor decide.\n */\nexport const NavigationBar = memo(\n ({\n items,\n activeKey,\n onChange,\n position = 'bottom',\n showLabels = true,\n safeArea = true,\n style,\n testID,\n }: NavigationBarProps) => {\n const theme = useTheme();\n const colors = useColors();\n const styles = useStyles();\n const insets = useInsets();\n const allowFontScaling = useAllowFontScaling();\n\n const [barWidth, setBarWidth] = useState(0);\n const slide = useRef(new Animated.Value(0)).current;\n const placed = useRef(false);\n\n const activeIndex = Math.max(\n 0,\n items.findIndex((item) => item.key === activeKey)\n );\n const itemWidth = items.length > 0 ? barWidth / items.length : 0;\n\n useEffect(() => {\n if (itemWidth <= 0) return;\n const to = activeIndex * itemWidth;\n // En el primer layout el indicador aparece en su lugar; animarlo desde 0\n // sería un deslizamiento que el usuario no pidió.\n if (!placed.current) {\n placed.current = true;\n slide.setValue(to);\n return;\n }\n Animated.spring(slide, { toValue: to, useNativeDriver: true, friction: 9, tension: 90 }).start();\n }, [activeIndex, itemWidth, slide]);\n\n const onLayout = (e: LayoutChangeEvent) => {\n const next = e.nativeEvent.layout.width - theme.tokens.spacing.xs * 2;\n if (next !== barWidth) setBarWidth(next);\n };\n\n const renderContent = (item: NavigationBarItem, color: string) => (\n <>\n {!!item.iconName && <Icon name={item.iconName} size={ICON_SIZE} color={color} />}\n {showLabels && (\n <Text allowFontScaling={allowFontScaling} numberOfLines={1} style={[styles.label, { color }]}>\n {item.label}\n </Text>\n )}\n </>\n );\n\n return (\n <View\n testID={testID}\n onLayout={onLayout}\n accessibilityRole=\"tablist\"\n style={[\n styles.bar,\n position === 'bottom' ? styles.barBottom : styles.barTop,\n safeArea && position === 'bottom' && { paddingBottom: theme.tokens.spacing.sm + insets.bottom },\n safeArea && position === 'top' && { paddingTop: theme.tokens.spacing.sm + insets.top },\n style,\n ]}\n >\n {itemWidth > 0 && (\n <Animated.View\n testID=\"navigation-bar-indicator\"\n pointerEvents=\"none\"\n style={[\n styles.indicator,\n safeArea && position === 'bottom' && { bottom: theme.tokens.spacing.xs + insets.bottom },\n safeArea && position === 'top' && { top: theme.tokens.spacing.xs + insets.top },\n {\n width: itemWidth - theme.tokens.spacing.sm * 2,\n transform: [{ translateX: slide }],\n },\n ]}\n />\n )}\n\n {items.map((item, index) => {\n const active = index === activeIndex;\n const range = [(index - 1) * itemWidth, index * itemWidth, (index + 1) * itemWidth];\n const scale =\n itemWidth > 0\n ? slide.interpolate({ inputRange: range, outputRange: [1, 1.08, 1], extrapolate: 'clamp' })\n : 1;\n const activeOpacity =\n itemWidth > 0\n ? slide.interpolate({ inputRange: range, outputRange: [0, 1, 0], extrapolate: 'clamp' })\n : 0;\n\n return (\n <TouchableOpacity\n key={item.key}\n style={styles.item}\n activeOpacity={0.7}\n onPress={() => onChange(item.key)}\n accessibilityRole=\"tab\"\n accessibilityLabel={item.label}\n accessibilityState={{ selected: active }}\n >\n <Animated.View style={[styles.itemContent, { transform: [{ scale }] }]}>\n {renderContent(item, colors.textLight)}\n {/* Copia decorativa: sin ocultarla, el lector lee el label dos veces. */}\n <Animated.View\n pointerEvents=\"none\"\n accessibilityElementsHidden\n importantForAccessibility=\"no-hide-descendants\"\n style={[StyleSheet.absoluteFill, styles.itemContent, { opacity: activeOpacity }]}\n >\n {renderContent(item, colors.primaryText)}\n </Animated.View>\n\n {item.badge !== undefined && (\n <View style={[styles.badge, item.badge === true && styles.badgeDot]}>\n {item.badge !== true && (\n <Text allowFontScaling={false} style={styles.badgeText}>\n {item.badge}\n </Text>\n )}\n </View>\n )}\n </Animated.View>\n </TouchableOpacity>\n );\n })}\n </View>\n );\n }\n);\nNavigationBar.displayName = 'NavigationBar';\n"
|
|
12
|
+
"content": "import React, { memo, useEffect, useRef, useState } from 'react';\nimport {\n Animated,\n StyleSheet,\n Text,\n TouchableOpacity,\n View,\n type LayoutChangeEvent,\n type StyleProp,\n type ViewStyle,\n} from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { useAllowFontScaling, useColors, useInsets, useTheme } from '../theme/UIProvider';\nimport { Icon } from '../icons/Icon';\nimport type { IconName } from '../icons/types';\n\nexport interface NavigationBarItem {\n key: string;\n label: string;\n iconName?: IconName;\n /** Contador sobre el ícono. `true` pinta sólo el punto. */\n badge?: number | string | true;\n}\n\nexport interface NavigationBarProps {\n items: NavigationBarItem[];\n activeKey: string;\n onChange: (key: string) => void;\n /** Borde de la pantalla donde vive la barra. @default 'bottom' */\n position?: 'bottom' | 'top';\n /** @default true */\n showLabels?: boolean;\n /** Suma el inset de safe area del borde donde vive la barra. @default true */\n safeArea?: boolean;\n style?: StyleProp<ViewStyle>;\n testID?: string;\n}\n\nconst useStyles = createStyles((theme) => ({\n bar: {\n flexDirection: 'row',\n backgroundColor: theme.colors.surface,\n borderColor: theme.colors.border,\n paddingHorizontal: theme.tokens.spacing.xs,\n paddingVertical: theme.tokens.spacing.sm,\n },\n barBottom: { borderTopWidth: StyleSheet.hairlineWidth },\n barTop: { borderBottomWidth: StyleSheet.hairlineWidth },\n indicator: {\n position: 'absolute',\n top: theme.tokens.spacing.xs,\n bottom: theme.tokens.spacing.xs,\n // El indicador se posiciona contra el borde de la barra, no contra su caja\n // interna: al `left` hay que sumarle el padding horizontal para que quede\n // centrado sobre la pestaña y no corrido hacia la izquierda.\n left: theme.tokens.spacing.xs + theme.tokens.spacing.sm,\n borderRadius: theme.tokens.radius.lg,\n backgroundColor: theme.colors.primarySoft,\n },\n item: { flex: 1, paddingVertical: theme.tokens.spacing.xs },\n itemContent: { alignItems: 'center', justifyContent: 'center', gap: 2 },\n label: { fontSize: theme.tokens.fontSize.xs, fontWeight: theme.tokens.fontWeight.medium },\n badge: {\n position: 'absolute',\n top: -4,\n left: '50%',\n marginLeft: 4,\n minWidth: 16,\n height: 16,\n paddingHorizontal: 4,\n borderRadius: theme.tokens.radius.full,\n backgroundColor: theme.colors.error,\n alignItems: 'center',\n justifyContent: 'center',\n },\n badgeDot: { minWidth: 10, height: 10, top: -2, paddingHorizontal: 0 },\n badgeText: {\n fontSize: 10,\n fontWeight: theme.tokens.fontWeight.bold,\n color: theme.colors.onError,\n },\n}));\n\nconst ICON_SIZE = 22;\n\n/**\n * NavigationBar\n * Barra de navegación por pestañas con la misma apariencia y las mismas\n * transiciones en iOS y Android: el indicador se desliza hasta la pestaña\n * activa y su contenido crece y toma el color de acento con un cross-fade.\n * No navega por su cuenta — reporta la pestaña tocada y el consumidor decide.\n */\nexport const NavigationBar = memo(\n ({\n items,\n activeKey,\n onChange,\n position = 'bottom',\n showLabels = true,\n safeArea = true,\n style,\n testID,\n }: NavigationBarProps) => {\n const theme = useTheme();\n const colors = useColors();\n const styles = useStyles();\n const insets = useInsets();\n const allowFontScaling = useAllowFontScaling();\n\n const [barWidth, setBarWidth] = useState(0);\n const slide = useRef(new Animated.Value(0)).current;\n const placed = useRef(false);\n\n const activeIndex = Math.max(\n 0,\n items.findIndex((item) => item.key === activeKey)\n );\n const itemWidth = items.length > 0 ? barWidth / items.length : 0;\n\n useEffect(() => {\n if (itemWidth <= 0) return;\n const to = activeIndex * itemWidth;\n // En el primer layout el indicador aparece en su lugar; animarlo desde 0\n // sería un deslizamiento que el usuario no pidió.\n if (!placed.current) {\n placed.current = true;\n slide.setValue(to);\n return;\n }\n Animated.spring(slide, { toValue: to, useNativeDriver: true, friction: 9, tension: 90 }).start();\n }, [activeIndex, itemWidth, slide]);\n\n const onLayout = (e: LayoutChangeEvent) => {\n const next = e.nativeEvent.layout.width - theme.tokens.spacing.xs * 2;\n if (next !== barWidth) setBarWidth(next);\n };\n\n const renderContent = (item: NavigationBarItem, color: string) => (\n <>\n {!!item.iconName && <Icon name={item.iconName} size={ICON_SIZE} color={color} />}\n {showLabels && (\n <Text allowFontScaling={allowFontScaling} numberOfLines={1} style={[styles.label, { color }]}>\n {item.label}\n </Text>\n )}\n </>\n );\n\n return (\n <View\n testID={testID}\n onLayout={onLayout}\n accessibilityRole=\"tablist\"\n style={[\n styles.bar,\n position === 'bottom' ? styles.barBottom : styles.barTop,\n safeArea && position === 'bottom' && { paddingBottom: theme.tokens.spacing.sm + insets.bottom },\n safeArea && position === 'top' && { paddingTop: theme.tokens.spacing.sm + insets.top },\n style,\n ]}\n >\n {itemWidth > 0 && (\n <Animated.View\n testID=\"navigation-bar-indicator\"\n pointerEvents=\"none\"\n style={[\n styles.indicator,\n safeArea && position === 'bottom' && { bottom: theme.tokens.spacing.xs + insets.bottom },\n safeArea && position === 'top' && { top: theme.tokens.spacing.xs + insets.top },\n {\n width: itemWidth - theme.tokens.spacing.sm * 2,\n transform: [{ translateX: slide }],\n },\n ]}\n />\n )}\n\n {items.map((item, index) => {\n const active = index === activeIndex;\n const range = [(index - 1) * itemWidth, index * itemWidth, (index + 1) * itemWidth];\n const scale =\n itemWidth > 0\n ? slide.interpolate({ inputRange: range, outputRange: [1, 1.08, 1], extrapolate: 'clamp' })\n : 1;\n const activeOpacity =\n itemWidth > 0\n ? slide.interpolate({ inputRange: range, outputRange: [0, 1, 0], extrapolate: 'clamp' })\n : 0;\n\n return (\n <TouchableOpacity\n key={item.key}\n style={styles.item}\n activeOpacity={0.7}\n onPress={() => onChange(item.key)}\n accessibilityRole=\"tab\"\n accessibilityLabel={item.label}\n accessibilityState={{ selected: active }}\n >\n <Animated.View style={[styles.itemContent, { transform: [{ scale }] }]}>\n {renderContent(item, colors.textLight)}\n {/* Copia decorativa: sin ocultarla, el lector lee el label dos veces. */}\n <Animated.View\n pointerEvents=\"none\"\n accessibilityElementsHidden\n importantForAccessibility=\"no-hide-descendants\"\n style={[StyleSheet.absoluteFill, styles.itemContent, { opacity: activeOpacity }]}\n >\n {renderContent(item, colors.primaryText)}\n </Animated.View>\n\n {item.badge !== undefined && (\n <View style={[styles.badge, item.badge === true && styles.badgeDot]}>\n {item.badge !== true && (\n <Text allowFontScaling={false} style={styles.badgeText}>\n {item.badge}\n </Text>\n )}\n </View>\n )}\n </Animated.View>\n </TouchableOpacity>\n );\n })}\n </View>\n );\n }\n);\nNavigationBar.displayName = 'NavigationBar';\n"
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"registryDependencies": [
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ReorderableList",
|
|
3
|
+
"slug": "reorderable-list",
|
|
4
|
+
"type": "component",
|
|
5
|
+
"category": "organisms",
|
|
6
|
+
"exportedNames": [
|
|
7
|
+
"ReorderableList"
|
|
8
|
+
],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "organisms/ReorderableList.tsx",
|
|
12
|
+
"content": "import React, { useRef, useState } from 'react';\nimport { Animated, PanResponder, View, type StyleProp, type ViewStyle } from 'react-native';\nimport { createStyles } from '../theme/createStyles';\n\nexport interface ReorderableListProps<T> {\n data: T[];\n keyExtractor: (item: T, index: number) => string;\n /** Alto fijo de cada fila: necesario para calcular el índice destino sin medir layout async. */\n itemHeight: number;\n renderItem: (item: T, index: number, dragging: boolean) => React.ReactElement;\n /** Se llama al soltar, con el arreglo ya reordenado. No muta `data`. */\n onReorder: (data: T[]) => void;\n disabled?: boolean;\n style?: StyleProp<ViewStyle>;\n}\n\n/** Reordena moviendo el ítem de `from` a `to`, sin mutar `data`. Exportada aparte por ser testeable sin simular gestos. */\nexport function move<T>(data: T[], from: number, to: number): T[] {\n const next = data.slice();\n const [item] = next.splice(from, 1) as [T];\n next.splice(to, 0, item);\n return next;\n}\n\nconst useStyles = createStyles(() => ({\n row: { width: '100%' },\n}));\n\n/**\n * ReorderableList\n * Ley de destino común (common fate): al arrastrar una fila, las filas que\n * tiene que atravesar se desplazan juntas en el mismo sentido — eso es lo\n * que comunica \"van a intercambiar lugar\" antes de soltar el dedo, no un\n * simple resaltado del ítem activo.\n *\n * Sin dependencias de gestos/animación externas (reanimated, gesture-handler):\n * PanResponder + Animated alcanzan porque el cálculo de índice destino\n * necesita el valor de arrastre en JS en cada movimiento (no algo que\n * useNativeDriver pueda resolver solo).\n */\nexport function ReorderableList<T>({\n data,\n keyExtractor,\n itemHeight,\n renderItem,\n onReorder,\n disabled = false,\n style,\n}: ReorderableListProps<T>) {\n const styles = useStyles();\n const [activeIndex, setActiveIndex] = useState<number | null>(null);\n const [targetIndex, setTargetIndex] = useState<number | null>(null);\n const dy = useRef(new Animated.Value(0)).current;\n const activeIndexRef = useRef<number | null>(null);\n // El PanResponder de cada fila se crea una sola vez (se cachea en el Map de\n // abajo) y su closure queda fijo al render de esa primera vez: leer\n // `targetIndex` (state) directamente ahí siempre daría el valor del\n // montaje. Un ref sí refleja la última escritura sin importar cuándo se\n // creó la closure que lo lee.\n const targetIndexRef = useRef<number | null>(null);\n const dataRef = useRef(data);\n dataRef.current = data;\n // Igual razón que targetIndexRef: el responder de cada fila se crea una\n // sola vez, así que leer `disabled` directo del closure congelaría el\n // valor del momento en que esa fila se creó.\n const disabledRef = useRef(disabled);\n disabledRef.current = disabled;\n\n const panResponders = useRef(new Map<number, ReturnType<typeof PanResponder.create>>()).current;\n // Si `data` se achica, los responders de índices que ya no existen quedan\n // colgados en el Map para siempre: se podan en cada render.\n for (const index of panResponders.keys()) {\n if (index >= data.length) panResponders.delete(index);\n }\n\n function panResponderFor(index: number) {\n let responder = panResponders.get(index);\n if (responder) return responder;\n\n responder = PanResponder.create({\n onStartShouldSetPanResponder: () => !disabledRef.current,\n onMoveShouldSetPanResponder: (_, gesture) => !disabledRef.current && Math.abs(gesture.dy) > 2,\n onPanResponderGrant: () => {\n activeIndexRef.current = index;\n targetIndexRef.current = index;\n setActiveIndex(index);\n setTargetIndex(index);\n dy.setValue(0);\n },\n onPanResponderMove: (_, gesture) => {\n dy.setValue(gesture.dy);\n const from = activeIndexRef.current;\n if (from === null) return;\n const rawTarget = from + Math.round(gesture.dy / itemHeight);\n const clamped = Math.max(0, Math.min(dataRef.current.length - 1, rawTarget));\n targetIndexRef.current = clamped;\n setTargetIndex((prev) => (prev === clamped ? prev : clamped));\n },\n onPanResponderRelease: () => {\n const from = activeIndexRef.current;\n const to = targetIndexRef.current;\n dy.setValue(0);\n activeIndexRef.current = null;\n targetIndexRef.current = null;\n setActiveIndex(null);\n setTargetIndex(null);\n if (from === null || to === null || from === to) return;\n onReorder(move(dataRef.current, from, to));\n },\n onPanResponderTerminate: () => {\n dy.setValue(0);\n activeIndexRef.current = null;\n targetIndexRef.current = null;\n setActiveIndex(null);\n setTargetIndex(null);\n },\n });\n panResponders.set(index, responder);\n return responder;\n }\n\n return (\n <View style={style}>\n {data.map((item, index) => {\n const isActive = activeIndex === index;\n // Filas entre el origen y el destino se corren un lugar entero en el\n // sentido del arrastre: es el efecto de \"grupo que se mueve junto\".\n let shift = 0;\n if (activeIndex !== null && targetIndex !== null && !isActive) {\n if (activeIndex < targetIndex && index > activeIndex && index <= targetIndex) shift = -itemHeight;\n else if (activeIndex > targetIndex && index < activeIndex && index >= targetIndex) shift = itemHeight;\n }\n\n return (\n <Animated.View\n key={keyExtractor(item, index)}\n style={[\n styles.row,\n isActive\n ? { transform: [{ translateY: dy }], zIndex: 1, elevation: 4 }\n : { transform: [{ translateY: shift }] },\n ]}\n {...panResponderFor(index).panHandlers}\n >\n {renderItem(item, index, isActive)}\n </Animated.View>\n );\n })}\n </View>\n );\n}\n"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [
|
|
16
|
+
"core"
|
|
17
|
+
]
|
|
18
|
+
}
|
package/registry/screen.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
{
|
|
11
11
|
"path": "organisms/Screen.tsx",
|
|
12
|
-
"content": "import React, { memo } from 'react';\nimport { KeyboardAvoidingView, Platform, ScrollView, View, type ViewStyle } from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { useInsets } from '../theme/UIProvider';\n\nexport type ScreenEdge = 'top' | 'bottom';\n\nexport interface ScreenProps {\n children: React.ReactNode;\n style?: ViewStyle;\n scrollable?: boolean;\n contentContainerStyle?: ViewStyle;\n /** Qué insets de safe area aplicar. @default ['top', 'bottom'] */\n edges?: ScreenEdge[];\n /**\n * Compensa el teclado: en iOS con scroll lo hace el propio ScrollView, en el\n * resto de los casos con un KeyboardAvoidingView. Ponelo en false cuando\n * el contenido ya trae su propio scroller que maneja el teclado (Wizard,\n * List, SearchList): las dos compensaciones se suman y el campo enfocado\n * termina empujado fuera de la pantalla.\n * @default true\n */\n keyboardAvoiding?: boolean;\n}\n\nconst useStyles = createStyles((theme) => ({\n wrapper: { flex: 1, backgroundColor: theme.colors.background },\n container: { flex: 1 },\n contentContainer: { flexGrow: 1 },\n}));\n\n/**\n * Screen\n * Contenedor raíz de pantalla: fondo del theme, safe area (via insets del\n * UIProvider), teclado y scroll opcional resueltos de forma consistente.\n */\nexport const Screen = memo(({\n children,\n style,\n scrollable = true,\n contentContainerStyle,\n edges = ['top', 'bottom'],\n keyboardAvoiding = true,\n}: ScreenProps) => {\n const insets = useInsets();\n const styles = useStyles();\n const applyTop = edges.includes('top');\n const applyBottom = edges.includes('bottom');\n\n // En iOS el ScrollView de abajo ya sube el campo enfocado con\n // automaticallyAdjustKeyboardInsets; envolverlo además en un\n // KeyboardAvoidingView compensa el teclado dos veces y el input termina\n // empujado fuera del borde superior. Android no tiene ese prop, así que ahí\n // el KeyboardAvoidingView sigue siendo el único mecanismo.\n const avoidWithView = keyboardAvoiding && !(scrollable && Platform.OS === 'ios');\n\n const KeyboardWrapper: React.ComponentType<any> = avoidWithView ? KeyboardAvoidingView : View;\n const keyboardProps = avoidWithView\n ? {\n behavior: Platform.OS === 'ios' ? ('padding' as const) : ('height' as const),\n keyboardVerticalOffset: Platform.OS === 'ios' ? 0 : 20,\n }\n : {};\n\n return (\n <View style={[styles.wrapper, style]}>\n <KeyboardWrapper style={styles.container} {...keyboardProps}>\n {scrollable ? (\n <ScrollView\n style={styles.container}\n
|
|
12
|
+
"content": "import React, { memo } from 'react';\nimport { KeyboardAvoidingView, Platform, ScrollView, View, type ViewStyle } from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { useInsets } from '../theme/UIProvider';\n\nexport type ScreenEdge = 'top' | 'bottom';\n\nexport interface ScreenProps {\n children: React.ReactNode;\n style?: ViewStyle;\n scrollable?: boolean;\n contentContainerStyle?: ViewStyle;\n /** Qué insets de safe area aplicar. @default ['top', 'bottom'] */\n edges?: ScreenEdge[];\n /**\n * Compensa el teclado: en iOS con scroll lo hace el propio ScrollView, en el\n * resto de los casos con un KeyboardAvoidingView. Ponelo en false cuando\n * el contenido ya trae su propio scroller que maneja el teclado (Wizard,\n * List, SearchList): las dos compensaciones se suman y el campo enfocado\n * termina empujado fuera de la pantalla.\n * @default true\n */\n keyboardAvoiding?: boolean;\n}\n\nconst useStyles = createStyles((theme) => ({\n wrapper: { flex: 1, backgroundColor: theme.colors.background },\n container: { flex: 1 },\n contentContainer: { flexGrow: 1 },\n}));\n\n/**\n * Screen\n * Contenedor raíz de pantalla: fondo del theme, safe area (via insets del\n * UIProvider), teclado y scroll opcional resueltos de forma consistente.\n */\nexport const Screen = memo(({\n children,\n style,\n scrollable = true,\n contentContainerStyle,\n edges = ['top', 'bottom'],\n keyboardAvoiding = true,\n}: ScreenProps) => {\n const insets = useInsets();\n const styles = useStyles();\n const applyTop = edges.includes('top');\n const applyBottom = edges.includes('bottom');\n\n // En iOS el ScrollView de abajo ya sube el campo enfocado con\n // automaticallyAdjustKeyboardInsets; envolverlo además en un\n // KeyboardAvoidingView compensa el teclado dos veces y el input termina\n // empujado fuera del borde superior. Android no tiene ese prop, así que ahí\n // el KeyboardAvoidingView sigue siendo el único mecanismo.\n const avoidWithView = keyboardAvoiding && !(scrollable && Platform.OS === 'ios');\n\n const KeyboardWrapper: React.ComponentType<any> = avoidWithView ? KeyboardAvoidingView : View;\n const keyboardProps = avoidWithView\n ? {\n behavior: Platform.OS === 'ios' ? ('padding' as const) : ('height' as const),\n keyboardVerticalOffset: Platform.OS === 'ios' ? 0 : 20,\n }\n : {};\n\n return (\n <View style={[styles.wrapper, style]}>\n <KeyboardWrapper style={styles.container} {...keyboardProps}>\n {scrollable ? (\n <ScrollView\n style={styles.container}\n automaticallyAdjustKeyboardInsets\n contentContainerStyle={[\n styles.contentContainer,\n { paddingTop: applyTop ? insets.top : 0 },\n applyBottom && { paddingBottom: insets.bottom },\n contentContainerStyle,\n ]}\n keyboardShouldPersistTaps=\"handled\"\n >\n {children}\n </ScrollView>\n ) : (\n <View style={[styles.container, { paddingTop: applyTop ? insets.top : 0, paddingBottom: applyBottom ? insets.bottom : 0 }]}>\n {children}\n </View>\n )}\n </KeyboardWrapper>\n </View>\n );\n});\nScreen.displayName = 'Screen';\n"
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"registryDependencies": [
|