@ankhorage/surface 0.1.5 → 0.1.6
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/CHANGELOG.md +6 -0
- package/dist/components/badge/Badge.js.map +1 -1
- package/dist/components/badge/index.js.map +1 -1
- package/dist/components/badge/types.js.map +1 -1
- package/dist/components/button/Button.js.map +1 -1
- package/dist/components/button/index.js.map +1 -1
- package/dist/components/button/types.js.map +1 -1
- package/dist/components/card/Card.js.map +1 -1
- package/dist/components/card/index.js.map +1 -1
- package/dist/components/card/types.js.map +1 -1
- package/dist/components/checkbox/Checkbox.js.map +1 -1
- package/dist/components/checkbox/index.js.map +1 -1
- package/dist/components/checkbox/types.js.map +1 -1
- package/dist/components/drawer/Drawer.js.map +1 -1
- package/dist/components/drawer/index.js.map +1 -1
- package/dist/components/drawer/types.js.map +1 -1
- package/dist/components/field/Field.js.map +1 -1
- package/dist/components/field/index.js.map +1 -1
- package/dist/components/field/types.js.map +1 -1
- package/dist/components/helper-text/HelperText.js.map +1 -1
- package/dist/components/helper-text/index.js.map +1 -1
- package/dist/components/helper-text/types.js.map +1 -1
- package/dist/components/icon-button/IconButton.js.map +1 -1
- package/dist/components/icon-button/index.js.map +1 -1
- package/dist/components/icon-button/types.js.map +1 -1
- package/dist/components/label/Label.js.map +1 -1
- package/dist/components/label/index.js.map +1 -1
- package/dist/components/label/types.js.map +1 -1
- package/dist/components/list-item/ListItem.js.map +1 -1
- package/dist/components/list-item/index.js.map +1 -1
- package/dist/components/list-item/types.js.map +1 -1
- package/dist/components/menu/Menu.js.map +1 -1
- package/dist/components/menu/index.js.map +1 -1
- package/dist/components/menu/navigation.js.map +1 -1
- package/dist/components/menu/types.js.map +1 -1
- package/dist/components/modal/Modal.js.map +1 -1
- package/dist/components/modal/index.js.map +1 -1
- package/dist/components/modal/types.js.map +1 -1
- package/dist/components/radio/Radio.js.map +1 -1
- package/dist/components/radio/index.js.map +1 -1
- package/dist/components/radio/types.js.map +1 -1
- package/dist/components/switch/Switch.js.map +1 -1
- package/dist/components/switch/index.js.map +1 -1
- package/dist/components/switch/types.js.map +1 -1
- package/dist/components/tabs/Tab.js.map +1 -1
- package/dist/components/tabs/TabList.js.map +1 -1
- package/dist/components/tabs/TabPanel.js.map +1 -1
- package/dist/components/tabs/Tabs.js.map +1 -1
- package/dist/components/tabs/a11y.js.map +1 -1
- package/dist/components/tabs/context.js.map +1 -1
- package/dist/components/tabs/index.js.map +1 -1
- package/dist/components/tabs/navigation.js.map +1 -1
- package/dist/components/tabs/types.js.map +1 -1
- package/dist/components/text-input/TextInput.js.map +1 -1
- package/dist/components/text-input/index.js.map +1 -1
- package/dist/components/text-input/types.js.map +1 -1
- package/dist/components/textarea/Textarea.js.map +1 -1
- package/dist/components/textarea/index.js.map +1 -1
- package/dist/components/textarea/types.js.map +1 -1
- package/dist/components/toast/Toast.js.map +1 -1
- package/dist/components/toast/ToastProvider.js.map +1 -1
- package/dist/components/toast/index.js.map +1 -1
- package/dist/components/toast/types.js.map +1 -1
- package/dist/components/tooltip/Tooltip.js.map +1 -1
- package/dist/components/tooltip/index.js.map +1 -1
- package/dist/components/tooltip/types.js.map +1 -1
- package/dist/context/FontContext.js.map +1 -1
- package/dist/context/TranslationContext.js.map +1 -1
- package/dist/core/responsive/ResponsiveProvider.js.map +1 -1
- package/dist/core/responsive/breakpoints.js.map +1 -1
- package/dist/core/responsive/getBreakpointFromWidth.js.map +1 -1
- package/dist/core/responsive/index.js.map +1 -1
- package/dist/core/responsive/resolve.js.map +1 -1
- package/dist/core/responsive/types.js.map +1 -1
- package/dist/core/responsive/useBreakpoint.js.map +1 -1
- package/dist/examples/DocsExamples.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/focus/FocusScope.js.map +1 -1
- package/dist/internal/focus/useFocusManager.js.map +1 -1
- package/dist/internal/overlay/OverlayProvider.js.map +1 -1
- package/dist/internal/overlay/Portal.js.map +1 -1
- package/dist/internal/overlay/useOverlayStack.js.map +1 -1
- package/dist/internal/resolvers/index.js.map +1 -1
- package/dist/internal/resolvers/resolveControlSize.js.map +1 -1
- package/dist/internal/resolvers/resolveFieldPresentation.js.map +1 -1
- package/dist/internal/resolvers/resolveFieldState.js.map +1 -1
- package/dist/internal/resolvers/resolveFocusRingStyles.js.map +1 -1
- package/dist/internal/resolvers/resolveIconSize.js.map +1 -1
- package/dist/internal/resolvers/resolveIndicatorSize.js.map +1 -1
- package/dist/internal/resolvers/resolveInteractiveColors.js.map +1 -1
- package/dist/internal/resolvers/resolveInteractiveState.js.map +1 -1
- package/dist/internal/resolvers/resolveOverlayAnimation.js.map +1 -1
- package/dist/internal/resolvers/resolveOverlayZIndex.js.map +1 -1
- package/dist/internal/resolvers/resolveSelectionControlBehavior.js.map +1 -1
- package/dist/internal/resolvers/resolveSelectionControlColors.js.map +1 -1
- package/dist/internal/resolvers/resolveTextColor.js.map +1 -1
- package/dist/internal/resolvers/resolveTextStyles.js.map +1 -1
- package/dist/internal/resolvers/resolveTone.js.map +1 -1
- package/dist/internal/useControllableState.js.map +1 -1
- package/dist/layout/Box.js.map +1 -1
- package/dist/layout/Center.js.map +1 -1
- package/dist/layout/Container.js.map +1 -1
- package/dist/layout/Divider.js.map +1 -1
- package/dist/layout/Grid.js.map +1 -1
- package/dist/layout/Inline.js.map +1 -1
- package/dist/layout/Show.js.map +1 -1
- package/dist/layout/Spacer.js.map +1 -1
- package/dist/layout/Stack.js.map +1 -1
- package/dist/layout/Surface.js.map +1 -1
- package/dist/layout/Template.js.map +1 -1
- package/dist/layout/helpers.js.map +1 -1
- package/dist/layout/index.js.map +1 -1
- package/dist/primitives/button-base/ButtonBase.js.map +1 -1
- package/dist/primitives/button-base/index.js.map +1 -1
- package/dist/primitives/button-base/types.js.map +1 -1
- package/dist/primitives/heading/Heading.js.map +1 -1
- package/dist/primitives/heading/index.js.map +1 -1
- package/dist/primitives/heading/resolveHeadingStyle.js.map +1 -1
- package/dist/primitives/heading/types.js.map +1 -1
- package/dist/primitives/icon/Icon.js.map +1 -1
- package/dist/primitives/icon/index.js.map +1 -1
- package/dist/primitives/icon/resolveExpoIconComponent.js.map +1 -1
- package/dist/primitives/text/Text.js.map +1 -1
- package/dist/primitives/text/index.js.map +1 -1
- package/dist/primitives/text/types.js.map +1 -1
- package/dist/theme/ThemeContext.js.map +1 -1
- package/dist/theme/colorEngine.js.map +1 -1
- package/dist/theme/createTheme.js.map +1 -1
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/resolveToken.js.map +1 -1
- package/dist/theme/types.js.map +1 -1
- package/dist/utils/deepEqual.js.map +1 -1
- package/dist/utils/deepMerge.js.map +1 -1
- package/package.json +4 -1
- package/src/components/badge/Badge.tsx +47 -0
- package/src/components/badge/index.ts +2 -0
- package/src/components/badge/types.ts +13 -0
- package/src/components/button/Button.tsx +104 -0
- package/src/components/button/index.ts +2 -0
- package/src/components/button/types.ts +26 -0
- package/src/components/card/Card.tsx +81 -0
- package/src/components/card/index.ts +2 -0
- package/src/components/card/types.ts +11 -0
- package/src/components/checkbox/Checkbox.tsx +111 -0
- package/src/components/checkbox/index.ts +2 -0
- package/src/components/checkbox/types.ts +19 -0
- package/src/components/drawer/Drawer.tsx +92 -0
- package/src/components/drawer/index.ts +2 -0
- package/src/components/drawer/types.ts +10 -0
- package/src/components/field/Field.tsx +43 -0
- package/src/components/field/index.ts +2 -0
- package/src/components/field/types.ts +13 -0
- package/src/components/helper-text/HelperText.tsx +12 -0
- package/src/components/helper-text/index.ts +2 -0
- package/src/components/helper-text/types.ts +9 -0
- package/src/components/icon-button/IconButton.tsx +60 -0
- package/src/components/icon-button/index.ts +2 -0
- package/src/components/icon-button/types.ts +19 -0
- package/src/components/label/Label.tsx +17 -0
- package/src/components/label/index.ts +2 -0
- package/src/components/label/types.ts +10 -0
- package/src/components/list-item/ListItem.tsx +72 -0
- package/src/components/list-item/index.ts +2 -0
- package/src/components/list-item/types.ts +11 -0
- package/src/components/menu/Menu.tsx +180 -0
- package/src/components/menu/index.ts +2 -0
- package/src/components/menu/navigation.test.ts +21 -0
- package/src/components/menu/navigation.ts +34 -0
- package/src/components/menu/types.ts +16 -0
- package/src/components/modal/Modal.tsx +87 -0
- package/src/components/modal/index.ts +2 -0
- package/src/components/modal/types.ts +9 -0
- package/src/components/radio/Radio.tsx +116 -0
- package/src/components/radio/index.ts +2 -0
- package/src/components/radio/types.ts +19 -0
- package/src/components/switch/Switch.tsx +116 -0
- package/src/components/switch/index.ts +2 -0
- package/src/components/switch/types.ts +19 -0
- package/src/components/tabs/Tab.tsx +82 -0
- package/src/components/tabs/TabList.tsx +51 -0
- package/src/components/tabs/TabPanel.tsx +29 -0
- package/src/components/tabs/Tabs.tsx +67 -0
- package/src/components/tabs/a11y.test.ts +15 -0
- package/src/components/tabs/a11y.ts +15 -0
- package/src/components/tabs/context.tsx +31 -0
- package/src/components/tabs/index.ts +5 -0
- package/src/components/tabs/navigation.test.ts +21 -0
- package/src/components/tabs/navigation.ts +32 -0
- package/src/components/tabs/types.ts +27 -0
- package/src/components/text-input/TextInput.tsx +116 -0
- package/src/components/text-input/index.ts +2 -0
- package/src/components/text-input/types.ts +32 -0
- package/src/components/textarea/Textarea.tsx +15 -0
- package/src/components/textarea/index.ts +2 -0
- package/src/components/textarea/types.ts +5 -0
- package/src/components/toast/Toast.tsx +54 -0
- package/src/components/toast/ToastProvider.tsx +114 -0
- package/src/components/toast/index.ts +3 -0
- package/src/components/toast/types.ts +16 -0
- package/src/components/tooltip/Tooltip.tsx +109 -0
- package/src/components/tooltip/index.ts +2 -0
- package/src/components/tooltip/types.ts +9 -0
- package/src/context/FontContext.tsx +59 -0
- package/src/context/TranslationContext.tsx +54 -0
- package/src/core/responsive/ResponsiveProvider.tsx +31 -0
- package/src/core/responsive/breakpoints.ts +9 -0
- package/src/core/responsive/getBreakpointFromWidth.test.ts +15 -0
- package/src/core/responsive/getBreakpointFromWidth.ts +10 -0
- package/src/core/responsive/index.ts +6 -0
- package/src/core/responsive/resolve.test.ts +25 -0
- package/src/core/responsive/resolve.ts +24 -0
- package/src/core/responsive/types.ts +10 -0
- package/src/core/responsive/useBreakpoint.ts +9 -0
- package/src/examples/DocsExamples.tsx +116 -0
- package/src/index.test.ts +64 -0
- package/src/index.ts +55 -0
- package/src/internal/focus/FocusScope.tsx +66 -0
- package/src/internal/focus/useFocusManager.test.ts +44 -0
- package/src/internal/focus/useFocusManager.ts +142 -0
- package/src/internal/overlay/OverlayProvider.tsx +74 -0
- package/src/internal/overlay/Portal.tsx +38 -0
- package/src/internal/overlay/useOverlayStack.test.ts +31 -0
- package/src/internal/overlay/useOverlayStack.ts +61 -0
- package/src/internal/resolvers/index.ts +15 -0
- package/src/internal/resolvers/resolveControlSize.test.ts +25 -0
- package/src/internal/resolvers/resolveControlSize.ts +45 -0
- package/src/internal/resolvers/resolveFieldPresentation.test.ts +31 -0
- package/src/internal/resolvers/resolveFieldPresentation.ts +30 -0
- package/src/internal/resolvers/resolveFieldState.test.ts +22 -0
- package/src/internal/resolvers/resolveFieldState.ts +36 -0
- package/src/internal/resolvers/resolveFocusRingStyles.ts +14 -0
- package/src/internal/resolvers/resolveIconSize.ts +6 -0
- package/src/internal/resolvers/resolveIndicatorSize.test.ts +19 -0
- package/src/internal/resolvers/resolveIndicatorSize.ts +47 -0
- package/src/internal/resolvers/resolveInteractiveColors.test.ts +57 -0
- package/src/internal/resolvers/resolveInteractiveColors.ts +134 -0
- package/src/internal/resolvers/resolveInteractiveState.test.ts +14 -0
- package/src/internal/resolvers/resolveInteractiveState.ts +15 -0
- package/src/internal/resolvers/resolveOverlayAnimation.test.ts +15 -0
- package/src/internal/resolvers/resolveOverlayAnimation.ts +24 -0
- package/src/internal/resolvers/resolveOverlayZIndex.test.ts +15 -0
- package/src/internal/resolvers/resolveOverlayZIndex.ts +13 -0
- package/src/internal/resolvers/resolveSelectionControlBehavior.test.ts +52 -0
- package/src/internal/resolvers/resolveSelectionControlBehavior.ts +23 -0
- package/src/internal/resolvers/resolveSelectionControlColors.test.ts +44 -0
- package/src/internal/resolvers/resolveSelectionControlColors.ts +81 -0
- package/src/internal/resolvers/resolveTextColor.test.ts +23 -0
- package/src/internal/resolvers/resolveTextColor.ts +40 -0
- package/src/internal/resolvers/resolveTextStyles.test.ts +27 -0
- package/src/internal/resolvers/resolveTextStyles.ts +95 -0
- package/src/internal/resolvers/resolveTone.ts +19 -0
- package/src/internal/useControllableState.ts +28 -0
- package/src/layout/Box.tsx +79 -0
- package/src/layout/Center.tsx +22 -0
- package/src/layout/Container.tsx +43 -0
- package/src/layout/Divider.tsx +26 -0
- package/src/layout/Grid.tsx +83 -0
- package/src/layout/Inline.tsx +9 -0
- package/src/layout/Show.tsx +15 -0
- package/src/layout/Spacer.tsx +22 -0
- package/src/layout/Stack.tsx +67 -0
- package/src/layout/Surface.tsx +70 -0
- package/src/layout/Template.tsx +85 -0
- package/src/layout/helpers.test.ts +71 -0
- package/src/layout/helpers.ts +208 -0
- package/src/layout/index.ts +22 -0
- package/src/primitives/button-base/ButtonBase.tsx +81 -0
- package/src/primitives/button-base/index.ts +2 -0
- package/src/primitives/button-base/types.ts +16 -0
- package/src/primitives/heading/Heading.tsx +60 -0
- package/src/primitives/heading/index.ts +2 -0
- package/src/primitives/heading/resolveHeadingStyle.test.ts +31 -0
- package/src/primitives/heading/resolveHeadingStyle.ts +17 -0
- package/src/primitives/heading/types.ts +13 -0
- package/src/primitives/icon/Icon.tsx +40 -0
- package/src/primitives/icon/index.ts +2 -0
- package/src/primitives/icon/resolveExpoIconComponent.test.ts +29 -0
- package/src/primitives/icon/resolveExpoIconComponent.ts +20 -0
- package/src/primitives/text/Text.tsx +66 -0
- package/src/primitives/text/index.ts +2 -0
- package/src/primitives/text/types.ts +18 -0
- package/src/theme/ThemeContext.tsx +95 -0
- package/src/theme/colorEngine.test.ts +114 -0
- package/src/theme/colorEngine.ts +480 -0
- package/src/theme/createTheme.ts +121 -0
- package/src/theme/index.ts +5 -0
- package/src/theme/resolveToken.ts +32 -0
- package/src/theme/types.ts +188 -0
- package/src/utils/deepEqual.ts +34 -0
- package/src/utils/deepMerge.test.ts +117 -0
- package/src/utils/deepMerge.ts +29 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import type {
|
|
3
|
+
StyleProp,
|
|
4
|
+
TextInputProps as ReactNativeTextInputProps,
|
|
5
|
+
TextStyle,
|
|
6
|
+
} from 'react-native';
|
|
7
|
+
|
|
8
|
+
import type { ControlSize } from '../../internal/resolvers/resolveControlSize';
|
|
9
|
+
|
|
10
|
+
export interface TextInputProps extends Omit<
|
|
11
|
+
ReactNativeTextInputProps,
|
|
12
|
+
| 'defaultValue'
|
|
13
|
+
| 'editable'
|
|
14
|
+
| 'onChangeText'
|
|
15
|
+
| 'placeholderTextColor'
|
|
16
|
+
| 'style'
|
|
17
|
+
| 'testID'
|
|
18
|
+
| 'value'
|
|
19
|
+
> {
|
|
20
|
+
value?: string;
|
|
21
|
+
defaultValue?: string;
|
|
22
|
+
onChangeText?: ((text: string) => void) | undefined;
|
|
23
|
+
placeholder?: string;
|
|
24
|
+
size?: ControlSize;
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
readOnly?: boolean;
|
|
27
|
+
invalid?: boolean;
|
|
28
|
+
leadingAccessory?: React.ReactNode;
|
|
29
|
+
trailingAccessory?: React.ReactNode;
|
|
30
|
+
style?: StyleProp<TextStyle>;
|
|
31
|
+
testID?: string;
|
|
32
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { TextInput } from '../text-input';
|
|
4
|
+
import type { TextareaProps } from './types';
|
|
5
|
+
|
|
6
|
+
export function Textarea({ rows = 4, numberOfLines, style, ...props }: TextareaProps) {
|
|
7
|
+
return (
|
|
8
|
+
<TextInput
|
|
9
|
+
{...props}
|
|
10
|
+
multiline
|
|
11
|
+
numberOfLines={numberOfLines ?? rows}
|
|
12
|
+
style={[{ textAlignVertical: 'top' }, style]}
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Pressable } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { Box, Inline, Surface } from '../../layout';
|
|
5
|
+
import { Text } from '../../primitives/text';
|
|
6
|
+
import { useTheme } from '../../theme/ThemeContext';
|
|
7
|
+
import type { ToastProps } from './types';
|
|
8
|
+
|
|
9
|
+
export function Toast({ title, description, tone = 'default', onDismiss, testID }: ToastProps) {
|
|
10
|
+
const { theme } = useTheme();
|
|
11
|
+
const toneColor =
|
|
12
|
+
tone === 'success'
|
|
13
|
+
? theme.semantics.success.base
|
|
14
|
+
: tone === 'danger'
|
|
15
|
+
? theme.semantics.danger.base
|
|
16
|
+
: theme.semantics.action.primary.base;
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<Surface
|
|
20
|
+
p="m"
|
|
21
|
+
style={{
|
|
22
|
+
borderLeftColor: toneColor,
|
|
23
|
+
borderLeftWidth: 3,
|
|
24
|
+
minWidth: 280,
|
|
25
|
+
shadowOpacity: 0.14,
|
|
26
|
+
shadowRadius: 12,
|
|
27
|
+
shadowOffset: { width: 0, height: 6 },
|
|
28
|
+
}}
|
|
29
|
+
testID={testID}
|
|
30
|
+
variant="raised"
|
|
31
|
+
>
|
|
32
|
+
<Inline align="center" justify="space-between">
|
|
33
|
+
<Box flex={1}>
|
|
34
|
+
{title ? (
|
|
35
|
+
<Text variant="label" weight="medium">
|
|
36
|
+
{title}
|
|
37
|
+
</Text>
|
|
38
|
+
) : null}
|
|
39
|
+
{description ? <Text tone="muted">{description}</Text> : null}
|
|
40
|
+
</Box>
|
|
41
|
+
{onDismiss ? (
|
|
42
|
+
<Pressable
|
|
43
|
+
accessibilityLabel="Dismiss notification"
|
|
44
|
+
accessibilityRole="button"
|
|
45
|
+
onPress={onDismiss}
|
|
46
|
+
testID={testID ? `${testID}-dismiss` : undefined}
|
|
47
|
+
>
|
|
48
|
+
<Text color={toneColor}>×</Text>
|
|
49
|
+
</Pressable>
|
|
50
|
+
) : null}
|
|
51
|
+
</Inline>
|
|
52
|
+
</Surface>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { Portal } from '../../internal/overlay/Portal';
|
|
4
|
+
import { resolveOverlayAnimation } from '../../internal/resolvers';
|
|
5
|
+
import { Stack } from '../../layout';
|
|
6
|
+
import { Toast } from './Toast';
|
|
7
|
+
import type { ToastOptions } from './types';
|
|
8
|
+
|
|
9
|
+
interface ToastEntry extends ToastOptions {
|
|
10
|
+
id: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ToastContextValue {
|
|
14
|
+
dismissToast: (id: string) => void;
|
|
15
|
+
showToast: (options: ToastOptions) => string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const ToastContext = React.createContext<ToastContextValue | null>(null);
|
|
19
|
+
|
|
20
|
+
let toastCounter = 0;
|
|
21
|
+
|
|
22
|
+
export function ToastProvider({
|
|
23
|
+
children,
|
|
24
|
+
defaultDuration = 4000,
|
|
25
|
+
}: {
|
|
26
|
+
children: React.ReactNode;
|
|
27
|
+
defaultDuration?: number;
|
|
28
|
+
}) {
|
|
29
|
+
const [toasts, setToasts] = React.useState<ToastEntry[]>([]);
|
|
30
|
+
const animation = resolveOverlayAnimation('toast');
|
|
31
|
+
const timersRef = React.useRef(new Map<string, ReturnType<typeof setTimeout>>());
|
|
32
|
+
|
|
33
|
+
const dismissToast = React.useCallback((id: string) => {
|
|
34
|
+
const timer = timersRef.current.get(id);
|
|
35
|
+
if (timer) {
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
timersRef.current.delete(id);
|
|
38
|
+
}
|
|
39
|
+
setToasts((current) => current.filter((toast) => toast.id !== id));
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
const showToast = React.useCallback((options: ToastOptions) => {
|
|
43
|
+
const id = options.id ?? `toast-${toastCounter++}`;
|
|
44
|
+
setToasts((current) => [...current, { ...options, id }]);
|
|
45
|
+
return id;
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
React.useEffect(() => {
|
|
49
|
+
toasts.forEach((toast) => {
|
|
50
|
+
if (timersRef.current.has(toast.id)) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const timer = setTimeout(() => {
|
|
55
|
+
dismissToast(toast.id);
|
|
56
|
+
}, toast.duration ?? defaultDuration);
|
|
57
|
+
|
|
58
|
+
timersRef.current.set(toast.id, timer);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const activeToastIds = new Set(toasts.map((toast) => toast.id));
|
|
62
|
+
timersRef.current.forEach((timer, id) => {
|
|
63
|
+
if (!activeToastIds.has(id)) {
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
timersRef.current.delete(id);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}, [defaultDuration, dismissToast, toasts]);
|
|
69
|
+
|
|
70
|
+
React.useEffect(() => {
|
|
71
|
+
return () => {
|
|
72
|
+
timersRef.current.forEach((timer) => clearTimeout(timer));
|
|
73
|
+
timersRef.current.clear();
|
|
74
|
+
};
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<ToastContext.Provider value={{ dismissToast, showToast }}>
|
|
79
|
+
{children}
|
|
80
|
+
<Portal layer="toast" visible={toasts.length > 0}>
|
|
81
|
+
<Stack
|
|
82
|
+
gap="s"
|
|
83
|
+
pointerEvents="box-none"
|
|
84
|
+
style={{
|
|
85
|
+
alignItems: 'flex-end',
|
|
86
|
+
padding: 16,
|
|
87
|
+
paddingTop: 16 + animation.offset,
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
{toasts.map((toast) => (
|
|
91
|
+
<Toast
|
|
92
|
+
description={toast.description}
|
|
93
|
+
key={toast.id}
|
|
94
|
+
onDismiss={() => dismissToast(toast.id)}
|
|
95
|
+
testID={toast.testID}
|
|
96
|
+
title={toast.title}
|
|
97
|
+
tone={toast.tone}
|
|
98
|
+
/>
|
|
99
|
+
))}
|
|
100
|
+
</Stack>
|
|
101
|
+
</Portal>
|
|
102
|
+
</ToastContext.Provider>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function useToast() {
|
|
107
|
+
const context = React.useContext(ToastContext);
|
|
108
|
+
|
|
109
|
+
if (!context) {
|
|
110
|
+
throw new Error('useToast must be used within <ToastProvider>.');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return context;
|
|
114
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
|
|
3
|
+
export type ToastTone = 'default' | 'success' | 'danger';
|
|
4
|
+
|
|
5
|
+
export interface ToastProps {
|
|
6
|
+
title?: React.ReactNode;
|
|
7
|
+
description?: React.ReactNode;
|
|
8
|
+
tone?: ToastTone;
|
|
9
|
+
onDismiss?: (() => void) | undefined;
|
|
10
|
+
testID?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ToastOptions extends Omit<ToastProps, 'onDismiss'> {
|
|
14
|
+
duration?: number;
|
|
15
|
+
id?: string;
|
|
16
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type LayoutRectangle, Platform, Pressable, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { Portal } from '../../internal/overlay/Portal';
|
|
5
|
+
import { resolveOverlayAnimation } from '../../internal/resolvers';
|
|
6
|
+
import { Surface } from '../../layout';
|
|
7
|
+
import { Text } from '../../primitives/text';
|
|
8
|
+
import { useTheme } from '../../theme/ThemeContext';
|
|
9
|
+
import type { TooltipProps } from './types';
|
|
10
|
+
|
|
11
|
+
interface MeasurableNode {
|
|
12
|
+
measureInWindow?: (
|
|
13
|
+
callback: (x: number, y: number, width: number, height: number) => void,
|
|
14
|
+
) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function measureNode(node: unknown, callback: (layout: LayoutRectangle) => void) {
|
|
18
|
+
const measurableNode = node as MeasurableNode | null;
|
|
19
|
+
measurableNode?.measureInWindow?.((x, y, width, height) => {
|
|
20
|
+
callback({ height, width, x, y });
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function Tooltip({
|
|
25
|
+
children,
|
|
26
|
+
content,
|
|
27
|
+
delay = 150,
|
|
28
|
+
placement = 'top',
|
|
29
|
+
testID,
|
|
30
|
+
}: TooltipProps) {
|
|
31
|
+
const { theme } = useTheme();
|
|
32
|
+
const animation = resolveOverlayAnimation('tooltip');
|
|
33
|
+
const anchorRef = React.useRef<View | null>(null);
|
|
34
|
+
const timeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
35
|
+
const [visible, setVisible] = React.useState(false);
|
|
36
|
+
const [layout, setLayout] = React.useState<LayoutRectangle | null>(null);
|
|
37
|
+
|
|
38
|
+
const show = React.useCallback(() => {
|
|
39
|
+
if (timeoutRef.current) {
|
|
40
|
+
clearTimeout(timeoutRef.current);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
timeoutRef.current = setTimeout(() => {
|
|
44
|
+
measureNode(anchorRef.current, setLayout);
|
|
45
|
+
setVisible(true);
|
|
46
|
+
}, delay);
|
|
47
|
+
}, [delay]);
|
|
48
|
+
|
|
49
|
+
const hide = React.useCallback(() => {
|
|
50
|
+
if (timeoutRef.current) {
|
|
51
|
+
clearTimeout(timeoutRef.current);
|
|
52
|
+
timeoutRef.current = null;
|
|
53
|
+
}
|
|
54
|
+
setVisible(false);
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
React.useEffect(
|
|
58
|
+
() => () => {
|
|
59
|
+
if (timeoutRef.current) {
|
|
60
|
+
clearTimeout(timeoutRef.current);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
[],
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const tooltipLeft = layout ? layout.x : 0;
|
|
67
|
+
const tooltipTop =
|
|
68
|
+
layout && placement === 'top'
|
|
69
|
+
? layout.y - animation.offset
|
|
70
|
+
: layout
|
|
71
|
+
? layout.y + layout.height + animation.offset
|
|
72
|
+
: 0;
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<View collapsable={false} ref={anchorRef} testID={testID ? `${testID}-anchor` : undefined}>
|
|
76
|
+
<Pressable
|
|
77
|
+
onBlur={hide}
|
|
78
|
+
onFocus={show}
|
|
79
|
+
onHoverIn={Platform.OS === 'web' ? show : undefined}
|
|
80
|
+
onHoverOut={Platform.OS === 'web' ? hide : undefined}
|
|
81
|
+
>
|
|
82
|
+
{children}
|
|
83
|
+
</Pressable>
|
|
84
|
+
<Portal layer="tooltip" visible={visible && Boolean(layout)}>
|
|
85
|
+
<View
|
|
86
|
+
pointerEvents="none"
|
|
87
|
+
style={{
|
|
88
|
+
left: tooltipLeft,
|
|
89
|
+
position: 'absolute',
|
|
90
|
+
top: tooltipTop,
|
|
91
|
+
}}
|
|
92
|
+
>
|
|
93
|
+
<Surface
|
|
94
|
+
p="s"
|
|
95
|
+
style={{
|
|
96
|
+
backgroundColor: theme.semantics.neutral.text,
|
|
97
|
+
}}
|
|
98
|
+
testID={testID}
|
|
99
|
+
variant="raised"
|
|
100
|
+
>
|
|
101
|
+
<Text color={theme.semantics.content.inverse} variant="caption">
|
|
102
|
+
{content}
|
|
103
|
+
</Text>
|
|
104
|
+
</Surface>
|
|
105
|
+
</View>
|
|
106
|
+
</Portal>
|
|
107
|
+
</View>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React, { createContext, useContext, useMemo, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Runtime font state consumed by theme and text primitives.
|
|
5
|
+
*/
|
|
6
|
+
export interface FontRuntime {
|
|
7
|
+
/** true when the active font assets have finished loading */
|
|
8
|
+
fontsLoaded: boolean;
|
|
9
|
+
|
|
10
|
+
/** The currently active font family id */
|
|
11
|
+
activeFontId: string | null;
|
|
12
|
+
|
|
13
|
+
/** Update the active font family id */
|
|
14
|
+
setActiveFontId: (id: string) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const fallbackRuntime: FontRuntime = {
|
|
18
|
+
fontsLoaded: true,
|
|
19
|
+
activeFontId: null,
|
|
20
|
+
setActiveFontId: () => {
|
|
21
|
+
/* fallback */
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const FontContext = createContext(fallbackRuntime);
|
|
26
|
+
|
|
27
|
+
export function FontProvider(props: {
|
|
28
|
+
fontsLoaded: boolean;
|
|
29
|
+
activeFontId?: string | null;
|
|
30
|
+
children: React.ReactNode;
|
|
31
|
+
onActiveFontChange?: (id: string) => void;
|
|
32
|
+
}) {
|
|
33
|
+
const {
|
|
34
|
+
fontsLoaded,
|
|
35
|
+
activeFontId: initialActiveFontId = null,
|
|
36
|
+
children,
|
|
37
|
+
onActiveFontChange,
|
|
38
|
+
} = props;
|
|
39
|
+
|
|
40
|
+
const [activeFontId, setActiveFontIdState] = useState(initialActiveFontId);
|
|
41
|
+
|
|
42
|
+
const value = useMemo<FontRuntime>(
|
|
43
|
+
() => ({
|
|
44
|
+
fontsLoaded,
|
|
45
|
+
activeFontId,
|
|
46
|
+
setActiveFontId: (id: string) => {
|
|
47
|
+
setActiveFontIdState(id);
|
|
48
|
+
if (onActiveFontChange) onActiveFontChange(id);
|
|
49
|
+
},
|
|
50
|
+
}),
|
|
51
|
+
[fontsLoaded, activeFontId, onActiveFontChange],
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
return <FontContext.Provider value={value}>{children}</FontContext.Provider>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function useFontContext(): FontRuntime {
|
|
58
|
+
return useContext(FontContext);
|
|
59
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React, { createContext, useContext, useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal translation runtime surface.
|
|
5
|
+
* Do not import i18next types here; keep Surface runtime-agnostic.
|
|
6
|
+
*/
|
|
7
|
+
export interface I18nInstance {
|
|
8
|
+
changeLanguage: (lng: string) => Promise<unknown>;
|
|
9
|
+
language?: string;
|
|
10
|
+
t?: Translator;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type Translator = (key: string, options?: Record<string, unknown>) => string;
|
|
14
|
+
|
|
15
|
+
export interface TranslationRuntime {
|
|
16
|
+
t: Translator;
|
|
17
|
+
i18n: I18nInstance | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const fallbackRuntime: TranslationRuntime = {
|
|
21
|
+
t: (key) => key,
|
|
22
|
+
i18n: null,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const TranslationContext = createContext(fallbackRuntime);
|
|
26
|
+
|
|
27
|
+
export function TranslationProvider(props: {
|
|
28
|
+
t: Translator;
|
|
29
|
+
i18n?: I18nInstance | null;
|
|
30
|
+
children: React.ReactNode;
|
|
31
|
+
}) {
|
|
32
|
+
const { t, i18n, children } = props;
|
|
33
|
+
|
|
34
|
+
const value = useMemo<TranslationRuntime>(
|
|
35
|
+
() => ({
|
|
36
|
+
t: (key: string, options?: Record<string, unknown>) => {
|
|
37
|
+
if (i18n?.t) {
|
|
38
|
+
const result = i18n.t(key, options);
|
|
39
|
+
// If translation returns the key, it's missing in the current dictionary
|
|
40
|
+
if (result !== key) return result;
|
|
41
|
+
}
|
|
42
|
+
return t(key, options);
|
|
43
|
+
},
|
|
44
|
+
i18n: i18n ?? null,
|
|
45
|
+
}),
|
|
46
|
+
[t, i18n, i18n?.language],
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
return <TranslationContext.Provider value={value}>{children}</TranslationContext.Provider>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function useTranslationContext(): TranslationRuntime {
|
|
53
|
+
return useContext(TranslationContext);
|
|
54
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React, { createContext, useContext, useMemo } from 'react';
|
|
2
|
+
import { useWindowDimensions } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { getBreakpointFromWidth } from './getBreakpointFromWidth';
|
|
5
|
+
import type { ResponsiveRuntime } from './types';
|
|
6
|
+
|
|
7
|
+
const ResponsiveContext = createContext<ResponsiveRuntime | null>(null);
|
|
8
|
+
|
|
9
|
+
export function ResponsiveProvider({ children }: { children: React.ReactNode }) {
|
|
10
|
+
const { width } = useWindowDimensions();
|
|
11
|
+
|
|
12
|
+
const value = useMemo<ResponsiveRuntime>(
|
|
13
|
+
() => ({
|
|
14
|
+
breakpoint: getBreakpointFromWidth(width),
|
|
15
|
+
width,
|
|
16
|
+
}),
|
|
17
|
+
[width],
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
return <ResponsiveContext.Provider value={value}>{children}</ResponsiveContext.Provider>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function useResponsiveRuntime(): ResponsiveRuntime {
|
|
24
|
+
const runtime = useContext(ResponsiveContext);
|
|
25
|
+
|
|
26
|
+
if (!runtime) {
|
|
27
|
+
throw new Error('useResponsiveRuntime must be used within a ResponsiveProvider');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return runtime;
|
|
31
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { getBreakpointFromWidth } from './getBreakpointFromWidth';
|
|
4
|
+
|
|
5
|
+
describe('getBreakpointFromWidth', () => {
|
|
6
|
+
test('resolves expected breakpoints', () => {
|
|
7
|
+
expect(getBreakpointFromWidth(0)).toBe('base');
|
|
8
|
+
expect(getBreakpointFromWidth(479)).toBe('base');
|
|
9
|
+
expect(getBreakpointFromWidth(480)).toBe('sm');
|
|
10
|
+
expect(getBreakpointFromWidth(767)).toBe('sm');
|
|
11
|
+
expect(getBreakpointFromWidth(768)).toBe('md');
|
|
12
|
+
expect(getBreakpointFromWidth(1024)).toBe('lg');
|
|
13
|
+
expect(getBreakpointFromWidth(1280)).toBe('xl');
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { BREAKPOINT_ORDER, BREAKPOINTS } from './breakpoints';
|
|
2
|
+
import type { Breakpoint } from './types';
|
|
3
|
+
|
|
4
|
+
export function getBreakpointFromWidth(width: number): Breakpoint {
|
|
5
|
+
let active: Breakpoint = 'base';
|
|
6
|
+
for (const key of BREAKPOINT_ORDER) {
|
|
7
|
+
if (width >= BREAKPOINTS[key]) active = key;
|
|
8
|
+
}
|
|
9
|
+
return active;
|
|
10
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { BREAKPOINT_ORDER, BREAKPOINTS } from './breakpoints';
|
|
2
|
+
export { getBreakpointFromWidth } from './getBreakpointFromWidth';
|
|
3
|
+
export { resolveResponsive } from './resolve';
|
|
4
|
+
export { ResponsiveProvider, useResponsiveRuntime } from './ResponsiveProvider';
|
|
5
|
+
export type { Breakpoint, Responsive, ResponsiveRuntime } from './types';
|
|
6
|
+
export { useBreakpoint } from './useBreakpoint';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { resolveResponsive } from './resolve';
|
|
4
|
+
|
|
5
|
+
describe('resolveResponsive', () => {
|
|
6
|
+
test('returns plain value when non-responsive', () => {
|
|
7
|
+
expect(resolveResponsive(12, 'md')).toBe(12);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test('resolves value at current breakpoint', () => {
|
|
11
|
+
expect(resolveResponsive({ base: 8, md: 16 }, 'md')).toBe(16);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('falls back to nearest lower breakpoint', () => {
|
|
15
|
+
expect(resolveResponsive({ base: 8, md: 16 }, 'lg')).toBe(16);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('falls back to base when no lower explicit value exists', () => {
|
|
19
|
+
expect(resolveResponsive({ base: 8, xl: 24 }, 'md')).toBe(8);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('returns undefined when value is undefined', () => {
|
|
23
|
+
expect(resolveResponsive(undefined, 'md')).toBeUndefined();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { BREAKPOINT_ORDER } from './breakpoints';
|
|
2
|
+
import type { Breakpoint, Responsive } from './types';
|
|
3
|
+
|
|
4
|
+
function isResponsiveRecord<T>(value: Responsive<T>): value is Partial<Record<Breakpoint, T>> {
|
|
5
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function resolveResponsive<T>(
|
|
9
|
+
value: Responsive<T> | undefined,
|
|
10
|
+
breakpoint: Breakpoint,
|
|
11
|
+
): T | undefined {
|
|
12
|
+
if (value === undefined) return undefined;
|
|
13
|
+
if (!isResponsiveRecord(value)) return value;
|
|
14
|
+
|
|
15
|
+
const activeIndex = BREAKPOINT_ORDER.indexOf(breakpoint);
|
|
16
|
+
for (let i = activeIndex; i >= 0; i -= 1) {
|
|
17
|
+
const key = BREAKPOINT_ORDER[i];
|
|
18
|
+
if (!key) continue;
|
|
19
|
+
const candidate = value[key];
|
|
20
|
+
if (candidate !== undefined) return candidate;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { BREAKPOINTS } from './breakpoints';
|
|
2
|
+
|
|
3
|
+
export type Breakpoint = keyof typeof BREAKPOINTS;
|
|
4
|
+
|
|
5
|
+
export type Responsive<T> = T | Partial<Record<Breakpoint, T>>;
|
|
6
|
+
|
|
7
|
+
export interface ResponsiveRuntime {
|
|
8
|
+
breakpoint: Breakpoint;
|
|
9
|
+
width: number;
|
|
10
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { useWindowDimensions } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { getBreakpointFromWidth } from './getBreakpointFromWidth';
|
|
4
|
+
import type { Breakpoint } from './types';
|
|
5
|
+
|
|
6
|
+
export function useBreakpoint(): Breakpoint {
|
|
7
|
+
const { width } = useWindowDimensions();
|
|
8
|
+
return getBreakpointFromWidth(width);
|
|
9
|
+
}
|