@spelyco/react-native 1.0.0-alpha.1 → 1.0.0
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/README.md +102 -36
- package/dist/bootUnistyles.d.ts +2 -0
- package/dist/bootUnistyles.d.ts.map +1 -0
- package/dist/components/ActionIcon/ActionIcon.d.ts +37 -0
- package/dist/components/ActionIcon/ActionIcon.d.ts.map +1 -0
- package/dist/components/Box/Box.d.ts +20 -0
- package/dist/components/Box/Box.d.ts.map +1 -0
- package/dist/components/Box/index.d.ts +2 -0
- package/dist/components/Box/index.d.ts.map +1 -0
- package/dist/components/Button/Button.d.ts +1 -1
- package/dist/components/Button/Button.d.ts.map +1 -1
- package/dist/components/Text/Text.d.ts +12 -0
- package/dist/components/Text/Text.d.ts.map +1 -0
- package/dist/components/Text/index.d.ts +2 -0
- package/dist/components/Text/index.d.ts.map +1 -0
- package/dist/components/index.d.ts +3 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/hooks/index.d.ts +3 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/useSpelycoColorScheme.d.ts +14 -0
- package/dist/hooks/useSpelycoColorScheme.d.ts.map +1 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/provider/SpelycoProvider.d.ts +11 -0
- package/dist/provider/SpelycoProvider.d.ts.map +1 -0
- package/dist/provider/index.d.ts +4 -0
- package/dist/provider/index.d.ts.map +1 -0
- package/dist/provider/toNavigationTheme.d.ts +20 -0
- package/dist/provider/toNavigationTheme.d.ts.map +1 -0
- package/dist/provider/toUnistylesTheme.d.ts +24 -0
- package/dist/provider/toUnistylesTheme.d.ts.map +1 -0
- package/dist/store/colorScheme.d.ts +31 -0
- package/dist/store/colorScheme.d.ts.map +1 -0
- package/dist/store/index.d.ts +2 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/types.d.ts +5 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +11 -8
- package/src/bootUnistyles.ts +27 -0
- package/src/components/ActionIcon/ActionIcon.test.ts +59 -0
- package/src/components/ActionIcon/ActionIcon.tsx +54 -0
- package/src/components/Box/Box.test.ts +42 -0
- package/src/components/Box/Box.tsx +48 -0
- package/src/components/Box/index.ts +1 -0
- package/src/components/Button/Button.test.ts +22 -19
- package/src/components/Button/Button.tsx +36 -31
- package/src/components/Text/Text.test.ts +59 -0
- package/src/components/Text/Text.tsx +61 -0
- package/src/components/Text/index.ts +1 -0
- package/src/components/index.ts +3 -1
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useSpelycoColorScheme.ts +33 -0
- package/src/index.ts +13 -12
- package/src/provider/SpelycoProvider.tsx +102 -0
- package/src/provider/index.ts +3 -0
- package/src/provider/toNavigationTheme.ts +41 -0
- package/src/provider/toUnistylesTheme.ts +58 -0
- package/src/store/colorScheme.test.ts +105 -0
- package/src/store/colorScheme.ts +63 -0
- package/src/store/index.ts +5 -0
- package/src/types.ts +5 -4
- package/dist/configure.d.ts +0 -2
- package/dist/configure.d.ts.map +0 -1
- package/dist/themes/dark.d.ts +0 -3
- package/dist/themes/dark.d.ts.map +0 -1
- package/dist/themes/index.d.ts +0 -12
- package/dist/themes/index.d.ts.map +0 -1
- package/dist/themes/light.d.ts +0 -29
- package/dist/themes/light.d.ts.map +0 -1
- package/dist/unistyles.d.ts +0 -8
- package/dist/unistyles.d.ts.map +0 -1
- package/src/configure.ts +0 -3
- package/src/themes/dark.ts +0 -29
- package/src/themes/index.ts +0 -13
- package/src/themes/light.ts +0 -55
- package/src/unistyles.ts +0 -27
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("react-native", () => ({
|
|
4
|
+
Pressable: "Pressable",
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
vi.mock("react-native-unistyles", () => {
|
|
8
|
+
type StylesObject = Record<string, unknown>;
|
|
9
|
+
return {
|
|
10
|
+
StyleSheet: {
|
|
11
|
+
create: (stylesFn: ((theme: unknown) => StylesObject) | StylesObject) => {
|
|
12
|
+
const stubTheme = {
|
|
13
|
+
colorScheme: "light",
|
|
14
|
+
primary: "#6366f1",
|
|
15
|
+
colors: {
|
|
16
|
+
neutral: ["#fafafa", "#f4f4f5", "", "", "", "", "", "", "#27272a", "#18181b"],
|
|
17
|
+
},
|
|
18
|
+
radius: { xs: 2, sm: 4, md: 8, lg: 12, xl: 16, pill: 9999 },
|
|
19
|
+
};
|
|
20
|
+
const styles = typeof stylesFn === "function" ? stylesFn(stubTheme) : stylesFn;
|
|
21
|
+
return Object.assign({}, styles, { useVariants: () => undefined });
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
vi.mock("@spelyco/react-lib", () => ({
|
|
28
|
+
createComponent: <TProps,>(config: {
|
|
29
|
+
name: string;
|
|
30
|
+
defaultProps?: Partial<TProps>;
|
|
31
|
+
render: (props: TProps) => unknown;
|
|
32
|
+
}) => {
|
|
33
|
+
const Component = (props: TProps) => config.render(props);
|
|
34
|
+
(Component as { displayName?: string }).displayName = config.name;
|
|
35
|
+
(
|
|
36
|
+
Component as { extend?: (o: unknown) => unknown }
|
|
37
|
+
).extend = (override) => override;
|
|
38
|
+
return Component;
|
|
39
|
+
},
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
const { ActionIcon } = await import("./ActionIcon");
|
|
43
|
+
|
|
44
|
+
describe("ActionIcon", () => {
|
|
45
|
+
it("is a function (factory-wrapped component)", () => {
|
|
46
|
+
expect(typeof ActionIcon).toBe("function");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("has displayName 'ActionIcon'", () => {
|
|
50
|
+
expect((ActionIcon as { displayName?: string }).displayName).toBe("ActionIcon");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("exposes .extend() that returns the override config unchanged", () => {
|
|
54
|
+
const override = { defaultProps: { variant: "filled" as const } };
|
|
55
|
+
expect(
|
|
56
|
+
(ActionIcon as { extend: (o: unknown) => unknown }).extend(override),
|
|
57
|
+
).toBe(override);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createComponent } from "@spelyco/react-lib";
|
|
2
|
+
import { Pressable, type PressableProps } from "react-native";
|
|
3
|
+
import { StyleSheet, type UnistylesVariants } from "react-native-unistyles";
|
|
4
|
+
|
|
5
|
+
type ActionIconVariants = UnistylesVariants<typeof styles>;
|
|
6
|
+
|
|
7
|
+
export type ActionIconProps = PressableProps & ActionIconVariants;
|
|
8
|
+
|
|
9
|
+
export const ActionIcon = createComponent<ActionIconProps>({
|
|
10
|
+
name: "ActionIcon",
|
|
11
|
+
defaultProps: { variant: "subtle", size: "md" },
|
|
12
|
+
render: ({ variant = "subtle", size = "md", disabled, children, style, ...rest }) => {
|
|
13
|
+
styles.useVariants({ variant, size, disabled: disabled ?? false });
|
|
14
|
+
return (
|
|
15
|
+
<Pressable
|
|
16
|
+
accessibilityRole="button"
|
|
17
|
+
disabled={disabled}
|
|
18
|
+
style={(state) => [styles.root, typeof style === "function" ? style(state) : style]}
|
|
19
|
+
{...rest}
|
|
20
|
+
>
|
|
21
|
+
{children}
|
|
22
|
+
</Pressable>
|
|
23
|
+
);
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const styles = StyleSheet.create((theme) => ({
|
|
28
|
+
root: {
|
|
29
|
+
alignItems: "center",
|
|
30
|
+
justifyContent: "center",
|
|
31
|
+
borderRadius: theme.radius.md,
|
|
32
|
+
variants: {
|
|
33
|
+
variant: {
|
|
34
|
+
subtle: {
|
|
35
|
+
backgroundColor: theme.colors.neutral[theme.colorScheme === "dark" ? 8 : 1],
|
|
36
|
+
},
|
|
37
|
+
filled: {
|
|
38
|
+
backgroundColor: theme.primary,
|
|
39
|
+
},
|
|
40
|
+
transparent: {
|
|
41
|
+
backgroundColor: "transparent",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
size: {
|
|
45
|
+
sm: { width: 32, height: 32 },
|
|
46
|
+
md: { width: 40, height: 40 },
|
|
47
|
+
lg: { width: 48, height: 48 },
|
|
48
|
+
},
|
|
49
|
+
disabled: {
|
|
50
|
+
true: { opacity: 0.5 },
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
}));
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("react-native", () => ({
|
|
4
|
+
View: "View",
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
vi.mock("@spelyco/react-lib", () => ({
|
|
8
|
+
createComponent: <TProps,>(config: {
|
|
9
|
+
name: string;
|
|
10
|
+
defaultProps?: Partial<TProps>;
|
|
11
|
+
render: (props: TProps) => unknown;
|
|
12
|
+
}) => {
|
|
13
|
+
const Component = (props: TProps) => config.render(props);
|
|
14
|
+
(Component as { displayName?: string }).displayName = config.name;
|
|
15
|
+
(
|
|
16
|
+
Component as { extend?: (o: unknown) => unknown }
|
|
17
|
+
).extend = (override) => override;
|
|
18
|
+
return Component;
|
|
19
|
+
},
|
|
20
|
+
useSpelycoTheme: () => ({
|
|
21
|
+
spacing: { xs: 4, sm: 8, md: 12, lg: 16, xl: 24 },
|
|
22
|
+
}),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
const { Box } = await import("./Box");
|
|
26
|
+
|
|
27
|
+
describe("Box", () => {
|
|
28
|
+
it("is a function (factory-wrapped component)", () => {
|
|
29
|
+
expect(typeof Box).toBe("function");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("has displayName 'Box'", () => {
|
|
33
|
+
expect((Box as { displayName?: string }).displayName).toBe("Box");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("exposes .extend() that returns the override config unchanged", () => {
|
|
37
|
+
const override = { defaultProps: { bg: "#fff" } };
|
|
38
|
+
expect(
|
|
39
|
+
(Box as { extend: (o: unknown) => unknown }).extend(override),
|
|
40
|
+
).toBe(override);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createComponent,
|
|
3
|
+
type SpelycoSize,
|
|
4
|
+
type SpelycoSpacing,
|
|
5
|
+
useSpelycoTheme,
|
|
6
|
+
} from "@spelyco/react-lib";
|
|
7
|
+
import { View, type ViewProps } from "react-native";
|
|
8
|
+
|
|
9
|
+
type SpacingToken = SpelycoSize | number;
|
|
10
|
+
|
|
11
|
+
export interface BoxProps extends ViewProps {
|
|
12
|
+
/** padding shorthand */
|
|
13
|
+
p?: SpacingToken;
|
|
14
|
+
px?: SpacingToken;
|
|
15
|
+
py?: SpacingToken;
|
|
16
|
+
/** margin shorthand */
|
|
17
|
+
m?: SpacingToken;
|
|
18
|
+
mx?: SpacingToken;
|
|
19
|
+
my?: SpacingToken;
|
|
20
|
+
/** Flex gap between children */
|
|
21
|
+
gap?: SpacingToken;
|
|
22
|
+
/** Background color (hex). Theme palette tokens land in a later phase. */
|
|
23
|
+
bg?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const space = (token: SpacingToken | undefined, spacing: SpelycoSpacing): number | undefined => {
|
|
27
|
+
if (token === undefined) return undefined;
|
|
28
|
+
if (typeof token === "number") return token;
|
|
29
|
+
return spacing[token];
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const Box = createComponent<BoxProps>({
|
|
33
|
+
name: "Box",
|
|
34
|
+
render: ({ p, px, py, m, mx, my, gap, bg, style, ...rest }) => {
|
|
35
|
+
const theme = useSpelycoTheme();
|
|
36
|
+
const dynamicStyle = {
|
|
37
|
+
padding: space(p, theme.spacing),
|
|
38
|
+
paddingHorizontal: space(px, theme.spacing),
|
|
39
|
+
paddingVertical: space(py, theme.spacing),
|
|
40
|
+
margin: space(m, theme.spacing),
|
|
41
|
+
marginHorizontal: space(mx, theme.spacing),
|
|
42
|
+
marginVertical: space(my, theme.spacing),
|
|
43
|
+
gap: space(gap, theme.spacing),
|
|
44
|
+
backgroundColor: bg,
|
|
45
|
+
};
|
|
46
|
+
return <View {...rest} style={[dynamicStyle, style]} />;
|
|
47
|
+
},
|
|
48
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Box, type BoxProps } from "./Box";
|
|
@@ -10,22 +10,20 @@ vi.mock("react-native-unistyles", () => {
|
|
|
10
10
|
return {
|
|
11
11
|
StyleSheet: {
|
|
12
12
|
create: (stylesFn: ((theme: unknown) => StylesObject) | StylesObject) => {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
})
|
|
28
|
-
: stylesFn;
|
|
13
|
+
const stubTheme = {
|
|
14
|
+
colorScheme: "light",
|
|
15
|
+
primary: "#6366f1",
|
|
16
|
+
primaryColor: "brand",
|
|
17
|
+
primaryShade: 6,
|
|
18
|
+
colors: {
|
|
19
|
+
brand: ["#eef2ff", "", "", "", "", "#6366f1", "#4f46e5", "", "", ""],
|
|
20
|
+
neutral: ["#fafafa", "#f4f4f5", "#e4e4e7", "#d4d4d8", "#a1a1aa", "#71717a", "#52525b", "#3f3f46", "#27272a", "#18181b"],
|
|
21
|
+
},
|
|
22
|
+
spacing: { xs: 4, sm: 8, md: 12, lg: 16, xl: 24 },
|
|
23
|
+
radius: { xs: 2, sm: 4, md: 8, lg: 12, xl: 16, pill: 9999 },
|
|
24
|
+
fontSizes: { xs: 12, sm: 14, md: 16, lg: 18, xl: 20 },
|
|
25
|
+
};
|
|
26
|
+
const styles = typeof stylesFn === "function" ? stylesFn(stubTheme) : stylesFn;
|
|
29
27
|
return Object.assign({}, styles, { useVariants: () => undefined });
|
|
30
28
|
},
|
|
31
29
|
},
|
|
@@ -35,11 +33,16 @@ vi.mock("react-native-unistyles", () => {
|
|
|
35
33
|
const { Button } = await import("./Button");
|
|
36
34
|
|
|
37
35
|
describe("Button", () => {
|
|
38
|
-
it("is
|
|
36
|
+
it("is a function (factory-wrapped component)", () => {
|
|
39
37
|
expect(typeof Button).toBe("function");
|
|
40
38
|
});
|
|
41
39
|
|
|
42
|
-
it("has
|
|
43
|
-
expect(Button.
|
|
40
|
+
it("has displayName 'Button'", () => {
|
|
41
|
+
expect(Button.displayName).toBe("Button");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("exposes .extend() that returns the override config unchanged", () => {
|
|
45
|
+
const override = { defaultProps: { variant: "ghost" as const } };
|
|
46
|
+
expect(Button.extend(override)).toBe(override);
|
|
44
47
|
});
|
|
45
48
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createComponent } from "@spelyco/react-lib";
|
|
1
2
|
import type { PressableProps } from "react-native";
|
|
2
3
|
import { Pressable, Text } from "react-native";
|
|
3
4
|
import { StyleSheet, type UnistylesVariants } from "react-native-unistyles";
|
|
@@ -9,45 +10,49 @@ export type ButtonProps = Omit<PressableProps, "children"> &
|
|
|
9
10
|
label: string;
|
|
10
11
|
};
|
|
11
12
|
|
|
12
|
-
export const Button = ({
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
...rest
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
export const Button = createComponent<ButtonProps>({
|
|
14
|
+
name: "Button",
|
|
15
|
+
defaultProps: {
|
|
16
|
+
variant: "primary",
|
|
17
|
+
size: "md",
|
|
18
|
+
},
|
|
19
|
+
render: ({ label, variant, size, disabled, style, ...rest }) => {
|
|
20
|
+
styles.useVariants({
|
|
21
|
+
variant: variant ?? "primary",
|
|
22
|
+
size: size ?? "md",
|
|
23
|
+
disabled: disabled ?? false,
|
|
24
|
+
});
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
26
|
+
return (
|
|
27
|
+
<Pressable
|
|
28
|
+
accessibilityRole="button"
|
|
29
|
+
disabled={disabled}
|
|
30
|
+
style={(state) => [styles.root, typeof style === "function" ? style(state) : style]}
|
|
31
|
+
{...rest}
|
|
32
|
+
>
|
|
33
|
+
<Text style={styles.label}>{label}</Text>
|
|
34
|
+
</Pressable>
|
|
35
|
+
);
|
|
36
|
+
},
|
|
37
|
+
});
|
|
33
38
|
|
|
34
39
|
const styles = StyleSheet.create((theme) => ({
|
|
35
40
|
root: {
|
|
36
41
|
alignItems: "center",
|
|
37
42
|
justifyContent: "center",
|
|
38
|
-
borderRadius: theme.
|
|
43
|
+
borderRadius: theme.radius.md,
|
|
39
44
|
variants: {
|
|
40
45
|
variant: {
|
|
41
46
|
primary: {
|
|
42
|
-
backgroundColor: theme.
|
|
47
|
+
backgroundColor: theme.primary,
|
|
43
48
|
},
|
|
44
49
|
secondary: {
|
|
45
|
-
backgroundColor: theme.colors.
|
|
50
|
+
backgroundColor: theme.colors.neutral[theme.colorScheme === "dark" ? 7 : 1],
|
|
46
51
|
},
|
|
47
52
|
ghost: {
|
|
48
|
-
backgroundColor:
|
|
53
|
+
backgroundColor: "transparent",
|
|
49
54
|
borderWidth: 1,
|
|
50
|
-
borderColor: theme.colors.
|
|
55
|
+
borderColor: theme.colors.neutral[theme.colorScheme === "dark" ? 6 : 3],
|
|
51
56
|
},
|
|
52
57
|
},
|
|
53
58
|
size: {
|
|
@@ -75,14 +80,14 @@ const styles = StyleSheet.create((theme) => ({
|
|
|
75
80
|
fontWeight: "600",
|
|
76
81
|
variants: {
|
|
77
82
|
variant: {
|
|
78
|
-
primary: { color:
|
|
79
|
-
secondary: { color: theme.colors.
|
|
80
|
-
ghost: { color: theme.colors.
|
|
83
|
+
primary: { color: "#ffffff" },
|
|
84
|
+
secondary: { color: theme.colors.neutral[theme.colorScheme === "dark" ? 0 : 9] },
|
|
85
|
+
ghost: { color: theme.colors.neutral[theme.colorScheme === "dark" ? 0 : 9] },
|
|
81
86
|
},
|
|
82
87
|
size: {
|
|
83
|
-
sm: { fontSize:
|
|
84
|
-
md: { fontSize:
|
|
85
|
-
lg: { fontSize:
|
|
88
|
+
sm: { fontSize: theme.fontSizes.sm },
|
|
89
|
+
md: { fontSize: theme.fontSizes.md },
|
|
90
|
+
lg: { fontSize: theme.fontSizes.lg },
|
|
86
91
|
},
|
|
87
92
|
},
|
|
88
93
|
},
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("react-native", () => ({
|
|
4
|
+
Text: "Text",
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
vi.mock("react-native-unistyles", () => {
|
|
8
|
+
type StylesObject = Record<string, unknown>;
|
|
9
|
+
return {
|
|
10
|
+
StyleSheet: {
|
|
11
|
+
create: (stylesFn: ((theme: unknown) => StylesObject) | StylesObject) => {
|
|
12
|
+
const stubTheme = {
|
|
13
|
+
colorScheme: "light",
|
|
14
|
+
colors: {
|
|
15
|
+
neutral: ["#fafafa", "", "", "", "", "", "", "", "", "#18181b"],
|
|
16
|
+
},
|
|
17
|
+
fontSizes: { xs: 12, sm: 14, md: 16, lg: 18, xl: 20 },
|
|
18
|
+
lineHeights: { xs: 1.4, sm: 1.45, md: 1.55, lg: 1.6, xl: 1.65 },
|
|
19
|
+
};
|
|
20
|
+
const styles = typeof stylesFn === "function" ? stylesFn(stubTheme) : stylesFn;
|
|
21
|
+
return Object.assign({}, styles, { useVariants: () => undefined });
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
vi.mock("@spelyco/react-lib", () => ({
|
|
28
|
+
createComponent: <TProps,>(config: {
|
|
29
|
+
name: string;
|
|
30
|
+
defaultProps?: Partial<TProps>;
|
|
31
|
+
render: (props: TProps) => unknown;
|
|
32
|
+
}) => {
|
|
33
|
+
const Component = (props: TProps) => config.render(props);
|
|
34
|
+
(Component as { displayName?: string }).displayName = config.name;
|
|
35
|
+
(
|
|
36
|
+
Component as { extend?: (o: unknown) => unknown }
|
|
37
|
+
).extend = (override) => override;
|
|
38
|
+
return Component;
|
|
39
|
+
},
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
const { Text } = await import("./Text");
|
|
43
|
+
|
|
44
|
+
describe("Text", () => {
|
|
45
|
+
it("is a function (factory-wrapped component)", () => {
|
|
46
|
+
expect(typeof Text).toBe("function");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("has displayName 'Text'", () => {
|
|
50
|
+
expect((Text as { displayName?: string }).displayName).toBe("Text");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("exposes .extend() that returns the override config unchanged", () => {
|
|
54
|
+
const override = { defaultProps: { weight: "bold" as const } };
|
|
55
|
+
expect(
|
|
56
|
+
(Text as { extend: (o: unknown) => unknown }).extend(override),
|
|
57
|
+
).toBe(override);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { createComponent, type SpelycoSize } from "@spelyco/react-lib";
|
|
2
|
+
import { Text as RNText, type TextProps as RNTextProps } from "react-native";
|
|
3
|
+
import { StyleSheet } from "react-native-unistyles";
|
|
4
|
+
|
|
5
|
+
export type TextWeight = "normal" | "medium" | "bold";
|
|
6
|
+
|
|
7
|
+
export interface TextProps extends RNTextProps {
|
|
8
|
+
size?: SpelycoSize;
|
|
9
|
+
weight?: TextWeight;
|
|
10
|
+
/** Direct hex color. When omitted, auto-resolves to the neutral text color
|
|
11
|
+
* for the active color scheme. */
|
|
12
|
+
color?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const Text = createComponent<TextProps>({
|
|
16
|
+
name: "Text",
|
|
17
|
+
defaultProps: { size: "md", weight: "normal" },
|
|
18
|
+
render: ({ size = "md", weight = "normal", color, style, children, ...rest }) => {
|
|
19
|
+
styles.useVariants({ size, weight });
|
|
20
|
+
return (
|
|
21
|
+
<RNText {...rest} style={[styles.text, color ? { color } : null, style]}>
|
|
22
|
+
{children}
|
|
23
|
+
</RNText>
|
|
24
|
+
);
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const styles = StyleSheet.create((theme) => ({
|
|
29
|
+
text: {
|
|
30
|
+
color: theme.colors.neutral[theme.colorScheme === "dark" ? 0 : 9],
|
|
31
|
+
variants: {
|
|
32
|
+
size: {
|
|
33
|
+
xs: {
|
|
34
|
+
fontSize: theme.fontSizes.xs,
|
|
35
|
+
lineHeight: theme.fontSizes.xs * theme.lineHeights.xs,
|
|
36
|
+
},
|
|
37
|
+
sm: {
|
|
38
|
+
fontSize: theme.fontSizes.sm,
|
|
39
|
+
lineHeight: theme.fontSizes.sm * theme.lineHeights.sm,
|
|
40
|
+
},
|
|
41
|
+
md: {
|
|
42
|
+
fontSize: theme.fontSizes.md,
|
|
43
|
+
lineHeight: theme.fontSizes.md * theme.lineHeights.md,
|
|
44
|
+
},
|
|
45
|
+
lg: {
|
|
46
|
+
fontSize: theme.fontSizes.lg,
|
|
47
|
+
lineHeight: theme.fontSizes.lg * theme.lineHeights.lg,
|
|
48
|
+
},
|
|
49
|
+
xl: {
|
|
50
|
+
fontSize: theme.fontSizes.xl,
|
|
51
|
+
lineHeight: theme.fontSizes.xl * theme.lineHeights.xl,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
weight: {
|
|
55
|
+
normal: { fontWeight: "400" },
|
|
56
|
+
medium: { fontWeight: "500" },
|
|
57
|
+
bold: { fontWeight: "700" },
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
}));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Text, type TextProps, type TextWeight } from "./Text";
|
package/src/components/index.ts
CHANGED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { SpelycoColorScheme, SpelycoComputedColorScheme } from "@spelyco/react-lib";
|
|
2
|
+
import { useColorSchemeStore } from "../store/colorScheme";
|
|
3
|
+
|
|
4
|
+
export interface UseSpelycoColorSchemeReturn {
|
|
5
|
+
/** User preference: 'light' | 'dark' | 'auto'. */
|
|
6
|
+
colorScheme: SpelycoColorScheme;
|
|
7
|
+
/** Resolved scheme actually applied: 'light' | 'dark'. */
|
|
8
|
+
computedColorScheme: SpelycoComputedColorScheme;
|
|
9
|
+
setColorScheme: (scheme: SpelycoColorScheme) => void;
|
|
10
|
+
/** Cycles light ↔ dark. If currently 'auto', toggles from the OS value. */
|
|
11
|
+
toggleColorScheme: () => void;
|
|
12
|
+
/** Resets back to 'auto' (follow OS). */
|
|
13
|
+
clearColorScheme: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const useSpelycoColorScheme = (): UseSpelycoColorSchemeReturn => {
|
|
17
|
+
const colorScheme = useColorSchemeStore((s) => s.colorScheme);
|
|
18
|
+
const systemColorScheme = useColorSchemeStore((s) => s.systemColorScheme);
|
|
19
|
+
const setColorScheme = useColorSchemeStore((s) => s.setColorScheme);
|
|
20
|
+
const toggleColorScheme = useColorSchemeStore((s) => s.toggleColorScheme);
|
|
21
|
+
const clearColorScheme = useColorSchemeStore((s) => s.clearColorScheme);
|
|
22
|
+
|
|
23
|
+
const computedColorScheme: SpelycoComputedColorScheme =
|
|
24
|
+
colorScheme === "auto" ? systemColorScheme : colorScheme;
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
colorScheme,
|
|
28
|
+
computedColorScheme,
|
|
29
|
+
setColorScheme,
|
|
30
|
+
toggleColorScheme,
|
|
31
|
+
clearColorScheme,
|
|
32
|
+
};
|
|
33
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
// Boot Unistyles BEFORE any component file is evaluated. The barrel below
|
|
2
|
+
// imports `Button`, `Box`, `Text` which all call `StyleSheet.create` at
|
|
3
|
+
// module-load time; without this side-effect import they would race
|
|
4
|
+
// `StyleSheet.configure` and Unistyles would warn.
|
|
5
|
+
import "./bootUnistyles";
|
|
6
|
+
|
|
7
|
+
// Re-export the shared lib so consumers only need `@spelyco/react-native`.
|
|
8
|
+
// Pulls in `useSpelycoTheme`, `resolveTheme`, `DEFAULT_THEME`, factory helpers,
|
|
9
|
+
// and all theme types.
|
|
10
|
+
export * from "@spelyco/react-lib";
|
|
1
11
|
export * from "./components";
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
breakpoints,
|
|
6
|
-
darkTheme,
|
|
7
|
-
lightTheme,
|
|
8
|
-
} from "./themes";
|
|
9
|
-
export {
|
|
10
|
-
type ConfigureUnistylesOptions,
|
|
11
|
-
configureUnistyles,
|
|
12
|
-
type UnistylesInitialTheme,
|
|
13
|
-
} from "./unistyles";
|
|
12
|
+
export * from "./hooks";
|
|
13
|
+
export * from "./provider";
|
|
14
|
+
export * from "./store";
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { ThemeProvider } from "@react-navigation/native";
|
|
2
|
+
import {
|
|
3
|
+
resolveTheme,
|
|
4
|
+
type SpelycoColorScheme,
|
|
5
|
+
SpelycoThemeContext,
|
|
6
|
+
type SpelycoThemeOverride,
|
|
7
|
+
} from "@spelyco/react-lib";
|
|
8
|
+
import { setBackgroundColorAsync } from "expo-system-ui";
|
|
9
|
+
import { type ReactNode, useEffect, useMemo, useState } from "react";
|
|
10
|
+
import { StatusBar } from "react-native";
|
|
11
|
+
import { UnistylesRuntime } from "react-native-unistyles";
|
|
12
|
+
import {
|
|
13
|
+
setupColorSchemeListener,
|
|
14
|
+
teardownColorSchemeListener,
|
|
15
|
+
useColorSchemeStore,
|
|
16
|
+
} from "../store/colorScheme";
|
|
17
|
+
import { toNavigationTheme } from "./toNavigationTheme";
|
|
18
|
+
import { toUnistylesTheme } from "./toUnistylesTheme";
|
|
19
|
+
|
|
20
|
+
export interface SpelycoProviderProps {
|
|
21
|
+
/** Partial theme override merged on top of `DEFAULT_THEME`. */
|
|
22
|
+
theme?: SpelycoThemeOverride;
|
|
23
|
+
/** User-facing default if no persisted preference exists. */
|
|
24
|
+
defaultColorScheme?: SpelycoColorScheme;
|
|
25
|
+
children: ReactNode;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function SpelycoProvider({
|
|
29
|
+
theme: themeOverride,
|
|
30
|
+
defaultColorScheme = "auto",
|
|
31
|
+
children,
|
|
32
|
+
}: SpelycoProviderProps) {
|
|
33
|
+
const resolvedTheme = useMemo(() => resolveTheme(themeOverride), [themeOverride]);
|
|
34
|
+
|
|
35
|
+
const colorScheme = useColorSchemeStore((s) => s.colorScheme);
|
|
36
|
+
const systemColorScheme = useColorSchemeStore((s) => s.systemColorScheme);
|
|
37
|
+
const setColorScheme = useColorSchemeStore((s) => s.setColorScheme);
|
|
38
|
+
|
|
39
|
+
const computedColorScheme = colorScheme === "auto" ? systemColorScheme : colorScheme;
|
|
40
|
+
|
|
41
|
+
// Honour `defaultColorScheme` on first mount when no preference is persisted.
|
|
42
|
+
useState(() => {
|
|
43
|
+
if (colorScheme === "auto" && defaultColorScheme !== "auto") {
|
|
44
|
+
setColorScheme(defaultColorScheme);
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Push the user-supplied theme override into Unistyles. Boot already ran
|
|
50
|
+
// with DEFAULT_THEME in `bootUnistyles.ts`; here we replace both schemes if
|
|
51
|
+
// an override exists so components see the merged tokens.
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (!themeOverride) return;
|
|
54
|
+
const nextLight = toUnistylesTheme(resolvedTheme, "light");
|
|
55
|
+
const nextDark = toUnistylesTheme(resolvedTheme, "dark");
|
|
56
|
+
UnistylesRuntime.updateTheme("light", () => nextLight);
|
|
57
|
+
UnistylesRuntime.updateTheme("dark", () => nextDark);
|
|
58
|
+
}, [themeOverride, resolvedTheme]);
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
setupColorSchemeListener();
|
|
62
|
+
return () => teardownColorSchemeListener();
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
UnistylesRuntime.setTheme(computedColorScheme);
|
|
67
|
+
}, [computedColorScheme]);
|
|
68
|
+
|
|
69
|
+
// System bars: status bar + Android root background. Runs every time the
|
|
70
|
+
// computed scheme flips so the OS chrome stays in sync.
|
|
71
|
+
const systemBars = resolvedTheme.systemBars?.[computedColorScheme];
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (systemBars?.statusBarBackgroundColor) {
|
|
74
|
+
setBackgroundColorAsync(systemBars.statusBarBackgroundColor).catch(() => {});
|
|
75
|
+
}
|
|
76
|
+
}, [systemBars?.statusBarBackgroundColor]);
|
|
77
|
+
|
|
78
|
+
const statusBarStyle = (() => {
|
|
79
|
+
const style = systemBars?.statusBarStyle ?? "auto";
|
|
80
|
+
if (style === "light") return "light-content" as const;
|
|
81
|
+
if (style === "dark") return "dark-content" as const;
|
|
82
|
+
return "default" as const;
|
|
83
|
+
})();
|
|
84
|
+
|
|
85
|
+
const navigationTheme = useMemo(
|
|
86
|
+
() => toNavigationTheme(resolvedTheme, computedColorScheme),
|
|
87
|
+
[resolvedTheme, computedColorScheme]
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<SpelycoThemeContext.Provider value={resolvedTheme}>
|
|
92
|
+
<ThemeProvider value={navigationTheme}>
|
|
93
|
+
<StatusBar
|
|
94
|
+
barStyle={statusBarStyle}
|
|
95
|
+
backgroundColor={systemBars?.statusBarBackgroundColor}
|
|
96
|
+
animated
|
|
97
|
+
/>
|
|
98
|
+
{children}
|
|
99
|
+
</ThemeProvider>
|
|
100
|
+
</SpelycoThemeContext.Provider>
|
|
101
|
+
);
|
|
102
|
+
}
|