@compsych/mobile-ui 1.0.11 → 1.0.14
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/package.json +2 -1
- package/src/components/ActionSheet/index.tsx +124 -57
- package/src/components/Alert/index.tsx +19 -10
- package/src/components/Avatar/index.tsx +95 -92
- package/src/components/Badge/index.tsx +39 -36
- package/src/components/BodyText/index.tsx +5 -5
- package/src/components/Breadcrumb/index.tsx +32 -30
- package/src/components/Button/Button.test.tsx +13 -7
- package/src/components/Button/index.tsx +114 -92
- package/src/components/ChatBubble/ChatBubble.test.tsx +77 -0
- package/src/components/ChatBubble/index.tsx +187 -0
- package/src/components/ChatInput/ChatInput.test.tsx +90 -0
- package/src/components/ChatInput/index.tsx +263 -0
- package/src/components/Checkbox/index.tsx +24 -21
- package/src/components/Chip/index.tsx +75 -72
- package/src/components/Divider/index.tsx +3 -3
- package/src/components/EmptyState/index.tsx +72 -55
- package/src/components/HeaderText/index.tsx +5 -5
- package/src/components/Input/index.tsx +40 -37
- package/src/components/List/index.tsx +2 -3
- package/src/components/Pagination/index.tsx +56 -43
- package/src/components/PlanCard/index.tsx +118 -50
- package/src/components/ProgressTracker/index.tsx +44 -26
- package/src/components/PromotionCard/index.tsx +217 -212
- package/src/components/RadioButton/index.tsx +27 -25
- package/src/components/ScreenContainer/ScreenContainer.test.tsx +12 -2
- package/src/components/ScreenContainer/index.tsx +21 -10
- package/src/components/SegmentedControl/index.tsx +37 -30
- package/src/components/SelectionCard/index.tsx +45 -32
- package/src/components/ServiceCard/index.tsx +125 -128
- package/src/components/Slider/index.tsx +77 -77
- package/src/components/Snackbar/index.tsx +69 -57
- package/src/components/Switch/index.tsx +13 -12
- package/src/components/Tooltip/index.tsx +3 -3
- package/src/icons/index.ts +12 -59
- package/src/icons/types.ts +7 -23
- package/src/index.ts +15 -22
- package/src/theme/ThemeContext.ts +11 -0
- package/src/theme/ThemeProvider.tsx +40 -0
- package/src/theme/__tests__/ThemeProvider.test.tsx +101 -0
- package/src/theme/__tests__/defaultTheme.test.ts +36 -0
- package/src/theme/__tests__/mergeTheme.test.ts +94 -0
- package/src/theme/defaultTheme.ts +15 -0
- package/src/theme/index.ts +8 -0
- package/src/theme/mergeTheme.ts +47 -0
- package/src/theme/types.ts +39 -0
- package/src/theme/useTheme.ts +16 -0
- package/src/icons/AtomIcon.tsx +0 -32
- package/src/icons/BinocularsIcon.tsx +0 -45
- package/src/icons/FileChartColumnIncreasingIcon.tsx +0 -44
- package/src/icons/FlagIcon.tsx +0 -20
- package/src/icons/GlobeIcon.tsx +0 -20
- package/src/icons/GraduationCapIcon.tsx +0 -32
- package/src/icons/HandHeartIcon.tsx +0 -38
- package/src/icons/HandshakeIcon.tsx +0 -39
- package/src/icons/HazeIcon.tsx +0 -42
- package/src/icons/HeartHandshakeIcon.tsx +0 -20
- package/src/icons/HourglassIcon.tsx +0 -33
- package/src/icons/IdCardIcon.tsx +0 -44
- package/src/icons/MessageCirclePlusIcon.tsx +0 -32
- package/src/icons/MountainSnowIcon.tsx +0 -26
- package/src/icons/SnowflakeIcon.tsx +0 -86
- package/src/icons/StethoscopeIcon.tsx +0 -34
- package/src/icons/UserRoundIcon.tsx +0 -26
- package/src/icons/WheatIcon.tsx +0 -62
package/src/index.ts
CHANGED
|
@@ -131,6 +131,15 @@ export type {
|
|
|
131
131
|
ActionSheetAction,
|
|
132
132
|
} from './components/ActionSheet';
|
|
133
133
|
|
|
134
|
+
export { ChatBubble } from './components/ChatBubble';
|
|
135
|
+
export type {
|
|
136
|
+
ChatBubbleProps,
|
|
137
|
+
ChatBubbleVariant,
|
|
138
|
+
} from './components/ChatBubble';
|
|
139
|
+
|
|
140
|
+
export { ChatInput } from './components/ChatInput';
|
|
141
|
+
export type { ChatInputProps, ChatAttachment } from './components/ChatInput';
|
|
142
|
+
|
|
134
143
|
export { BodyText } from './components/BodyText';
|
|
135
144
|
export type { BodyTextProps, BodyVariant } from './components/BodyText';
|
|
136
145
|
|
|
@@ -140,26 +149,10 @@ export type { HeaderTextProps, HeaderVariant } from './components/HeaderText';
|
|
|
140
149
|
// Re-export tokens for consumers who need direct access
|
|
141
150
|
export { sys } from './tokens';
|
|
142
151
|
|
|
143
|
-
//
|
|
144
|
-
export {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
AtomIcon,
|
|
150
|
-
HazeIcon,
|
|
151
|
-
HourglassIcon,
|
|
152
|
-
GraduationCapIcon,
|
|
153
|
-
HandHeartIcon,
|
|
154
|
-
IdCardIcon,
|
|
155
|
-
MessageCirclePlusIcon,
|
|
156
|
-
StethoscopeIcon,
|
|
157
|
-
BinocularsIcon,
|
|
158
|
-
FlagIcon,
|
|
159
|
-
MountainSnowIcon,
|
|
160
|
-
SnowflakeIcon,
|
|
161
|
-
FileChartColumnIncreasingIcon,
|
|
162
|
-
WheatIcon,
|
|
163
|
-
} from './icons';
|
|
152
|
+
// Theme system — override the default GuidanceNow theme per-app or per-subtree
|
|
153
|
+
export { ThemeProvider, useTheme, defaultTheme } from './theme';
|
|
154
|
+
export type { ThemeProviderProps, Theme, ThemeOverride } from './theme';
|
|
155
|
+
|
|
156
|
+
// Icons — resolved from lucide-react-native at runtime
|
|
157
|
+
export { resolveIcon, SIZE_MAP, DEFAULT_ICON_COLOR } from './icons';
|
|
164
158
|
export type { IconProps, IconSize, IconName } from './icons';
|
|
165
|
-
export { ICON_MAP } from './icons';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createContext } from 'react';
|
|
2
|
+
|
|
3
|
+
import { sys } from '../tokens';
|
|
4
|
+
import type { Theme } from './types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Provides the active resolved theme to all components.
|
|
8
|
+
* Default value is the GuidanceNow theme so components render correctly
|
|
9
|
+
* even when no ThemeProvider is present in the tree.
|
|
10
|
+
*/
|
|
11
|
+
export const ThemeContext = createContext<Theme>(sys as unknown as Theme);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
import { ThemeContext } from './ThemeContext';
|
|
4
|
+
import { mergeTheme } from './mergeTheme';
|
|
5
|
+
import type { ThemeOverride } from './types';
|
|
6
|
+
|
|
7
|
+
export interface ThemeProviderProps {
|
|
8
|
+
/**
|
|
9
|
+
* Optional partial theme override. Keys present here replace the
|
|
10
|
+
* corresponding GuidanceNow default values; all other tokens keep their
|
|
11
|
+
* defaults. Pass `undefined` (or omit this prop) to use the GuidanceNow
|
|
12
|
+
* theme unchanged.
|
|
13
|
+
*/
|
|
14
|
+
theme?: ThemeOverride;
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Wraps your application (or a subtree) and provides a resolved theme to all
|
|
20
|
+
* `@compsych/mobile-ui` components beneath it.
|
|
21
|
+
*
|
|
22
|
+
* @example — default GuidanceNow theme
|
|
23
|
+
* <ThemeProvider>
|
|
24
|
+
* <App />
|
|
25
|
+
* </ThemeProvider>
|
|
26
|
+
*
|
|
27
|
+
* @example — custom brand override
|
|
28
|
+
* <ThemeProvider theme={{ colorRoles: { accent: { primary: { sysPrimary: '#c00000' } } } }}>
|
|
29
|
+
* <App />
|
|
30
|
+
* </ThemeProvider>
|
|
31
|
+
*/
|
|
32
|
+
export function ThemeProvider({ theme, children }: ThemeProviderProps) {
|
|
33
|
+
const resolvedTheme = useMemo(() => mergeTheme(theme), [theme]);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<ThemeContext.Provider value={resolvedTheme}>
|
|
37
|
+
{children}
|
|
38
|
+
</ThemeContext.Provider>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import React, { useContext } from 'react';
|
|
2
|
+
|
|
3
|
+
import { Text } from 'react-native';
|
|
4
|
+
|
|
5
|
+
import { render } from '@testing-library/react-native';
|
|
6
|
+
|
|
7
|
+
import { sys } from '../../tokens';
|
|
8
|
+
import { ThemeContext } from '../ThemeContext';
|
|
9
|
+
import { ThemeProvider } from '../ThemeProvider';
|
|
10
|
+
|
|
11
|
+
function ThemeConsumer() {
|
|
12
|
+
const theme = useContext(ThemeContext);
|
|
13
|
+
return (
|
|
14
|
+
<Text testID="primary-color">
|
|
15
|
+
{theme.colorRoles.accent.primary.sysPrimary}
|
|
16
|
+
</Text>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('ThemeProvider', () => {
|
|
21
|
+
it('renders children', () => {
|
|
22
|
+
const { getByText } = render(
|
|
23
|
+
<ThemeProvider>
|
|
24
|
+
<Text>hello</Text>
|
|
25
|
+
</ThemeProvider>,
|
|
26
|
+
);
|
|
27
|
+
expect(getByText('hello')).toBeTruthy();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('provides the GuidanceNow default theme when no override is given', () => {
|
|
31
|
+
const { getByTestId } = render(
|
|
32
|
+
<ThemeProvider>
|
|
33
|
+
<ThemeConsumer />
|
|
34
|
+
</ThemeProvider>,
|
|
35
|
+
);
|
|
36
|
+
expect(getByTestId('primary-color').props.children).toBe(
|
|
37
|
+
sys.colorRoles.accent.primary.sysPrimary,
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('provides the merged custom theme when an override is supplied', () => {
|
|
42
|
+
const { getByTestId } = render(
|
|
43
|
+
<ThemeProvider
|
|
44
|
+
theme={{
|
|
45
|
+
colorRoles: { accent: { primary: { sysPrimary: '#c00000' } } },
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
<ThemeConsumer />
|
|
49
|
+
</ThemeProvider>,
|
|
50
|
+
);
|
|
51
|
+
expect(getByTestId('primary-color').props.children).toBe('#c00000');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('keeps default values for tokens not included in the override', () => {
|
|
55
|
+
function ErrorColorConsumer() {
|
|
56
|
+
const theme = useContext(ThemeContext);
|
|
57
|
+
return (
|
|
58
|
+
<Text testID="error-color">{theme.colorRoles.error.sysError}</Text>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const { getByTestId } = render(
|
|
63
|
+
<ThemeProvider
|
|
64
|
+
theme={{
|
|
65
|
+
colorRoles: { accent: { primary: { sysPrimary: '#c00000' } } },
|
|
66
|
+
}}
|
|
67
|
+
>
|
|
68
|
+
<ErrorColorConsumer />
|
|
69
|
+
</ThemeProvider>,
|
|
70
|
+
);
|
|
71
|
+
expect(getByTestId('error-color').props.children).toBe(
|
|
72
|
+
sys.colorRoles.error.sysError,
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('works correctly when nested — inner provider wins', () => {
|
|
77
|
+
function Consumer() {
|
|
78
|
+
const theme = useContext(ThemeContext);
|
|
79
|
+
return (
|
|
80
|
+
<Text testID="color">{theme.colorRoles.accent.primary.sysPrimary}</Text>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const { getByTestId } = render(
|
|
85
|
+
<ThemeProvider
|
|
86
|
+
theme={{
|
|
87
|
+
colorRoles: { accent: { primary: { sysPrimary: '#aaaaaa' } } },
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
<ThemeProvider
|
|
91
|
+
theme={{
|
|
92
|
+
colorRoles: { accent: { primary: { sysPrimary: '#bbbbbb' } } },
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
<Consumer />
|
|
96
|
+
</ThemeProvider>
|
|
97
|
+
</ThemeProvider>,
|
|
98
|
+
);
|
|
99
|
+
expect(getByTestId('color').props.children).toBe('#bbbbbb');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { sys } from '../../tokens';
|
|
2
|
+
import { defaultTheme } from '../defaultTheme';
|
|
3
|
+
|
|
4
|
+
describe('defaultTheme', () => {
|
|
5
|
+
it('equals the GuidanceNow default token values', () => {
|
|
6
|
+
expect(defaultTheme).toEqual(sys);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('exposes colorRoles tokens at the correct paths', () => {
|
|
10
|
+
expect(defaultTheme.colorRoles.accent.primary.sysPrimary).toBe(
|
|
11
|
+
sys.colorRoles.accent.primary.sysPrimary,
|
|
12
|
+
);
|
|
13
|
+
expect(defaultTheme.colorRoles.error.sysError).toBe(
|
|
14
|
+
sys.colorRoles.error.sysError,
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('exposes dimension tokens at the correct paths', () => {
|
|
19
|
+
expect(defaultTheme.dimensions.borderRadius.sysRadiusMd).toBe(
|
|
20
|
+
sys.dimensions.borderRadius.sysRadiusMd,
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('exposes typeScale tokens at the correct paths', () => {
|
|
25
|
+
expect(defaultTheme.typeScale.bodyMedium.sysFontSize).toBe(
|
|
26
|
+
sys.typeScale.bodyMedium.sysFontSize,
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('is typed as Theme — readable without a ThemeProvider in scope', () => {
|
|
31
|
+
// This is a compile-time check: accessing a property on the widened Theme type
|
|
32
|
+
// (not the narrow `as const` literal type) must not require a type cast.
|
|
33
|
+
const primary: string = defaultTheme.colorRoles.accent.primary.sysPrimary;
|
|
34
|
+
expect(typeof primary).toBe('string');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { sys } from '../../tokens';
|
|
2
|
+
import { mergeTheme } from '../mergeTheme';
|
|
3
|
+
|
|
4
|
+
describe('mergeTheme', () => {
|
|
5
|
+
it('returns the default GuidanceNow theme when no override is provided', () => {
|
|
6
|
+
expect(mergeTheme()).toEqual(sys);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('returns the default theme when override is undefined', () => {
|
|
10
|
+
expect(mergeTheme(undefined)).toEqual(sys);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('overrides a single top-level color token', () => {
|
|
14
|
+
const result = mergeTheme({
|
|
15
|
+
colorRoles: { accent: { primary: { sysPrimary: '#c00000' } } },
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
expect(result.colorRoles.accent.primary.sysPrimary).toBe('#c00000');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('preserves all non-overridden default values', () => {
|
|
22
|
+
const result = mergeTheme({
|
|
23
|
+
colorRoles: { accent: { primary: { sysPrimary: '#c00000' } } },
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
expect(result.colorRoles.accent.primary.sysOnPrimary).toBe(
|
|
27
|
+
sys.colorRoles.accent.primary.sysOnPrimary,
|
|
28
|
+
);
|
|
29
|
+
expect(result.colorRoles.error.sysError).toBe(
|
|
30
|
+
sys.colorRoles.error.sysError,
|
|
31
|
+
);
|
|
32
|
+
expect(result.dimensions.borderRadius.sysRadiusMd).toBe(
|
|
33
|
+
sys.dimensions.borderRadius.sysRadiusMd,
|
|
34
|
+
);
|
|
35
|
+
expect(result.typeScale.bodyMedium.sysFontSize).toBe(
|
|
36
|
+
sys.typeScale.bodyMedium.sysFontSize,
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('overrides a numeric dimension token', () => {
|
|
41
|
+
const result = mergeTheme({
|
|
42
|
+
dimensions: { borderRadius: { sysRadiusMd: 99 } },
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(result.dimensions.borderRadius.sysRadiusMd).toBe(99);
|
|
46
|
+
expect(result.dimensions.borderRadius.sysRadiusSm).toBe(
|
|
47
|
+
sys.dimensions.borderRadius.sysRadiusSm,
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('overrides a typeScale token', () => {
|
|
52
|
+
const result = mergeTheme({
|
|
53
|
+
typeScale: { bodyMedium: { sysFontSize: 18 } },
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(result.typeScale.bodyMedium.sysFontSize).toBe(18);
|
|
57
|
+
expect(result.typeScale.bodyMedium.sysLineHeight).toBe(
|
|
58
|
+
sys.typeScale.bodyMedium.sysLineHeight,
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('ignores null override values and keeps defaults', () => {
|
|
63
|
+
const result = mergeTheme({
|
|
64
|
+
colorRoles: {
|
|
65
|
+
accent: { primary: { sysPrimary: null as unknown as string } },
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(result.colorRoles.accent.primary.sysPrimary).toBe(
|
|
70
|
+
sys.colorRoles.accent.primary.sysPrimary,
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('handles an empty override object without changing anything', () => {
|
|
75
|
+
const result = mergeTheme({});
|
|
76
|
+
expect(result).toEqual(sys);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('handles deeply partial overrides spanning multiple branches', () => {
|
|
80
|
+
const result = mergeTheme({
|
|
81
|
+
colorRoles: { accent: { primary: { sysPrimary: '#aabbcc' } } },
|
|
82
|
+
dimensions: { borderRadius: { sysRadiusFull: 500 } },
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(result.colorRoles.accent.primary.sysPrimary).toBe('#aabbcc');
|
|
86
|
+
expect(result.dimensions.borderRadius.sysRadiusFull).toBe(500);
|
|
87
|
+
expect(result.colorRoles.error.sysError).toBe(
|
|
88
|
+
sys.colorRoles.error.sysError,
|
|
89
|
+
);
|
|
90
|
+
expect(result.dimensions.borderRadius.sysRadiusMd).toBe(
|
|
91
|
+
sys.dimensions.borderRadius.sysRadiusMd,
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { sys } from '../tokens';
|
|
2
|
+
import type { Theme } from './types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The GuidanceNow default theme — the full resolved token tree typed as `Theme`
|
|
6
|
+
* (widened, mutable types) so consumers can read individual values without a
|
|
7
|
+
* ThemeProvider in scope.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* import { defaultTheme } from '@compsych/mobile-ui';
|
|
11
|
+
*
|
|
12
|
+
* const primaryColor = defaultTheme.colorRoles.accent.primary.sysPrimary;
|
|
13
|
+
* const bodyFont = defaultTheme.typeScale.body.md.fontFamily;
|
|
14
|
+
*/
|
|
15
|
+
export const defaultTheme: Theme = sys as unknown as Theme;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { sys } from '../tokens';
|
|
2
|
+
import type { Theme, ThemeOverride } from './types';
|
|
3
|
+
|
|
4
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
5
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function deepMerge(
|
|
9
|
+
base: Record<string, unknown>,
|
|
10
|
+
override: Record<string, unknown>,
|
|
11
|
+
): Record<string, unknown> {
|
|
12
|
+
const result: Record<string, unknown> = { ...base };
|
|
13
|
+
|
|
14
|
+
for (const key of Object.keys(override)) {
|
|
15
|
+
const overrideVal = override[key];
|
|
16
|
+
const baseVal = base[key];
|
|
17
|
+
|
|
18
|
+
if (overrideVal === undefined || overrideVal === null) {
|
|
19
|
+
// Ignore nullish overrides — preserve the default
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (isPlainObject(baseVal) && isPlainObject(overrideVal)) {
|
|
24
|
+
result[key] = deepMerge(baseVal, overrideVal);
|
|
25
|
+
} else {
|
|
26
|
+
result[key] = overrideVal;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Deep-merges a partial ThemeOverride on top of the GuidanceNow defaults.
|
|
35
|
+
* Any key omitted from the override keeps its default value, so missing or
|
|
36
|
+
* invalid overrides can never break the UI.
|
|
37
|
+
*/
|
|
38
|
+
export function mergeTheme(override?: ThemeOverride): Theme {
|
|
39
|
+
if (!override) {
|
|
40
|
+
return sys as unknown as Theme;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return deepMerge(
|
|
44
|
+
sys as unknown as Record<string, unknown>,
|
|
45
|
+
override as Record<string, unknown>,
|
|
46
|
+
) as Theme;
|
|
47
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { sys } from '../tokens';
|
|
2
|
+
|
|
3
|
+
// Strip `as const` readonly modifiers and widen literal types to their
|
|
4
|
+
// primitive base (string, number, boolean) so consumers can supply any valid
|
|
5
|
+
// CSS color or numeric value in a ThemeOverride — not just the exact GuidanceNow
|
|
6
|
+
// hex literals.
|
|
7
|
+
type DeepWritable<T> = {
|
|
8
|
+
-readonly [P in keyof T]: DeepWritable<T[P]>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type WidenLiterals<T> = T extends string
|
|
12
|
+
? string
|
|
13
|
+
: T extends number
|
|
14
|
+
? number
|
|
15
|
+
: T extends boolean
|
|
16
|
+
? boolean
|
|
17
|
+
: T extends object
|
|
18
|
+
? { [K in keyof T]: WidenLiterals<T[K]> }
|
|
19
|
+
: T;
|
|
20
|
+
|
|
21
|
+
/** The full resolved theme object consumed by every component via useTheme(). */
|
|
22
|
+
export type Theme = WidenLiterals<DeepWritable<typeof sys>>;
|
|
23
|
+
|
|
24
|
+
type DeepPartial<T> = T extends object
|
|
25
|
+
? { [P in keyof T]?: DeepPartial<T[P]> }
|
|
26
|
+
: T;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A partial theme override — any subset of the token tree.
|
|
30
|
+
* Missing keys fall back to the GuidanceNow defaults.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* const brandTheme: ThemeOverride = {
|
|
34
|
+
* colorRoles: {
|
|
35
|
+
* accent: { primary: { sysPrimary: '#c00000' } },
|
|
36
|
+
* },
|
|
37
|
+
* };
|
|
38
|
+
*/
|
|
39
|
+
export type ThemeOverride = DeepPartial<Theme>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
|
|
3
|
+
import { ThemeContext } from './ThemeContext';
|
|
4
|
+
import type { Theme } from './types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Returns the active resolved theme. Components use this hook to read token
|
|
8
|
+
* values at render time so they automatically reflect any ThemeProvider
|
|
9
|
+
* override in the tree.
|
|
10
|
+
*
|
|
11
|
+
* Falls back to the GuidanceNow default theme when called outside a
|
|
12
|
+
* ThemeProvider.
|
|
13
|
+
*/
|
|
14
|
+
export function useTheme(): Theme {
|
|
15
|
+
return useContext(ThemeContext);
|
|
16
|
+
}
|
package/src/icons/AtomIcon.tsx
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import Svg, { Path } from 'react-native-svg';
|
|
2
|
-
|
|
3
|
-
import { COMMON, DEFAULT_ICON_COLOR, type IconProps, SIZE_MAP } from './types';
|
|
4
|
-
|
|
5
|
-
export function AtomIcon({
|
|
6
|
-
size = 'medium',
|
|
7
|
-
color = DEFAULT_ICON_COLOR,
|
|
8
|
-
}: IconProps) {
|
|
9
|
-
const { size: px, strokeWidth } = SIZE_MAP[size];
|
|
10
|
-
return (
|
|
11
|
-
<Svg width={px} height={px} viewBox="0 0 48 48" fill="none">
|
|
12
|
-
<Path
|
|
13
|
-
d="M24 26C25.1046 26 26 25.1046 26 24C26 22.8954 25.1046 22 24 22C22.8954 22 22 22.8954 22 24C22 25.1046 22.8954 26 24 26Z"
|
|
14
|
-
stroke={color}
|
|
15
|
-
strokeWidth={strokeWidth}
|
|
16
|
-
{...COMMON}
|
|
17
|
-
/>
|
|
18
|
-
<Path
|
|
19
|
-
d="M40.4 40.4C44.48 36.34 40.44 25.68 31.4 16.6C22.32 7.56002 11.66 3.52002 7.60002 7.60002C3.52002 11.66 7.56002 22.32 16.6 31.4C25.68 40.44 36.34 44.48 40.4 40.4Z"
|
|
20
|
-
stroke={color}
|
|
21
|
-
strokeWidth={strokeWidth}
|
|
22
|
-
{...COMMON}
|
|
23
|
-
/>
|
|
24
|
-
<Path
|
|
25
|
-
d="M31.4 31.4C40.44 22.32 44.48 11.66 40.4 7.60002C36.34 3.52002 25.68 7.56002 16.6 16.6C7.56002 25.68 3.52002 36.34 7.60002 40.4C11.66 44.48 22.32 40.44 31.4 31.4Z"
|
|
26
|
-
stroke={color}
|
|
27
|
-
strokeWidth={strokeWidth}
|
|
28
|
-
{...COMMON}
|
|
29
|
-
/>
|
|
30
|
-
</Svg>
|
|
31
|
-
);
|
|
32
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import Svg, { Path } from 'react-native-svg';
|
|
2
|
-
|
|
3
|
-
import { COMMON, DEFAULT_ICON_COLOR, type IconProps, SIZE_MAP } from './types';
|
|
4
|
-
|
|
5
|
-
export function BinocularsIcon({
|
|
6
|
-
size = 'medium',
|
|
7
|
-
color = DEFAULT_ICON_COLOR,
|
|
8
|
-
}: IconProps) {
|
|
9
|
-
const { size: px, strokeWidth } = SIZE_MAP[size];
|
|
10
|
-
return (
|
|
11
|
-
<Svg width={px} height={px} viewBox="0 0 48 48" fill="none">
|
|
12
|
-
<Path
|
|
13
|
-
d="M20 20H28"
|
|
14
|
-
stroke={color}
|
|
15
|
-
strokeWidth={strokeWidth}
|
|
16
|
-
{...COMMON}
|
|
17
|
-
/>
|
|
18
|
-
<Path
|
|
19
|
-
d="M38 14V8C38 7.46957 37.7893 6.96086 37.4142 6.58579C37.0391 6.21071 36.5304 6 36 6H32C31.4696 6 30.9609 6.21071 30.5858 6.58579C30.2107 6.96086 30 7.46957 30 8V14"
|
|
20
|
-
stroke={color}
|
|
21
|
-
strokeWidth={strokeWidth}
|
|
22
|
-
{...COMMON}
|
|
23
|
-
/>
|
|
24
|
-
<Path
|
|
25
|
-
d="M40 42C41.0609 42 42.0783 41.5786 42.8284 40.8284C43.5786 40.0783 44 39.0609 44 38V30.298C44 27.518 40 24.374 40 20.64V16C40 15.4696 39.7893 14.9609 39.4142 14.5858C39.0391 14.2107 38.5304 14 38 14H30C29.4696 14 28.9609 14.2107 28.5858 14.5858C28.2107 14.9609 28 15.4696 28 16V38C28 39.0609 28.4214 40.0783 29.1716 40.8284C29.9217 41.5786 30.9391 42 32 42H40Z"
|
|
26
|
-
stroke={color}
|
|
27
|
-
strokeWidth={strokeWidth}
|
|
28
|
-
{...COMMON}
|
|
29
|
-
/>
|
|
30
|
-
<Path d="M44 32H4" stroke={color} strokeWidth={strokeWidth} {...COMMON} />
|
|
31
|
-
<Path
|
|
32
|
-
d="M8 42C6.93913 42 5.92172 41.5786 5.17157 40.8284C4.42143 40.0783 4 39.0609 4 38V30.298C4 27.518 8 24.374 8 20.64V16C8 15.4696 8.21071 14.9609 8.58579 14.5858C8.96086 14.2107 9.46957 14 10 14H18C18.5304 14 19.0391 14.2107 19.4142 14.5858C19.7893 14.9609 20 15.4696 20 16V38C20 39.0609 19.5786 40.0783 18.8284 40.8284C18.0783 41.5786 17.0609 42 16 42H8Z"
|
|
33
|
-
stroke={color}
|
|
34
|
-
strokeWidth={strokeWidth}
|
|
35
|
-
{...COMMON}
|
|
36
|
-
/>
|
|
37
|
-
<Path
|
|
38
|
-
d="M18 14V8C18 7.46957 17.7893 6.96086 17.4142 6.58579C17.0391 6.21071 16.5304 6 16 6H12C11.4696 6 10.9609 6.21071 10.5858 6.58579C10.2107 6.96086 10 7.46957 10 8V14"
|
|
39
|
-
stroke={color}
|
|
40
|
-
strokeWidth={strokeWidth}
|
|
41
|
-
{...COMMON}
|
|
42
|
-
/>
|
|
43
|
-
</Svg>
|
|
44
|
-
);
|
|
45
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import Svg, { Path } from 'react-native-svg';
|
|
2
|
-
|
|
3
|
-
import { COMMON, DEFAULT_ICON_COLOR, type IconProps, SIZE_MAP } from './types';
|
|
4
|
-
|
|
5
|
-
export function FileChartColumnIncreasingIcon({
|
|
6
|
-
size = 'medium',
|
|
7
|
-
color = DEFAULT_ICON_COLOR,
|
|
8
|
-
}: IconProps) {
|
|
9
|
-
const { size: px, strokeWidth } = SIZE_MAP[size];
|
|
10
|
-
return (
|
|
11
|
-
<Svg width={px} height={px} viewBox="0 0 48 48" fill="none">
|
|
12
|
-
<Path
|
|
13
|
-
d="M12 44C10.9391 44 9.92172 43.5786 9.17157 42.8284C8.42143 42.0783 8 41.0609 8 40V8.00001C8 6.93914 8.42143 5.92172 9.17157 5.17158C9.92172 4.42143 10.9391 4.00001 12 4.00001H28C28.6331 3.99898 29.2602 4.12321 29.8451 4.36554C30.43 4.60788 30.9611 4.96353 31.408 5.41201L38.584 12.588C39.0337 13.035 39.3903 13.5667 39.6334 14.1523C39.8764 14.738 40.001 15.3659 40 16V40C40 41.0609 39.5786 42.0783 38.8284 42.8284C38.0783 43.5786 37.0609 44 36 44H12Z"
|
|
14
|
-
stroke={color}
|
|
15
|
-
strokeWidth={strokeWidth}
|
|
16
|
-
{...COMMON}
|
|
17
|
-
/>
|
|
18
|
-
<Path
|
|
19
|
-
d="M28 4V14C28 14.5304 28.2107 15.0391 28.5858 15.4142C28.9609 15.7893 29.4696 16 30 16H40"
|
|
20
|
-
stroke={color}
|
|
21
|
-
strokeWidth={strokeWidth}
|
|
22
|
-
{...COMMON}
|
|
23
|
-
/>
|
|
24
|
-
<Path
|
|
25
|
-
d="M16 36V32"
|
|
26
|
-
stroke={color}
|
|
27
|
-
strokeWidth={strokeWidth}
|
|
28
|
-
{...COMMON}
|
|
29
|
-
/>
|
|
30
|
-
<Path
|
|
31
|
-
d="M24 36V28"
|
|
32
|
-
stroke={color}
|
|
33
|
-
strokeWidth={strokeWidth}
|
|
34
|
-
{...COMMON}
|
|
35
|
-
/>
|
|
36
|
-
<Path
|
|
37
|
-
d="M32 36V24"
|
|
38
|
-
stroke={color}
|
|
39
|
-
strokeWidth={strokeWidth}
|
|
40
|
-
{...COMMON}
|
|
41
|
-
/>
|
|
42
|
-
</Svg>
|
|
43
|
-
);
|
|
44
|
-
}
|
package/src/icons/FlagIcon.tsx
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import Svg, { Path } from 'react-native-svg';
|
|
2
|
-
|
|
3
|
-
import { COMMON, DEFAULT_ICON_COLOR, type IconProps, SIZE_MAP } from './types';
|
|
4
|
-
|
|
5
|
-
export function FlagIcon({
|
|
6
|
-
size = 'medium',
|
|
7
|
-
color = DEFAULT_ICON_COLOR,
|
|
8
|
-
}: IconProps) {
|
|
9
|
-
const { size: px, strokeWidth } = SIZE_MAP[size];
|
|
10
|
-
return (
|
|
11
|
-
<Svg width={px} height={px} viewBox="0 0 48 48" fill="none">
|
|
12
|
-
<Path
|
|
13
|
-
d="M8 44V8C8 7.68951 8.07229 7.38328 8.21115 7.10557C8.35 6.82786 8.55161 6.58629 8.8 6.4C10.8772 4.84213 13.4036 4 16 4C22 4 26 8 30.666 8C33.3327 8 35.3773 7.46667 36.8 6.4C37.0971 6.17715 37.4505 6.04144 37.8204 6.00808C38.1903 5.97473 38.5622 6.04504 38.8944 6.21115C39.2266 6.37725 39.506 6.63259 39.7013 6.94854C39.8966 7.26449 40 7.62858 40 8V28C40 28.3105 39.9277 28.6167 39.7889 28.8944C39.65 29.1721 39.4484 29.4137 39.2 29.6C37.1228 31.1579 34.5964 32 32 32C26 32 22 28 16 28C13.0482 28.0001 10.2 29.0881 8 31.056"
|
|
14
|
-
stroke={color}
|
|
15
|
-
strokeWidth={strokeWidth}
|
|
16
|
-
{...COMMON}
|
|
17
|
-
/>
|
|
18
|
-
</Svg>
|
|
19
|
-
);
|
|
20
|
-
}
|
package/src/icons/GlobeIcon.tsx
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import Svg, { Path } from 'react-native-svg';
|
|
2
|
-
|
|
3
|
-
import { COMMON, DEFAULT_ICON_COLOR, type IconProps, SIZE_MAP } from './types';
|
|
4
|
-
|
|
5
|
-
export function GlobeIcon({
|
|
6
|
-
size = 'medium',
|
|
7
|
-
color = DEFAULT_ICON_COLOR,
|
|
8
|
-
}: IconProps) {
|
|
9
|
-
const { size: px, strokeWidth } = SIZE_MAP[size];
|
|
10
|
-
return (
|
|
11
|
-
<Svg width={px} height={px} viewBox="0 0 48 48" fill="none">
|
|
12
|
-
<Path
|
|
13
|
-
d="M44 24C44 35.0457 35.0457 44 24 44M44 24C44 12.9543 35.0457 4 24 4M44 24H4M24 44C12.9543 44 4 35.0457 4 24M24 44C18.8645 38.6077 16 31.4465 16 24C16 16.5535 18.8645 9.39231 24 4M24 44C29.1355 38.6077 32 31.4465 32 24C32 16.5535 29.1355 9.39231 24 4M4 24C4 12.9543 12.9543 4 24 4"
|
|
14
|
-
stroke={color}
|
|
15
|
-
strokeWidth={strokeWidth}
|
|
16
|
-
{...COMMON}
|
|
17
|
-
/>
|
|
18
|
-
</Svg>
|
|
19
|
-
);
|
|
20
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import Svg, { Path } from 'react-native-svg';
|
|
2
|
-
|
|
3
|
-
import { COMMON, DEFAULT_ICON_COLOR, type IconProps, SIZE_MAP } from './types';
|
|
4
|
-
|
|
5
|
-
export function GraduationCapIcon({
|
|
6
|
-
size = 'medium',
|
|
7
|
-
color = DEFAULT_ICON_COLOR,
|
|
8
|
-
}: IconProps) {
|
|
9
|
-
const { size: px, strokeWidth } = SIZE_MAP[size];
|
|
10
|
-
return (
|
|
11
|
-
<Svg width={px} height={px} viewBox="0 0 48 48" fill="none">
|
|
12
|
-
<Path
|
|
13
|
-
d="M42.84 21.844C43.1981 21.686 43.5019 21.4265 43.7139 21.0976C43.9259 20.7687 44.0367 20.3848 44.0327 19.9935C44.0286 19.6022 43.9099 19.2206 43.6911 18.8961C43.4724 18.5717 43.1632 18.3185 42.802 18.168L25.66 10.36C25.1389 10.1223 24.5728 9.99927 24 9.99927C23.4272 9.99927 22.8611 10.1223 22.34 10.36L5.20002 18.16C4.84396 18.3159 4.54106 18.5723 4.32836 18.8976C4.11566 19.223 4.00238 19.6033 4.00238 19.992C4.00238 20.3807 4.11566 20.761 4.32836 21.0864C4.54106 21.4117 4.84396 21.668 5.20002 21.824L22.34 29.64C22.8611 29.8777 23.4272 30.0007 24 30.0007C24.5728 30.0007 25.1389 29.8777 25.66 29.64L42.84 21.844Z"
|
|
14
|
-
stroke={color}
|
|
15
|
-
strokeWidth={strokeWidth}
|
|
16
|
-
{...COMMON}
|
|
17
|
-
/>
|
|
18
|
-
<Path
|
|
19
|
-
d="M44 20V32"
|
|
20
|
-
stroke={color}
|
|
21
|
-
strokeWidth={strokeWidth}
|
|
22
|
-
{...COMMON}
|
|
23
|
-
/>
|
|
24
|
-
<Path
|
|
25
|
-
d="M12 25V32C12 33.5913 13.2643 35.1174 15.5147 36.2426C17.7652 37.3679 20.8174 38 24 38C27.1826 38 30.2348 37.3679 32.4853 36.2426C34.7357 35.1174 36 33.5913 36 32V25"
|
|
26
|
-
stroke={color}
|
|
27
|
-
strokeWidth={strokeWidth}
|
|
28
|
-
{...COMMON}
|
|
29
|
-
/>
|
|
30
|
-
</Svg>
|
|
31
|
-
);
|
|
32
|
-
}
|