@praxiis/ui 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +52556 -0
- package/dist/index.d.ts +52556 -0
- package/dist/index.js +8753 -0
- package/dist/index.mjs +8777 -0
- package/package.json +70 -0
- package/src/__test-utils__/index.tsx +39 -0
- package/src/components/CalendarStrip/CalendarStrip.helpers.ts +106 -0
- package/src/components/CalendarStrip/CalendarStrip.tsx +83 -0
- package/src/components/CalendarStrip/CalendarStrip.types.ts +133 -0
- package/src/components/CalendarStrip/DayCard/DayCard.helpers.ts +44 -0
- package/src/components/CalendarStrip/DayCard/DayCard.tsx +71 -0
- package/src/components/CalendarStrip/DayCard/DayCard.types.ts +134 -0
- package/src/components/CalendarStrip/DayCard/index.ts +2 -0
- package/src/components/CalendarStrip/DayCard/useDayCardLogic.ts +45 -0
- package/src/components/CalendarStrip/index.ts +9 -0
- package/src/components/CalendarStrip/useCalendarStripLogic.ts +53 -0
- package/src/components/EmptyState/EmptyState.helpers.ts +104 -0
- package/src/components/EmptyState/EmptyState.tsx +205 -0
- package/src/components/EmptyState/EmptyState.types.ts +213 -0
- package/src/components/EmptyState/index.ts +44 -0
- package/src/components/EmptyState/useEmptyStateLogic.ts +131 -0
- package/src/components/Header/Header.helpers.ts +93 -0
- package/src/components/Header/Header.tsx +185 -0
- package/src/components/Header/Header.types.ts +153 -0
- package/src/components/Header/index.ts +44 -0
- package/src/components/Header/useHeaderLogic.ts +146 -0
- package/src/components/ScheduleItem/ScheduleItem/ScheduleItem.helpers.ts +50 -0
- package/src/components/ScheduleItem/ScheduleItem/ScheduleItem.tsx +78 -0
- package/src/components/ScheduleItem/ScheduleItem/ScheduleItem.types.ts +99 -0
- package/src/components/ScheduleItem/ScheduleItem/index.ts +16 -0
- package/src/components/ScheduleItem/ScheduleItem/useScheduleItemLogic.ts +31 -0
- package/src/components/ScheduleItem/index.ts +15 -0
- package/src/components/index.ts +40 -0
- package/src/core/index.ts +34 -0
- package/src/core/restyle/RestyleThemeProviderWrapper.tsx +31 -0
- package/src/core/restyle/index.ts +38 -0
- package/src/core/restyle/restylePresetRegistry.ts +195 -0
- package/src/core/restyle/restyleTheme.ts +1352 -0
- package/src/core/restyle/restyleTypes.ts +8 -0
- package/src/core/restyle/useRestyleTheme.ts +10 -0
- package/src/hooks/animations/index.ts +3 -0
- package/src/hooks/animations/useAnimatedValue.ts +10 -0
- package/src/hooks/animations/useEntranceAnimation.ts +106 -0
- package/src/hooks/animations/usePulseAnimation.ts +63 -0
- package/src/hooks/index.ts +30 -0
- package/src/hooks/useReducedMotion.ts +60 -0
- package/src/i18n/index.ts +2 -0
- package/src/i18n/labels/en.ts +120 -0
- package/src/i18n/labels/es.ts +120 -0
- package/src/i18n/labels/index.ts +6 -0
- package/src/i18n/labels/types.ts +165 -0
- package/src/index.tsx +215 -0
- package/src/primitives/actions/Button/Button.helpers.ts +243 -0
- package/src/primitives/actions/Button/Button.tsx +198 -0
- package/src/primitives/actions/Button/Button.types.ts +207 -0
- package/src/primitives/actions/Button/index.ts +41 -0
- package/src/primitives/actions/Button/useButtonLogic.ts +160 -0
- package/src/primitives/actions/IconButton/IconButton.helpers.ts +235 -0
- package/src/primitives/actions/IconButton/IconButton.tsx +177 -0
- package/src/primitives/actions/IconButton/IconButton.types.ts +273 -0
- package/src/primitives/actions/IconButton/index.ts +30 -0
- package/src/primitives/actions/IconButton/useIconButtonLogic.ts +172 -0
- package/src/primitives/actions/index.ts +20 -0
- package/src/primitives/content/Avatar/Avatar.helpers.ts +177 -0
- package/src/primitives/content/Avatar/Avatar.tsx +199 -0
- package/src/primitives/content/Avatar/Avatar.types.ts +222 -0
- package/src/primitives/content/Avatar/index.ts +46 -0
- package/src/primitives/content/Avatar/useAvatarLogic.ts +149 -0
- package/src/primitives/content/Badge/Badge.helpers.ts +175 -0
- package/src/primitives/content/Badge/Badge.tsx +174 -0
- package/src/primitives/content/Badge/Badge.types.ts +223 -0
- package/src/primitives/content/Badge/index.ts +40 -0
- package/src/primitives/content/Badge/useBadgeLogic.ts +128 -0
- package/src/primitives/content/Card/Card.helpers.ts +27 -0
- package/src/primitives/content/Card/Card.tsx +123 -0
- package/src/primitives/content/Card/Card.types.ts +95 -0
- package/src/primitives/content/Card/index.ts +20 -0
- package/src/primitives/content/Card/useCardLogic.ts +48 -0
- package/src/primitives/content/Chip/Chip.helpers.ts +304 -0
- package/src/primitives/content/Chip/Chip.tsx +205 -0
- package/src/primitives/content/Chip/Chip.types.ts +234 -0
- package/src/primitives/content/Chip/index.ts +47 -0
- package/src/primitives/content/Chip/useChipLogic.ts +167 -0
- package/src/primitives/content/Icon/Icon.helpers.ts +54 -0
- package/src/primitives/content/Icon/Icon.tsx +110 -0
- package/src/primitives/content/Icon/Icon.types.ts +95 -0
- package/src/primitives/content/Icon/index.ts +20 -0
- package/src/primitives/content/Icon/useIconLogic.ts +73 -0
- package/src/primitives/content/index.ts +45 -0
- package/src/primitives/feedback/ProgressBar/ProgressBar.helpers.ts +122 -0
- package/src/primitives/feedback/ProgressBar/ProgressBar.tsx +154 -0
- package/src/primitives/feedback/ProgressBar/ProgressBar.types.ts +178 -0
- package/src/primitives/feedback/ProgressBar/index.ts +17 -0
- package/src/primitives/feedback/ProgressBar/useProgressBarLogic.ts +120 -0
- package/src/primitives/feedback/Skeleton/Skeleton.helpers.ts +145 -0
- package/src/primitives/feedback/Skeleton/Skeleton.tsx +155 -0
- package/src/primitives/feedback/Skeleton/Skeleton.types.ts +223 -0
- package/src/primitives/feedback/Skeleton/index.ts +44 -0
- package/src/primitives/feedback/Skeleton/useSkeletonLogic.ts +125 -0
- package/src/primitives/feedback/Spinner/Spinner.helpers.ts +40 -0
- package/src/primitives/feedback/Spinner/Spinner.tsx +105 -0
- package/src/primitives/feedback/Spinner/Spinner.types.ts +114 -0
- package/src/primitives/feedback/Spinner/index.ts +18 -0
- package/src/primitives/feedback/Spinner/useSpinnerLogic.ts +84 -0
- package/src/primitives/feedback/Toast/Toast.helpers.ts +163 -0
- package/src/primitives/feedback/Toast/Toast.tsx +190 -0
- package/src/primitives/feedback/Toast/Toast.types.ts +270 -0
- package/src/primitives/feedback/Toast/ToastContext.tsx +96 -0
- package/src/primitives/feedback/Toast/ToastProvider.tsx +241 -0
- package/src/primitives/feedback/Toast/index.ts +59 -0
- package/src/primitives/feedback/Toast/useToastLogic.ts +112 -0
- package/src/primitives/feedback/index.ts +45 -0
- package/src/primitives/index.ts +158 -0
- package/src/primitives/inputs/Checkbox/Checkbox.helpers.ts +132 -0
- package/src/primitives/inputs/Checkbox/Checkbox.tsx +150 -0
- package/src/primitives/inputs/Checkbox/Checkbox.types.ts +106 -0
- package/src/primitives/inputs/Checkbox/index.ts +30 -0
- package/src/primitives/inputs/Checkbox/useCheckboxLogic.ts +121 -0
- package/src/primitives/inputs/RadioButton/RadioButton.helpers.ts +123 -0
- package/src/primitives/inputs/RadioButton/RadioButton.tsx +159 -0
- package/src/primitives/inputs/RadioButton/RadioButton.types.ts +106 -0
- package/src/primitives/inputs/RadioButton/index.ts +25 -0
- package/src/primitives/inputs/RadioButton/useRadioButtonLogic.ts +117 -0
- package/src/primitives/inputs/SegmentedControl/SegmentedControl.helpers.ts +174 -0
- package/src/primitives/inputs/SegmentedControl/SegmentedControl.tsx +224 -0
- package/src/primitives/inputs/SegmentedControl/SegmentedControl.types.ts +187 -0
- package/src/primitives/inputs/SegmentedControl/index.ts +39 -0
- package/src/primitives/inputs/SegmentedControl/useSegmentedControlLogic.ts +151 -0
- package/src/primitives/inputs/SelectSheet/SelectSheet.helpers.ts +147 -0
- package/src/primitives/inputs/SelectSheet/SelectSheet.tsx +247 -0
- package/src/primitives/inputs/SelectSheet/SelectSheet.types.ts +196 -0
- package/src/primitives/inputs/SelectSheet/SelectSheetOption.tsx +177 -0
- package/src/primitives/inputs/SelectSheet/index.ts +48 -0
- package/src/primitives/inputs/SelectSheet/useSelectSheetLogic.ts +309 -0
- package/src/primitives/inputs/Switch/Switch.helpers.ts +109 -0
- package/src/primitives/inputs/Switch/Switch.tsx +191 -0
- package/src/primitives/inputs/Switch/Switch.types.ts +154 -0
- package/src/primitives/inputs/Switch/index.ts +40 -0
- package/src/primitives/inputs/Switch/useSwitchLogic.ts +192 -0
- package/src/primitives/inputs/TextInput/TextInput.helpers.ts +206 -0
- package/src/primitives/inputs/TextInput/TextInput.tsx +392 -0
- package/src/primitives/inputs/TextInput/TextInput.types.ts +216 -0
- package/src/primitives/inputs/TextInput/index.ts +37 -0
- package/src/primitives/inputs/TextInput/useTextInputLogic.ts +195 -0
- package/src/primitives/inputs/index.ts +52 -0
- package/src/primitives/layout/AnimatedBox.tsx +44 -0
- package/src/primitives/layout/Box.tsx +71 -0
- package/src/primitives/layout/Divider/Divider.helpers.ts +115 -0
- package/src/primitives/layout/Divider/Divider.tsx +139 -0
- package/src/primitives/layout/Divider/Divider.types.ts +178 -0
- package/src/primitives/layout/Divider/index.ts +24 -0
- package/src/primitives/layout/Divider/useDividerLogic.ts +109 -0
- package/src/primitives/layout/FlatList.tsx +66 -0
- package/src/primitives/layout/Pressable.tsx +74 -0
- package/src/primitives/layout/ScrollView.tsx +63 -0
- package/src/primitives/layout/Stack.tsx +69 -0
- package/src/primitives/layout/index.ts +40 -0
- package/src/primitives/navigation/index.ts +6 -0
- package/src/primitives/overlays/Modal/Modal.helpers.ts +31 -0
- package/src/primitives/overlays/Modal/Modal.tsx +264 -0
- package/src/primitives/overlays/Modal/Modal.types.ts +193 -0
- package/src/primitives/overlays/Modal/index.ts +43 -0
- package/src/primitives/overlays/Modal/useModalLogic.ts +103 -0
- package/src/primitives/overlays/index.ts +12 -0
- package/src/primitives/typography/Text.tsx +51 -0
- package/src/primitives/typography/index.ts +1 -0
- package/src/provider/DesignSystemContext.ts +22 -0
- package/src/provider/DesignSystemProvider.tsx +121 -0
- package/src/provider/index.ts +7 -0
- package/src/providers/ThemeProvider/createTheme.ts +304 -0
- package/src/providers/ThemeProvider/defaultTheme.ts +70 -0
- package/src/providers/ThemeProvider/index.ts +34 -0
- package/src/providers/ThemeProvider/types.ts +249 -0
- package/src/providers/index.ts +29 -0
- package/src/tokens/colors.ts +371 -0
- package/src/tokens/index.ts +145 -0
- package/src/tokens/motion.ts +176 -0
- package/src/tokens/radii.ts +82 -0
- package/src/tokens/scales.ts +588 -0
- package/src/tokens/shadows.ts +190 -0
- package/src/tokens/spacing.ts +140 -0
- package/src/tokens/tokens.json +207 -0
- package/src/tokens/typography.ts +251 -0
- package/src/types.ts +50 -0
- package/src/utils/accessibility.ts +169 -0
- package/src/utils/index.ts +25 -0
- package/src/utils/platform.ts +72 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import React, { forwardRef } from "react";
|
|
2
|
+
import {
|
|
3
|
+
ScrollView as RNScrollView,
|
|
4
|
+
ScrollViewProps as RNScrollViewProps,
|
|
5
|
+
} from "react-native";
|
|
6
|
+
import {
|
|
7
|
+
backgroundColor,
|
|
8
|
+
BackgroundColorProps,
|
|
9
|
+
composeRestyleFunctions,
|
|
10
|
+
layout,
|
|
11
|
+
LayoutProps,
|
|
12
|
+
spacing,
|
|
13
|
+
SpacingProps,
|
|
14
|
+
useRestyle,
|
|
15
|
+
} from "@shopify/restyle";
|
|
16
|
+
import { RestyleTheme } from "../../core/restyle";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Restyle props that can be applied to ScrollView
|
|
20
|
+
*/
|
|
21
|
+
export type ScrollViewRestyleProps = SpacingProps<RestyleTheme> &
|
|
22
|
+
LayoutProps<RestyleTheme> &
|
|
23
|
+
BackgroundColorProps<RestyleTheme>;
|
|
24
|
+
|
|
25
|
+
export type ScrollViewProps = ScrollViewRestyleProps & RNScrollViewProps;
|
|
26
|
+
|
|
27
|
+
/** Ref type for the ScrollView component — the underlying React Native ScrollView instance. */
|
|
28
|
+
export type ScrollViewRef = RNScrollView;
|
|
29
|
+
|
|
30
|
+
const restyleFunctions = composeRestyleFunctions<
|
|
31
|
+
RestyleTheme,
|
|
32
|
+
ScrollViewRestyleProps
|
|
33
|
+
>([spacing, layout, backgroundColor]);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* ScrollView — Restyled ScrollView component
|
|
37
|
+
*
|
|
38
|
+
* A ScrollView wrapper that accepts Restyle props (padding, margin, flex, backgroundColor)
|
|
39
|
+
* while preserving all native ScrollView functionality and supporting ref forwarding.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```tsx
|
|
43
|
+
* <ScrollView
|
|
44
|
+
* ref={scrollRef}
|
|
45
|
+
* horizontal
|
|
46
|
+
* paddingHorizontal="md"
|
|
47
|
+
* showsHorizontalScrollIndicator={false}
|
|
48
|
+
* contentContainerStyle={{ gap: 8 }}
|
|
49
|
+
* >
|
|
50
|
+
* {items.map((item) => (
|
|
51
|
+
* <Chip key={item.id} label={item.name} />
|
|
52
|
+
* ))}
|
|
53
|
+
* </ScrollView>
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export const ScrollView = forwardRef<RNScrollView, ScrollViewProps>(
|
|
57
|
+
function ScrollView(props, ref) {
|
|
58
|
+
const { style, ...rest } = useRestyle(restyleFunctions, props);
|
|
59
|
+
return <RNScrollView ref={ref} style={style} {...rest} />;
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
ScrollView.displayName = "ScrollView";
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HStack / VStack Components
|
|
3
|
+
*
|
|
4
|
+
* @description Convenience layout primitives for flex-based stacking - Atom
|
|
5
|
+
*
|
|
6
|
+
* Thin wrappers around Box that set sensible flexDirection defaults,
|
|
7
|
+
* eliminating the most common Box boilerplate in consumer code.
|
|
8
|
+
*
|
|
9
|
+
* ## Components
|
|
10
|
+
* | Component | Direction | Use Case |
|
|
11
|
+
* |-----------|-----------|----------|
|
|
12
|
+
* | HStack | row | Toolbars, inline groups |
|
|
13
|
+
* | VStack | column | Vertical lists, form fields |
|
|
14
|
+
*
|
|
15
|
+
* All Box/Restyle props are forwarded — spacing, colors, borders, etc.
|
|
16
|
+
* For edge cases like `row-reverse`, use `Box` directly.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* // Vertical stack with gap
|
|
20
|
+
* <VStack gap="md" padding="lg">
|
|
21
|
+
* <Text>First</Text>
|
|
22
|
+
* <Text>Second</Text>
|
|
23
|
+
* </VStack>
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* // Horizontal stack centered
|
|
27
|
+
* <HStack gap="sm" alignItems="center">
|
|
28
|
+
* <Icon name="check" />
|
|
29
|
+
* <Text>Confirmed</Text>
|
|
30
|
+
* </HStack>
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import React, { memo } from "react";
|
|
34
|
+
import { Box, BoxRestyleProps } from "./Box";
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Types
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
export type HStackProps = BoxRestyleProps;
|
|
41
|
+
export type VStackProps = BoxRestyleProps;
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Components
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Horizontal stack (row direction). All Box props forwarded.
|
|
49
|
+
*/
|
|
50
|
+
function HStackComponent({ style, ...rest }: HStackProps) {
|
|
51
|
+
return <Box flexDirection="row" style={style} {...rest} />;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Vertical stack (column direction). All Box props forwarded.
|
|
56
|
+
*/
|
|
57
|
+
function VStackComponent({ style, ...rest }: VStackProps) {
|
|
58
|
+
return <Box flexDirection="column" style={style} {...rest} />;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Exports
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
export const HStack = memo(HStackComponent);
|
|
66
|
+
HStack.displayName = "HStack";
|
|
67
|
+
|
|
68
|
+
export const VStack = memo(VStackComponent);
|
|
69
|
+
VStack.displayName = "VStack";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout Components Module
|
|
3
|
+
*
|
|
4
|
+
* Components that structure and organize space.
|
|
5
|
+
*
|
|
6
|
+
* Note: Box and AnimatedBox are core primitives — import from "@/design-system/core".
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export * from "./AnimatedBox";
|
|
10
|
+
export * from "./Box";
|
|
11
|
+
export * from "./Stack";
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
Divider,
|
|
15
|
+
type DividerColor,
|
|
16
|
+
type DividerOrientation,
|
|
17
|
+
type DividerProps,
|
|
18
|
+
type DividerSpacing,
|
|
19
|
+
type DividerThickness,
|
|
20
|
+
} from "./Divider";
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
FlatList,
|
|
24
|
+
type FlatListProps,
|
|
25
|
+
type FlatListRef,
|
|
26
|
+
type FlatListRestyleProps,
|
|
27
|
+
} from "./FlatList";
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
Pressable,
|
|
31
|
+
type PressableProps,
|
|
32
|
+
type PressableRestyleProps,
|
|
33
|
+
} from "./Pressable";
|
|
34
|
+
|
|
35
|
+
export {
|
|
36
|
+
ScrollView,
|
|
37
|
+
type ScrollViewProps,
|
|
38
|
+
type ScrollViewRef,
|
|
39
|
+
type ScrollViewRestyleProps,
|
|
40
|
+
} from "./ScrollView";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modal Helper Functions
|
|
3
|
+
*
|
|
4
|
+
* Pure utility functions for Modal component.
|
|
5
|
+
* All values derived from tokens/scales.ts for consistency.
|
|
6
|
+
*
|
|
7
|
+
* @see tokens/scales.ts - MODAL_LAYOUT
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ModalPosition } from "./Modal.types";
|
|
11
|
+
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// ANIMATION HELPERS
|
|
14
|
+
// =============================================================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get the animation type based on modal position.
|
|
18
|
+
*
|
|
19
|
+
* - center: fade (standard dialog)
|
|
20
|
+
* - top: slide (slides down from top)
|
|
21
|
+
* - bottom: slide (slides up from bottom)
|
|
22
|
+
*
|
|
23
|
+
* @param position - Modal position
|
|
24
|
+
* @returns RN Modal animationType
|
|
25
|
+
*/
|
|
26
|
+
export function getAnimationType(
|
|
27
|
+
position: ModalPosition
|
|
28
|
+
): "fade" | "slide" | "none" {
|
|
29
|
+
if (position === "center") return "fade";
|
|
30
|
+
return "slide";
|
|
31
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modal Component
|
|
3
|
+
*
|
|
4
|
+
* @description Dialog overlay for focused user interactions - Molecule
|
|
5
|
+
*
|
|
6
|
+
* Modal presents content in a focused overlay that requires user attention.
|
|
7
|
+
* Supports multiple positions (center, top, bottom) with appropriate
|
|
8
|
+
* animations and safe area handling.
|
|
9
|
+
*
|
|
10
|
+
* ## Width Behavior
|
|
11
|
+
* | Viewport | Width |
|
|
12
|
+
* |----------|------------------------------------------|
|
|
13
|
+
* | Phone | Full width minus lg (16px) horizontal |
|
|
14
|
+
* | Tablet | Capped at 480px max-width |
|
|
15
|
+
*
|
|
16
|
+
* ## Positions
|
|
17
|
+
* - `center`: Standard dialog centered on screen (default)
|
|
18
|
+
* - `top`: Anchored to top with slide-down animation
|
|
19
|
+
* - `bottom`: Anchored to bottom with slide-up animation
|
|
20
|
+
*
|
|
21
|
+
* ## Features
|
|
22
|
+
* - Three position variants with matching animations
|
|
23
|
+
* - Optional title and close button header
|
|
24
|
+
* - Primary and secondary action buttons in footer
|
|
25
|
+
* - Backdrop press to dismiss
|
|
26
|
+
* - Safe area aware (status bar + home indicator)
|
|
27
|
+
* - Full accessibility support (dialog role)
|
|
28
|
+
* - Memoized for performance
|
|
29
|
+
*
|
|
30
|
+
* @see Modal.types.ts - Type definitions
|
|
31
|
+
* @see Modal.helpers.ts - Animation type resolution
|
|
32
|
+
* @see Modal.a11y.ts - Accessibility prop generation
|
|
33
|
+
* @see tokens/scales.ts - MODAL_LAYOUT
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* // Basic usage
|
|
37
|
+
* <Modal visible={isOpen} onClose={handleClose} title="Confirm">
|
|
38
|
+
* <Text>Are you sure?</Text>
|
|
39
|
+
* </Modal>
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* // With actions
|
|
43
|
+
* <Modal
|
|
44
|
+
* visible={isOpen}
|
|
45
|
+
* onClose={handleClose}
|
|
46
|
+
* title="Delete Item"
|
|
47
|
+
* position="center"
|
|
48
|
+
* primaryAction={{ label: "Delete", onPress: handleDelete }}
|
|
49
|
+
* secondaryAction={{ label: "Cancel", onPress: handleClose }}
|
|
50
|
+
* accessibilityLabel="Delete confirmation dialog"
|
|
51
|
+
* />
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* // Bottom sheet style
|
|
55
|
+
* <Modal
|
|
56
|
+
* visible={isOpen}
|
|
57
|
+
* onClose={handleClose}
|
|
58
|
+
* title="Options"
|
|
59
|
+
* position="bottom"
|
|
60
|
+
* >
|
|
61
|
+
* <Button title="Share" onPress={handleShare} />
|
|
62
|
+
* <Button title="Edit" onPress={handleEdit} />
|
|
63
|
+
* </Modal>
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
import React, { memo } from "react";
|
|
67
|
+
import { Pressable, Modal as RNModal, StyleSheet } from "react-native";
|
|
68
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
69
|
+
import { MODAL_LAYOUT } from "../../../tokens/scales";
|
|
70
|
+
import { Button } from "../../actions/Button";
|
|
71
|
+
import { IconButton } from "../../actions/IconButton";
|
|
72
|
+
import { Box } from "../../layout";
|
|
73
|
+
import { Text } from "../../typography";
|
|
74
|
+
import type { ModalProps } from "./Modal.types";
|
|
75
|
+
import { useModalLogic } from "./useModalLogic";
|
|
76
|
+
|
|
77
|
+
// =============================================================================
|
|
78
|
+
// COMPONENT IMPLEMENTATION
|
|
79
|
+
// =============================================================================
|
|
80
|
+
|
|
81
|
+
function ModalComponent({
|
|
82
|
+
visible,
|
|
83
|
+
onClose,
|
|
84
|
+
children,
|
|
85
|
+
title,
|
|
86
|
+
position = "center",
|
|
87
|
+
closeOnBackdropPress = true,
|
|
88
|
+
showCloseButton = true,
|
|
89
|
+
primaryAction,
|
|
90
|
+
secondaryAction,
|
|
91
|
+
testID,
|
|
92
|
+
accessibilityLabel,
|
|
93
|
+
}: ModalProps) {
|
|
94
|
+
const insets = useSafeAreaInsets();
|
|
95
|
+
|
|
96
|
+
const { handleBackdropPress, animationType, a11yProps, closeButtonA11yProps } = useModalLogic({
|
|
97
|
+
visible,
|
|
98
|
+
onClose,
|
|
99
|
+
title,
|
|
100
|
+
position,
|
|
101
|
+
closeOnBackdropPress,
|
|
102
|
+
showCloseButton,
|
|
103
|
+
accessibilityLabel,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Safe area aware container padding — modal floats inside safe areas
|
|
107
|
+
const containerPadding = {
|
|
108
|
+
paddingTop:
|
|
109
|
+
position === "top"
|
|
110
|
+
? insets.top + MODAL_LAYOUT.horizontalMargin
|
|
111
|
+
: position === "center"
|
|
112
|
+
? MODAL_LAYOUT.horizontalMargin
|
|
113
|
+
: MODAL_LAYOUT.horizontalMargin,
|
|
114
|
+
paddingBottom:
|
|
115
|
+
position === "bottom"
|
|
116
|
+
? insets.bottom + MODAL_LAYOUT.horizontalMargin
|
|
117
|
+
: position === "center"
|
|
118
|
+
? MODAL_LAYOUT.horizontalMargin
|
|
119
|
+
: MODAL_LAYOUT.horizontalMargin,
|
|
120
|
+
paddingHorizontal: MODAL_LAYOUT.horizontalMargin,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const containerStyle =
|
|
124
|
+
position === "bottom"
|
|
125
|
+
? styles.bottomContainer
|
|
126
|
+
: position === "top"
|
|
127
|
+
? styles.topContainer
|
|
128
|
+
: styles.centerContainer;
|
|
129
|
+
|
|
130
|
+
const hasFooter = !!primaryAction || !!secondaryAction;
|
|
131
|
+
const hasHeader = !!title || showCloseButton;
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<RNModal
|
|
135
|
+
visible={visible}
|
|
136
|
+
transparent
|
|
137
|
+
animationType={animationType}
|
|
138
|
+
onRequestClose={onClose}
|
|
139
|
+
statusBarTranslucent
|
|
140
|
+
>
|
|
141
|
+
<Pressable
|
|
142
|
+
style={[styles.overlay, containerStyle, containerPadding]}
|
|
143
|
+
onPress={handleBackdropPress}
|
|
144
|
+
>
|
|
145
|
+
<Pressable style={styles.sheet} onPress={(e) => e.stopPropagation()}>
|
|
146
|
+
<Box
|
|
147
|
+
testID={testID}
|
|
148
|
+
backgroundColor="surfaceElevated"
|
|
149
|
+
borderRadius="xl"
|
|
150
|
+
overflow="hidden"
|
|
151
|
+
{...a11yProps}
|
|
152
|
+
>
|
|
153
|
+
{/* Header */}
|
|
154
|
+
{hasHeader && (
|
|
155
|
+
<Box
|
|
156
|
+
flexDirection="row"
|
|
157
|
+
alignItems="center"
|
|
158
|
+
justifyContent="space-between"
|
|
159
|
+
paddingHorizontal="lg"
|
|
160
|
+
paddingVertical="md"
|
|
161
|
+
borderBottomWidth={1}
|
|
162
|
+
borderBottomColor="borderSubtle"
|
|
163
|
+
>
|
|
164
|
+
{title ? (
|
|
165
|
+
<Box flex={1} marginRight={showCloseButton ? "sm" : "none"}>
|
|
166
|
+
<Text variant="headingSmall">{title}</Text>
|
|
167
|
+
</Box>
|
|
168
|
+
) : (
|
|
169
|
+
<Box flex={1} />
|
|
170
|
+
)}
|
|
171
|
+
{showCloseButton && (
|
|
172
|
+
<IconButton
|
|
173
|
+
iconName="close"
|
|
174
|
+
variant="ghost"
|
|
175
|
+
size="sm"
|
|
176
|
+
onPress={onClose}
|
|
177
|
+
accessibilityLabel={closeButtonA11yProps.accessibilityLabel}
|
|
178
|
+
accessibilityHint={closeButtonA11yProps.accessibilityHint}
|
|
179
|
+
/>
|
|
180
|
+
)}
|
|
181
|
+
</Box>
|
|
182
|
+
)}
|
|
183
|
+
|
|
184
|
+
{/* Body (children) */}
|
|
185
|
+
<Box paddingHorizontal="lg" paddingVertical="md">
|
|
186
|
+
{children}
|
|
187
|
+
</Box>
|
|
188
|
+
|
|
189
|
+
{/* Footer */}
|
|
190
|
+
{hasFooter && (
|
|
191
|
+
<Box
|
|
192
|
+
paddingHorizontal="lg"
|
|
193
|
+
paddingVertical="md"
|
|
194
|
+
borderTopWidth={1}
|
|
195
|
+
borderTopColor="borderSubtle"
|
|
196
|
+
flexDirection="row"
|
|
197
|
+
justifyContent="flex-end"
|
|
198
|
+
gap="sm"
|
|
199
|
+
>
|
|
200
|
+
{secondaryAction && (
|
|
201
|
+
<Button
|
|
202
|
+
title={secondaryAction.label}
|
|
203
|
+
variant="outline"
|
|
204
|
+
size="md"
|
|
205
|
+
onPress={secondaryAction.onPress}
|
|
206
|
+
isLoading={secondaryAction.loading}
|
|
207
|
+
disabled={secondaryAction.disabled}
|
|
208
|
+
testID={testID ? `${testID}-secondary-action` : undefined}
|
|
209
|
+
/>
|
|
210
|
+
)}
|
|
211
|
+
{primaryAction && (
|
|
212
|
+
<Button
|
|
213
|
+
title={primaryAction.label}
|
|
214
|
+
variant="solid"
|
|
215
|
+
size="md"
|
|
216
|
+
onPress={primaryAction.onPress}
|
|
217
|
+
isLoading={primaryAction.loading}
|
|
218
|
+
disabled={primaryAction.disabled}
|
|
219
|
+
testID={testID ? `${testID}-primary-action` : undefined}
|
|
220
|
+
/>
|
|
221
|
+
)}
|
|
222
|
+
</Box>
|
|
223
|
+
)}
|
|
224
|
+
</Box>
|
|
225
|
+
</Pressable>
|
|
226
|
+
</Pressable>
|
|
227
|
+
</RNModal>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// =============================================================================
|
|
232
|
+
// STYLES (POSITIONING ONLY - COLORS FROM THEME)
|
|
233
|
+
// =============================================================================
|
|
234
|
+
|
|
235
|
+
const styles = StyleSheet.create({
|
|
236
|
+
overlay: {
|
|
237
|
+
flex: 1,
|
|
238
|
+
backgroundColor: `rgba(0, 0, 0, ${MODAL_LAYOUT.overlayOpacity})`,
|
|
239
|
+
},
|
|
240
|
+
centerContainer: {
|
|
241
|
+
justifyContent: "center",
|
|
242
|
+
alignItems: "center",
|
|
243
|
+
},
|
|
244
|
+
topContainer: {
|
|
245
|
+
justifyContent: "flex-start",
|
|
246
|
+
alignItems: "center",
|
|
247
|
+
},
|
|
248
|
+
bottomContainer: {
|
|
249
|
+
justifyContent: "flex-end",
|
|
250
|
+
alignItems: "center",
|
|
251
|
+
},
|
|
252
|
+
sheet: {
|
|
253
|
+
width: "100%",
|
|
254
|
+
maxWidth: MODAL_LAYOUT.tabletMaxWidth,
|
|
255
|
+
alignItems: "stretch",
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// =============================================================================
|
|
260
|
+
// EXPORTS
|
|
261
|
+
// =============================================================================
|
|
262
|
+
|
|
263
|
+
export const Modal = memo(ModalComponent);
|
|
264
|
+
Modal.displayName = "Modal";
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modal Component Types
|
|
3
|
+
*
|
|
4
|
+
* Type definitions for the Modal dialog overlay component.
|
|
5
|
+
*
|
|
6
|
+
* Width behavior:
|
|
7
|
+
* - Phone: Full width minus lg (16px) horizontal padding on each side
|
|
8
|
+
* - Tablet (≥768px): Capped at 480px max-width (phone-like experience)
|
|
9
|
+
*
|
|
10
|
+
* @see tokens/scales.ts - MODAL_LAYOUT
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ReactNode } from "react";
|
|
14
|
+
import type { AccessibilityProps } from "react-native";
|
|
15
|
+
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// VARIANT TYPES (Explicitly exported for external use)
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Modal position controlling vertical placement.
|
|
22
|
+
*
|
|
23
|
+
* - `center`: Centered vertically (default, standard dialog)
|
|
24
|
+
* - `top`: Anchored to top of screen (notifications, alerts)
|
|
25
|
+
* - `bottom`: Anchored to bottom of screen (action sheets)
|
|
26
|
+
*/
|
|
27
|
+
export type ModalPosition = "center" | "top" | "bottom";
|
|
28
|
+
|
|
29
|
+
// =============================================================================
|
|
30
|
+
// ACTION TYPES
|
|
31
|
+
// =============================================================================
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Action button configuration for modal footer.
|
|
35
|
+
*/
|
|
36
|
+
export interface ModalAction {
|
|
37
|
+
/** Button label text */
|
|
38
|
+
label: string;
|
|
39
|
+
/** Press handler */
|
|
40
|
+
onPress: () => void;
|
|
41
|
+
/** Whether the action is in a loading state */
|
|
42
|
+
loading?: boolean;
|
|
43
|
+
/** Whether the action is disabled */
|
|
44
|
+
disabled?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// =============================================================================
|
|
48
|
+
// COMPONENT PROPS
|
|
49
|
+
// =============================================================================
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Modal component props.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```tsx
|
|
56
|
+
* <Modal
|
|
57
|
+
* visible={isVisible}
|
|
58
|
+
* onClose={handleClose}
|
|
59
|
+
* title="Confirm Action"
|
|
60
|
+
* position="center"
|
|
61
|
+
* >
|
|
62
|
+
* <Text>Are you sure you want to proceed?</Text>
|
|
63
|
+
* </Modal>
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export interface ModalProps {
|
|
67
|
+
// -------------------------------------------------------------------------
|
|
68
|
+
// Required Props
|
|
69
|
+
// -------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
/** Whether the modal is visible */
|
|
72
|
+
visible: boolean;
|
|
73
|
+
/** Callback when modal should close */
|
|
74
|
+
onClose: () => void;
|
|
75
|
+
/** Modal content */
|
|
76
|
+
children: ReactNode;
|
|
77
|
+
|
|
78
|
+
// -------------------------------------------------------------------------
|
|
79
|
+
// Content Props
|
|
80
|
+
// -------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
/** Modal title displayed in the header */
|
|
83
|
+
title?: string;
|
|
84
|
+
|
|
85
|
+
// -------------------------------------------------------------------------
|
|
86
|
+
// Position Props
|
|
87
|
+
// -------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Vertical position of the modal
|
|
91
|
+
* @default "center"
|
|
92
|
+
*/
|
|
93
|
+
position?: ModalPosition;
|
|
94
|
+
|
|
95
|
+
// -------------------------------------------------------------------------
|
|
96
|
+
// Behavior Props
|
|
97
|
+
// -------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Whether tapping the backdrop closes the modal
|
|
101
|
+
* @default true
|
|
102
|
+
*/
|
|
103
|
+
closeOnBackdropPress?: boolean;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Whether to show the close button in the header
|
|
107
|
+
* @default true
|
|
108
|
+
*/
|
|
109
|
+
showCloseButton?: boolean;
|
|
110
|
+
|
|
111
|
+
// -------------------------------------------------------------------------
|
|
112
|
+
// Action Props
|
|
113
|
+
// -------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
/** Primary action button (e.g., "Confirm", "Save") */
|
|
116
|
+
primaryAction?: ModalAction;
|
|
117
|
+
/** Secondary action button (e.g., "Cancel") */
|
|
118
|
+
secondaryAction?: ModalAction;
|
|
119
|
+
|
|
120
|
+
// -------------------------------------------------------------------------
|
|
121
|
+
// Accessibility Props
|
|
122
|
+
// -------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
/** Test ID for testing */
|
|
125
|
+
testID?: string;
|
|
126
|
+
/** Accessibility label override */
|
|
127
|
+
accessibilityLabel?: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// =============================================================================
|
|
131
|
+
// INTERNAL TYPES
|
|
132
|
+
// =============================================================================
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Parameters for useModalLogic hook.
|
|
136
|
+
*/
|
|
137
|
+
export interface UseModalLogicParams {
|
|
138
|
+
visible: boolean;
|
|
139
|
+
onClose: () => void;
|
|
140
|
+
title?: string;
|
|
141
|
+
position: ModalPosition;
|
|
142
|
+
closeOnBackdropPress: boolean;
|
|
143
|
+
showCloseButton: boolean;
|
|
144
|
+
accessibilityLabel?: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Return value from useModalLogic hook.
|
|
149
|
+
*/
|
|
150
|
+
export interface UseModalLogicReturn {
|
|
151
|
+
/** Handler for backdrop press */
|
|
152
|
+
handleBackdropPress: () => void;
|
|
153
|
+
/** Animation type for the RN Modal */
|
|
154
|
+
animationType: "fade" | "slide" | "none";
|
|
155
|
+
/** Accessibility props for the modal container */
|
|
156
|
+
a11yProps: AccessibilityProps;
|
|
157
|
+
/** Accessibility props for the close button */
|
|
158
|
+
closeButtonA11yProps: {
|
|
159
|
+
accessible: true;
|
|
160
|
+
accessibilityRole: "button";
|
|
161
|
+
accessibilityLabel: string;
|
|
162
|
+
accessibilityHint: string;
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Parameters for getModalA11y function.
|
|
168
|
+
*/
|
|
169
|
+
export interface ModalA11yParams {
|
|
170
|
+
title?: string;
|
|
171
|
+
position: ModalPosition;
|
|
172
|
+
accessibilityLabel?: string;
|
|
173
|
+
fallbackTitle: string;
|
|
174
|
+
positionLabels: { bottom: string; top: string; center: string };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Parameters for getModalCloseButtonA11yProps function.
|
|
179
|
+
*/
|
|
180
|
+
export interface ModalCloseButtonA11yParams {
|
|
181
|
+
closeLabel: string;
|
|
182
|
+
closeHint: string;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Accessibility props returned by getModalA11y.
|
|
187
|
+
*/
|
|
188
|
+
export interface ModalA11yProps {
|
|
189
|
+
accessible: true;
|
|
190
|
+
accessibilityRole: "none";
|
|
191
|
+
accessibilityLabel: string;
|
|
192
|
+
accessibilityViewIsModal: true;
|
|
193
|
+
}
|