@boostdev/design-system-react-native 0.1.0 → 0.2.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/CHANGELOG.md +55 -0
- package/LICENSE +15 -0
- package/README.md +104 -2
- package/dist/index.cjs +106 -60
- package/dist/index.d.cts +61 -6
- package/dist/index.d.ts +61 -6
- package/dist/index.js +106 -60
- package/package.json +30 -10
- package/src/components/interaction/Button/Button.mdx +91 -0
- package/src/components/interaction/Button/Button.spec.tsx +55 -0
- package/src/components/interaction/Button/Button.stories.tsx +141 -0
- package/src/components/interaction/Button/Button.tsx +105 -0
- package/src/components/interaction/Dialog/Dialog.mdx +61 -0
- package/src/components/interaction/Dialog/Dialog.spec.tsx +73 -0
- package/src/components/interaction/Dialog/Dialog.stories.tsx +53 -0
- package/src/components/interaction/Dialog/Dialog.tsx +128 -0
- package/src/components/interaction/DropdownMenu/DropdownMenu.mdx +74 -0
- package/src/components/interaction/DropdownMenu/DropdownMenu.spec.tsx +78 -0
- package/src/components/interaction/DropdownMenu/DropdownMenu.stories.tsx +51 -0
- package/src/components/interaction/DropdownMenu/DropdownMenu.tsx +254 -0
- package/src/components/interaction/Popover/Popover.mdx +61 -0
- package/src/components/interaction/Popover/Popover.spec.tsx +73 -0
- package/src/components/interaction/Popover/Popover.stories.tsx +44 -0
- package/src/components/interaction/Popover/Popover.tsx +87 -0
- package/src/components/interaction/Rating/Rating.mdx +55 -0
- package/src/components/interaction/Rating/Rating.spec.tsx +38 -0
- package/src/components/interaction/Rating/Rating.stories.tsx +37 -0
- package/src/components/interaction/Rating/Rating.tsx +50 -0
- package/src/components/interaction/SideSheet/SideSheet.mdx +60 -0
- package/src/components/interaction/SideSheet/SideSheet.spec.tsx +80 -0
- package/src/components/interaction/SideSheet/SideSheet.stories.tsx +33 -0
- package/src/components/interaction/SideSheet/SideSheet.tsx +175 -0
- package/src/components/interaction/Toast/Toast.mdx +81 -0
- package/src/components/interaction/Toast/Toast.spec.tsx +80 -0
- package/src/components/interaction/Toast/Toast.stories.tsx +47 -0
- package/src/components/interaction/Toast/Toast.tsx +202 -0
- package/src/components/interaction/form/Checkbox/Checkbox.mdx +74 -0
- package/src/components/interaction/form/Checkbox/Checkbox.spec.tsx +42 -0
- package/src/components/interaction/form/Checkbox/Checkbox.stories.tsx +34 -0
- package/src/components/interaction/form/Checkbox/Checkbox.tsx +80 -0
- package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.mdx +78 -0
- package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.spec.tsx +57 -0
- package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.stories.tsx +96 -0
- package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.tsx +61 -0
- package/src/components/interaction/form/Combobox/Combobox.mdx +86 -0
- package/src/components/interaction/form/Combobox/Combobox.spec.tsx +60 -0
- package/src/components/interaction/form/Combobox/Combobox.stories.tsx +99 -0
- package/src/components/interaction/form/Combobox/Combobox.tsx +212 -0
- package/src/components/interaction/form/FileInput/FileInput.mdx +86 -0
- package/src/components/interaction/form/FileInput/FileInput.spec.tsx +73 -0
- package/src/components/interaction/form/FileInput/FileInput.stories.tsx +69 -0
- package/src/components/interaction/form/FileInput/FileInput.tsx +129 -0
- package/src/components/interaction/form/FormInput/FormInput.mdx +81 -0
- package/src/components/interaction/form/FormInput/FormInput.spec.tsx +81 -0
- package/src/components/interaction/form/FormInput/FormInput.stories.tsx +54 -0
- package/src/components/interaction/form/FormInput/FormInput.tsx +123 -0
- package/src/components/interaction/form/NumberInput/NumberInput.mdx +87 -0
- package/src/components/interaction/form/NumberInput/NumberInput.spec.tsx +63 -0
- package/src/components/interaction/form/NumberInput/NumberInput.stories.tsx +63 -0
- package/src/components/interaction/form/NumberInput/NumberInput.tsx +199 -0
- package/src/components/interaction/form/Radio/Radio.spec.tsx +79 -0
- package/src/components/interaction/form/Radio/Radio.stories.tsx +44 -0
- package/src/components/interaction/form/Radio/Radio.tsx +79 -0
- package/src/components/interaction/form/RadioGroup/RadioGroup.mdx +79 -0
- package/src/components/interaction/form/RadioGroup/RadioGroup.spec.tsx +66 -0
- package/src/components/interaction/form/RadioGroup/RadioGroup.stories.tsx +96 -0
- package/src/components/interaction/form/RadioGroup/RadioGroup.tsx +61 -0
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.mdx +78 -0
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.spec.tsx +82 -0
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.stories.tsx +99 -0
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.tsx +121 -0
- package/src/components/interaction/form/Select/Select.mdx +85 -0
- package/src/components/interaction/form/Select/Select.spec.tsx +64 -0
- package/src/components/interaction/form/Select/Select.stories.tsx +75 -0
- package/src/components/interaction/form/Select/Select.tsx +252 -0
- package/src/components/interaction/form/Slider/Slider.mdx +83 -0
- package/src/components/interaction/form/Slider/Slider.spec.tsx +51 -0
- package/src/components/interaction/form/Slider/Slider.stories.tsx +58 -0
- package/src/components/interaction/form/Slider/Slider.tsx +188 -0
- package/src/components/interaction/form/Switch/Switch.spec.tsx +60 -0
- package/src/components/interaction/form/Switch/Switch.stories.tsx +33 -0
- package/src/components/interaction/form/Switch/Switch.tsx +59 -0
- package/src/components/interaction/form/Textarea/Textarea.mdx +76 -0
- package/src/components/interaction/form/Textarea/Textarea.spec.tsx +50 -0
- package/src/components/interaction/form/Textarea/Textarea.stories.tsx +50 -0
- package/src/components/interaction/form/Textarea/Textarea.tsx +108 -0
- package/src/components/interaction/form/atoms/Label.spec.tsx +39 -0
- package/src/components/interaction/form/atoms/Label.stories.tsx +18 -0
- package/src/components/interaction/form/atoms/Label.tsx +29 -0
- package/src/components/interaction/form/atoms/Message.spec.tsx +30 -0
- package/src/components/interaction/form/atoms/Message.stories.tsx +22 -0
- package/src/components/interaction/form/atoms/Message.tsx +34 -0
- package/src/components/layout/ButtonGroup/ButtonGroup.spec.tsx +83 -0
- package/src/components/layout/ButtonGroup/ButtonGroup.stories.tsx +44 -0
- package/src/components/layout/ButtonGroup/ButtonGroup.tsx +44 -0
- package/src/components/layout/Card/Card.spec.tsx +102 -0
- package/src/components/layout/Card/Card.stories.tsx +51 -0
- package/src/components/layout/Card/Card.tsx +89 -0
- package/src/components/layout/IconWrapper/IconWrapper.spec.tsx +39 -0
- package/src/components/layout/IconWrapper/IconWrapper.stories.tsx +37 -0
- package/src/components/layout/IconWrapper/IconWrapper.tsx +20 -0
- package/src/components/layout/SectionHeader/SectionHeader.spec.tsx +51 -0
- package/src/components/layout/SectionHeader/SectionHeader.stories.tsx +36 -0
- package/src/components/layout/SectionHeader/SectionHeader.tsx +79 -0
- package/src/components/ui/Accordion/Accordion.mdx +65 -0
- package/src/components/ui/Accordion/Accordion.spec.tsx +69 -0
- package/src/components/ui/Accordion/Accordion.stories.tsx +52 -0
- package/src/components/ui/Accordion/Accordion.tsx +141 -0
- package/src/components/ui/Alert/Alert.mdx +62 -0
- package/src/components/ui/Alert/Alert.spec.tsx +76 -0
- package/src/components/ui/Alert/Alert.stories.tsx +41 -0
- package/src/components/ui/Alert/Alert.tsx +94 -0
- package/src/components/ui/Avatar/Avatar.mdx +50 -0
- package/src/components/ui/Avatar/Avatar.spec.tsx +47 -0
- package/src/components/ui/Avatar/Avatar.stories.tsx +32 -0
- package/src/components/ui/Avatar/Avatar.tsx +85 -0
- package/src/components/ui/Badge/Badge.mdx +54 -0
- package/src/components/ui/Badge/Badge.spec.tsx +45 -0
- package/src/components/ui/Badge/Badge.stories.tsx +34 -0
- package/src/components/ui/Badge/Badge.tsx +50 -0
- package/src/components/ui/Breadcrumb/Breadcrumb.mdx +52 -0
- package/src/components/ui/Breadcrumb/Breadcrumb.spec.tsx +49 -0
- package/src/components/ui/Breadcrumb/Breadcrumb.stories.tsx +46 -0
- package/src/components/ui/Breadcrumb/Breadcrumb.tsx +83 -0
- package/src/components/ui/Calendar/Calendar.mdx +58 -0
- package/src/components/ui/Calendar/Calendar.spec.tsx +92 -0
- package/src/components/ui/Calendar/Calendar.stories.tsx +45 -0
- package/src/components/ui/Calendar/Calendar.tsx +294 -0
- package/src/components/ui/Carousel/Carousel.mdx +55 -0
- package/src/components/ui/Carousel/Carousel.spec.tsx +55 -0
- package/src/components/ui/Carousel/Carousel.stories.tsx +59 -0
- package/src/components/ui/Carousel/Carousel.tsx +184 -0
- package/src/components/ui/Collapsible/Collapsible.mdx +53 -0
- package/src/components/ui/Collapsible/Collapsible.spec.tsx +68 -0
- package/src/components/ui/Collapsible/Collapsible.stories.tsx +48 -0
- package/src/components/ui/Collapsible/Collapsible.tsx +139 -0
- package/src/components/ui/DescriptionList/DescriptionList.mdx +50 -0
- package/src/components/ui/DescriptionList/DescriptionList.spec.tsx +37 -0
- package/src/components/ui/DescriptionList/DescriptionList.stories.tsx +48 -0
- package/src/components/ui/DescriptionList/DescriptionList.tsx +54 -0
- package/src/components/ui/Link/Link.mdx +53 -0
- package/src/components/ui/Link/Link.spec.tsx +35 -0
- package/src/components/ui/Link/Link.stories.tsx +46 -0
- package/src/components/ui/Link/Link.tsx +48 -0
- package/src/components/ui/Loading/Loading.spec.tsx +24 -0
- package/src/components/ui/Loading/Loading.stories.tsx +31 -0
- package/src/components/ui/Loading/Loading.tsx +29 -0
- package/src/components/ui/NotificationBanner/NotificationBanner.spec.tsx +85 -0
- package/src/components/ui/NotificationBanner/NotificationBanner.stories.tsx +37 -0
- package/src/components/ui/NotificationBanner/NotificationBanner.tsx +76 -0
- package/src/components/ui/Pagination/Pagination.mdx +62 -0
- package/src/components/ui/Pagination/Pagination.spec.tsx +86 -0
- package/src/components/ui/Pagination/Pagination.stories.tsx +47 -0
- package/src/components/ui/Pagination/Pagination.tsx +157 -0
- package/src/components/ui/Progress/Progress.spec.tsx +56 -0
- package/src/components/ui/Progress/Progress.stories.tsx +34 -0
- package/src/components/ui/Progress/Progress.tsx +84 -0
- package/src/components/ui/ProgressCircle/ProgressCircle.mdx +63 -0
- package/src/components/ui/ProgressCircle/ProgressCircle.spec.tsx +39 -0
- package/src/components/ui/ProgressCircle/ProgressCircle.stories.tsx +36 -0
- package/src/components/ui/ProgressCircle/ProgressCircle.tsx +176 -0
- package/src/components/ui/Separator/Separator.spec.tsx +39 -0
- package/src/components/ui/Separator/Separator.stories.tsx +38 -0
- package/src/components/ui/Separator/Separator.tsx +32 -0
- package/src/components/ui/Skeleton/Skeleton.spec.tsx +39 -0
- package/src/components/ui/Skeleton/Skeleton.stories.tsx +31 -0
- package/src/components/ui/Skeleton/Skeleton.tsx +42 -0
- package/src/components/ui/Table/Table.mdx +72 -0
- package/src/components/ui/Table/Table.spec.tsx +83 -0
- package/src/components/ui/Table/Table.stories.tsx +55 -0
- package/src/components/ui/Table/Table.tsx +196 -0
- package/src/components/ui/Tabs/Tabs.mdx +65 -0
- package/src/components/ui/Tabs/Tabs.spec.tsx +65 -0
- package/src/components/ui/Tabs/Tabs.stories.tsx +57 -0
- package/src/components/ui/Tabs/Tabs.tsx +133 -0
- package/src/components/ui/Tooltip/Tooltip.mdx +47 -0
- package/src/components/ui/Tooltip/Tooltip.spec.tsx +37 -0
- package/src/components/ui/Tooltip/Tooltip.stories.tsx +38 -0
- package/src/components/ui/Tooltip/Tooltip.tsx +57 -0
- package/src/components/ui/Typography/Typography.mdx +56 -0
- package/src/components/ui/Typography/Typography.spec.tsx +49 -0
- package/src/components/ui/Typography/Typography.stories.tsx +36 -0
- package/src/components/ui/Typography/Typography.tsx +65 -0
- package/src/index.ts +86 -0
- package/src/native/ReactNavigationTheme.mdx +140 -0
- package/src/native/ReactNavigationTheme.spec.tsx +35 -0
- package/src/native/ReactNavigationTheme.ts +102 -0
- package/src/native/ThemeContext.spec.tsx +57 -0
- package/src/native/ThemeContext.tsx +48 -0
- package/src/native/tokens.ts +13 -0
- package/src/test/setup.ts +1 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { Meta } from '@storybook/blocks';
|
|
2
|
+
|
|
3
|
+
<Meta title="Theming/React Navigation" />
|
|
4
|
+
|
|
5
|
+
# Theming
|
|
6
|
+
|
|
7
|
+
`@boostdev/design-system-react-native` is designed to work two ways:
|
|
8
|
+
|
|
9
|
+
1. **Standalone** — wrap in `ThemeProvider` and use components directly. No other dependencies.
|
|
10
|
+
2. **Alongside React Navigation** — opt into a bridge that produces a React Navigation–compatible `Theme` from the same foundation tokens.
|
|
11
|
+
|
|
12
|
+
Both paths share the same source of truth: tokens from `@boostdev/design-system-foundation`. The mapping from those tokens to React Navigation's `Theme` shape lives in this repo, not the foundation.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Standalone
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
import { ThemeProvider, useTheme, Button } from '@boostdev/design-system-react-native';
|
|
20
|
+
|
|
21
|
+
export default function App() {
|
|
22
|
+
return (
|
|
23
|
+
<ThemeProvider>
|
|
24
|
+
<Button onPress={() => {}}>Press me</Button>
|
|
25
|
+
</ThemeProvider>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`ThemeProvider` auto-detects the system color scheme via React Native's `useColorScheme`. Force a scheme with `colorScheme="light" | "dark"`.
|
|
31
|
+
|
|
32
|
+
Inside the tree:
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
const { colors, isDark } = useTheme();
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`colors` is the full foundation token palette (`colorBg`, `colorOnBg`, `colorInteractive`, …) — a superset of what React Navigation expects.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## React Navigation bridge
|
|
43
|
+
|
|
44
|
+
`@react-navigation/native` is an **optional** peer dependency. You only need to install it if you use the bridge.
|
|
45
|
+
|
|
46
|
+
### `useReactNavigationTheme()` — when wrapped in `ThemeProvider`
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
import { NavigationContainer } from '@react-navigation/native';
|
|
50
|
+
import { ThemeProvider, useReactNavigationTheme } from '@boostdev/design-system-react-native';
|
|
51
|
+
|
|
52
|
+
function NavRoot({ children }) {
|
|
53
|
+
const theme = useReactNavigationTheme();
|
|
54
|
+
return <NavigationContainer theme={theme}>{children}</NavigationContainer>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default function App() {
|
|
58
|
+
return (
|
|
59
|
+
<ThemeProvider>
|
|
60
|
+
<NavRoot>{/* screens */}</NavRoot>
|
|
61
|
+
</ThemeProvider>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Reflects light/dark switches automatically. Accepts optional `{ colors, fonts }` overrides.
|
|
67
|
+
|
|
68
|
+
### `createReactNavigationTheme()` — pure builder
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
import { createReactNavigationTheme } from '@boostdev/design-system-react-native';
|
|
72
|
+
|
|
73
|
+
const lightTheme = createReactNavigationTheme({ dark: false });
|
|
74
|
+
const darkTheme = createReactNavigationTheme({ dark: true });
|
|
75
|
+
|
|
76
|
+
const branded = createReactNavigationTheme({
|
|
77
|
+
dark: false,
|
|
78
|
+
colors: { primary: '#ff5500' },
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Use this when you do not wrap in `ThemeProvider`, or when you need a theme outside React (tests, navigators created at module scope, etc.).
|
|
83
|
+
|
|
84
|
+
### Options
|
|
85
|
+
|
|
86
|
+
| Option | Type | Description |
|
|
87
|
+
| -------- | ------------------------------------- | ------------------------------------------------------------------------ |
|
|
88
|
+
| `dark` | `boolean` | Selects the dark token set. Default `false`. |
|
|
89
|
+
| `tokens` | foundation `colors.light` shape | Bring your own token set. Defaults to the foundation's built-in palette. |
|
|
90
|
+
| `colors` | `Partial<ReactNavigationThemeColors>` | Override individual Navigation colors. |
|
|
91
|
+
| `fonts` | `Partial<ReactNavigationThemeFonts>` | Override individual Navigation font styles. |
|
|
92
|
+
|
|
93
|
+
### Token mapping
|
|
94
|
+
|
|
95
|
+
The bridge maps foundation tokens to React Navigation's six-color palette:
|
|
96
|
+
|
|
97
|
+
| React Navigation | Foundation token |
|
|
98
|
+
| ---------------- | ----------------------------------------------- |
|
|
99
|
+
| `primary` | `colorInteractive` |
|
|
100
|
+
| `background` | `colorBg` |
|
|
101
|
+
| `card` | `colorBgSubtle` |
|
|
102
|
+
| `text` | `colorOnBg` |
|
|
103
|
+
| `border` | `palette.grey200` (light) / `grey700` (dark) |
|
|
104
|
+
| `notification` | `colorError` |
|
|
105
|
+
|
|
106
|
+
Fonts use `font.family.body` with weights from `font.weight` (`body`, `medium`, `bold`, `heading`).
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Custom themes
|
|
111
|
+
|
|
112
|
+
Author a token set matching the foundation's `colors.light` / `colors.dark` shape, then pass it through the bridge:
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
import { createReactNavigationTheme } from '@boostdev/design-system-react-native';
|
|
116
|
+
import { colors } from '@boostdev/design-system-foundation/native';
|
|
117
|
+
|
|
118
|
+
const myTokens = { ...colors.light, colorInteractive: '#ff5500' };
|
|
119
|
+
const theme = createReactNavigationTheme({ tokens: myTokens, dark: false });
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The foundation package stays untouched — all theme assembly lives in this repo.
|
|
123
|
+
|
|
124
|
+
### Brand overrides flow through automatically
|
|
125
|
+
|
|
126
|
+
`useReactNavigationTheme()` reads the colors from `ThemeProvider`, so any `tokens`
|
|
127
|
+
overrides you pass there (e.g. a brand `colorInteractive`) are reflected in the
|
|
128
|
+
navigation theme's `primary`, `background`, `card`, `text` and `notification`
|
|
129
|
+
colors without extra wiring. With expo-router, pass the result to
|
|
130
|
+
`ThemeProvider` from `@react-navigation/native` in your root layout:
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
import { ThemeProvider as NavThemeProvider } from '@react-navigation/native';
|
|
134
|
+
import { useReactNavigationTheme } from '@boostdev/design-system-react-native';
|
|
135
|
+
|
|
136
|
+
function RootNavigationTheme({ children }: { children: React.ReactNode }) {
|
|
137
|
+
const navTheme = useReactNavigationTheme();
|
|
138
|
+
return <NavThemeProvider value={navTheme}>{children}</NavThemeProvider>;
|
|
139
|
+
}
|
|
140
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { Text } from 'react-native';
|
|
3
|
+
import { colors } from './tokens';
|
|
4
|
+
import { ThemeProvider } from './ThemeContext';
|
|
5
|
+
import { createReactNavigationTheme, useReactNavigationTheme } from './ReactNavigationTheme';
|
|
6
|
+
|
|
7
|
+
function NavProbe() {
|
|
8
|
+
const theme = useReactNavigationTheme();
|
|
9
|
+
return (
|
|
10
|
+
<>
|
|
11
|
+
<Text testID="primary">{theme.colors.primary}</Text>
|
|
12
|
+
<Text testID="dark">{String(theme.dark)}</Text>
|
|
13
|
+
</>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('useReactNavigationTheme', () => {
|
|
18
|
+
it('maps ThemeProvider token overrides into the nav theme primary color', () => {
|
|
19
|
+
render(
|
|
20
|
+
<ThemeProvider tokens={{ light: { colorInteractive: '#00c853' } }}>
|
|
21
|
+
<NavProbe />
|
|
22
|
+
</ThemeProvider>,
|
|
23
|
+
);
|
|
24
|
+
expect(screen.getByTestId('primary')).toHaveTextContent('#00c853');
|
|
25
|
+
expect(screen.getByTestId('dark')).toHaveTextContent('false');
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('createReactNavigationTheme', () => {
|
|
30
|
+
it('builds from foundation colors when no tokens passed', () => {
|
|
31
|
+
const theme = createReactNavigationTheme();
|
|
32
|
+
expect(theme.colors.primary).toBe(colors.light.colorInteractive);
|
|
33
|
+
expect(theme.dark).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { palette, colors as foundationColors, font } from '@boostdev/design-system-foundation/native';
|
|
2
|
+
import { useTheme, type ThemeColors } from './ThemeContext';
|
|
3
|
+
|
|
4
|
+
export interface ReactNavigationFontStyle {
|
|
5
|
+
fontFamily: string;
|
|
6
|
+
fontWeight:
|
|
7
|
+
| 'normal'
|
|
8
|
+
| 'bold'
|
|
9
|
+
| '100'
|
|
10
|
+
| '200'
|
|
11
|
+
| '300'
|
|
12
|
+
| '400'
|
|
13
|
+
| '500'
|
|
14
|
+
| '600'
|
|
15
|
+
| '700'
|
|
16
|
+
| '800'
|
|
17
|
+
| '900';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ReactNavigationThemeColors {
|
|
21
|
+
primary: string;
|
|
22
|
+
background: string;
|
|
23
|
+
card: string;
|
|
24
|
+
text: string;
|
|
25
|
+
border: string;
|
|
26
|
+
notification: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ReactNavigationThemeFonts {
|
|
30
|
+
regular: ReactNavigationFontStyle;
|
|
31
|
+
medium: ReactNavigationFontStyle;
|
|
32
|
+
bold: ReactNavigationFontStyle;
|
|
33
|
+
heavy: ReactNavigationFontStyle;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ReactNavigationTheme {
|
|
37
|
+
dark: boolean;
|
|
38
|
+
colors: ReactNavigationThemeColors;
|
|
39
|
+
fonts: ReactNavigationThemeFonts;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface CreateReactNavigationThemeOptions {
|
|
43
|
+
dark?: boolean;
|
|
44
|
+
tokens?: ThemeColors;
|
|
45
|
+
colors?: Partial<ReactNavigationThemeColors>;
|
|
46
|
+
fonts?: Partial<ReactNavigationThemeFonts>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function mapColors(tokens: ThemeColors, dark: boolean): ReactNavigationThemeColors {
|
|
50
|
+
return {
|
|
51
|
+
primary: tokens.colorInteractive,
|
|
52
|
+
background: tokens.colorBg,
|
|
53
|
+
card: tokens.colorBgSubtle,
|
|
54
|
+
text: tokens.colorOnBg,
|
|
55
|
+
border: dark ? palette.grey700 : palette.grey200,
|
|
56
|
+
notification: tokens.colorError,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function mapFonts(): ReactNavigationThemeFonts {
|
|
61
|
+
const family = font.family.body;
|
|
62
|
+
return {
|
|
63
|
+
regular: { fontFamily: family, fontWeight: String(font.weight.body) as ReactNavigationFontStyle['fontWeight'] },
|
|
64
|
+
medium: { fontFamily: family, fontWeight: String(font.weight.medium) as ReactNavigationFontStyle['fontWeight'] },
|
|
65
|
+
bold: { fontFamily: family, fontWeight: String(font.weight.bold) as ReactNavigationFontStyle['fontWeight'] },
|
|
66
|
+
heavy: { fontFamily: family, fontWeight: String(font.weight.heading) as ReactNavigationFontStyle['fontWeight'] },
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Pure builder. Produces a React Navigation–compatible theme from the design
|
|
72
|
+
* system foundation tokens. Use this when you are not wrapping in our
|
|
73
|
+
* `ThemeProvider`, or when you need a theme outside React.
|
|
74
|
+
*/
|
|
75
|
+
export function createReactNavigationTheme(
|
|
76
|
+
options: CreateReactNavigationThemeOptions = {},
|
|
77
|
+
): ReactNavigationTheme {
|
|
78
|
+
const dark = options.dark ?? false;
|
|
79
|
+
const tokens = options.tokens ?? (dark ? foundationColors.dark : foundationColors.light);
|
|
80
|
+
return {
|
|
81
|
+
dark,
|
|
82
|
+
colors: { ...mapColors(tokens, dark), ...options.colors },
|
|
83
|
+
fonts: { ...mapFonts(), ...options.fonts },
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Hook variant. Reads from the design system's `ThemeProvider` and returns a
|
|
89
|
+
* React Navigation–compatible theme. Pass the result directly to
|
|
90
|
+
* `<NavigationContainer theme={...} />`.
|
|
91
|
+
*/
|
|
92
|
+
export function useReactNavigationTheme(
|
|
93
|
+
overrides: Pick<CreateReactNavigationThemeOptions, 'colors' | 'fonts'> = {},
|
|
94
|
+
): ReactNavigationTheme {
|
|
95
|
+
const { colors: tokens, isDark } = useTheme();
|
|
96
|
+
return createReactNavigationTheme({
|
|
97
|
+
dark: isDark,
|
|
98
|
+
tokens,
|
|
99
|
+
colors: overrides.colors,
|
|
100
|
+
fonts: overrides.fonts,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { Text } from 'react-native';
|
|
3
|
+
import { colors } from './tokens';
|
|
4
|
+
import { ThemeProvider, useTheme } from './ThemeContext';
|
|
5
|
+
|
|
6
|
+
function Probe() {
|
|
7
|
+
const { colors: themeColors, isDark } = useTheme();
|
|
8
|
+
return (
|
|
9
|
+
<>
|
|
10
|
+
<Text testID="interactive">{themeColors.colorInteractive}</Text>
|
|
11
|
+
<Text testID="bg">{themeColors.colorBg}</Text>
|
|
12
|
+
<Text testID="mode">{isDark ? 'dark' : 'light'}</Text>
|
|
13
|
+
</>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('ThemeProvider token overrides', () => {
|
|
18
|
+
it('provides foundation colors when no overrides given', () => {
|
|
19
|
+
render(
|
|
20
|
+
<ThemeProvider>
|
|
21
|
+
<Probe />
|
|
22
|
+
</ThemeProvider>,
|
|
23
|
+
);
|
|
24
|
+
expect(screen.getByTestId('interactive')).toHaveTextContent(colors.light.colorInteractive);
|
|
25
|
+
expect(screen.getByTestId('bg')).toHaveTextContent(colors.light.colorBg);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('merges light overrides over the foundation light colors', () => {
|
|
29
|
+
render(
|
|
30
|
+
<ThemeProvider tokens={{ light: { colorInteractive: '#00c853' } }}>
|
|
31
|
+
<Probe />
|
|
32
|
+
</ThemeProvider>,
|
|
33
|
+
);
|
|
34
|
+
expect(screen.getByTestId('interactive')).toHaveTextContent('#00c853');
|
|
35
|
+
// untouched tokens keep their foundation values
|
|
36
|
+
expect(screen.getByTestId('bg')).toHaveTextContent(colors.light.colorBg);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('applies dark overrides when colorScheme is dark', () => {
|
|
40
|
+
render(
|
|
41
|
+
<ThemeProvider colorScheme="dark" tokens={{ dark: { colorInteractive: '#00c853' } }}>
|
|
42
|
+
<Probe />
|
|
43
|
+
</ThemeProvider>,
|
|
44
|
+
);
|
|
45
|
+
expect(screen.getByTestId('mode')).toHaveTextContent('dark');
|
|
46
|
+
expect(screen.getByTestId('interactive')).toHaveTextContent('#00c853');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('does not apply light overrides in the dark scheme', () => {
|
|
50
|
+
render(
|
|
51
|
+
<ThemeProvider colorScheme="dark" tokens={{ light: { colorInteractive: '#00c853' } }}>
|
|
52
|
+
<Probe />
|
|
53
|
+
</ThemeProvider>,
|
|
54
|
+
);
|
|
55
|
+
expect(screen.getByTestId('interactive')).toHaveTextContent(colors.dark.colorInteractive);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createContext, ReactNode, useContext, useMemo } from 'react';
|
|
2
|
+
import { useColorScheme } from 'react-native';
|
|
3
|
+
import { colors } from './tokens';
|
|
4
|
+
|
|
5
|
+
export type ThemeColorName = keyof typeof colors.light;
|
|
6
|
+
|
|
7
|
+
/** Semantic theme colors, widened to `string` so brand overrides typecheck. */
|
|
8
|
+
export type ThemeColors = Readonly<Record<ThemeColorName, string>>;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Per-scheme token overrides, merged over the foundation colors.
|
|
12
|
+
* The React Native equivalent of overriding tokens in the web
|
|
13
|
+
* `boostdev.tokens.override` CSS layer.
|
|
14
|
+
*/
|
|
15
|
+
export interface ThemeTokenOverrides {
|
|
16
|
+
light?: Partial<Record<ThemeColorName, string>>;
|
|
17
|
+
dark?: Partial<Record<ThemeColorName, string>>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface ThemeContextValue {
|
|
21
|
+
colors: ThemeColors;
|
|
22
|
+
isDark: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
|
26
|
+
|
|
27
|
+
interface ThemeProviderProps {
|
|
28
|
+
children: ReactNode;
|
|
29
|
+
colorScheme?: 'light' | 'dark';
|
|
30
|
+
tokens?: ThemeTokenOverrides;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function ThemeProvider({ children, colorScheme, tokens }: ThemeProviderProps) {
|
|
34
|
+
const systemScheme = useColorScheme();
|
|
35
|
+
const isDark = (colorScheme ?? systemScheme) === 'dark';
|
|
36
|
+
const value = useMemo<ThemeContextValue>(() => {
|
|
37
|
+
const base = isDark ? colors.dark : colors.light;
|
|
38
|
+
const overrides = isDark ? tokens?.dark : tokens?.light;
|
|
39
|
+
return { colors: { ...base, ...overrides }, isDark };
|
|
40
|
+
}, [isDark, tokens]);
|
|
41
|
+
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function useTheme(): ThemeContextValue {
|
|
45
|
+
const ctx = useContext(ThemeContext);
|
|
46
|
+
if (!ctx) throw new Error('useTheme must be used within ThemeProvider');
|
|
47
|
+
return ctx;
|
|
48
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { colors, spacing, font, border, zIndex, animation } from '@boostdev/design-system-foundation/native';
|
|
2
|
+
|
|
3
|
+
export { colors, spacing, font, border, zIndex, animation };
|
|
4
|
+
|
|
5
|
+
/** Absolute lineHeight values for React Native (ratio × fontSize). */
|
|
6
|
+
export const lineHeight = {
|
|
7
|
+
display: Math.round(font.size.display * font.lineHeight.display),
|
|
8
|
+
heading1: Math.round(font.size.heading1 * font.lineHeight.heading),
|
|
9
|
+
heading2: Math.round(font.size.heading2 * font.lineHeight.heading),
|
|
10
|
+
heading3: Math.round(font.size.heading3 * font.lineHeight.heading),
|
|
11
|
+
body: Math.round(font.size.body * font.lineHeight.body),
|
|
12
|
+
bodyS: Math.round(font.size.bodyS * font.lineHeight.body),
|
|
13
|
+
} as const;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|