@spelyco/react-native 0.0.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 +218 -0
- 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 +59 -0
- package/dist/components/Button/Button.d.ts.map +1 -0
- package/dist/components/Button/index.d.ts +3 -0
- package/dist/components/Button/index.d.ts.map +1 -0
- 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 +4 -0
- package/dist/components/index.d.ts.map +1 -0
- 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 +7 -11
- package/dist/index.d.ts.map +1 -0
- 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 +11 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +43 -16
- 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 +48 -0
- package/src/components/Button/Button.tsx +94 -0
- package/src/components/Button/index.ts +2 -0
- 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 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useSpelycoColorScheme.ts +33 -0
- package/src/index.ts +14 -0
- 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 +10 -0
- package/dist/index.d.mts +0 -11
- package/dist/index.js +0 -28
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -26
- package/dist/index.mjs.map +0 -1
package/package.json
CHANGED
|
@@ -1,23 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spelyco/react-native",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "React Native UI components for Spelyco",
|
|
5
|
-
"keywords": [
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "React Native UI components for Spelyco, powered by Unistyles 3",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"react-native",
|
|
7
|
+
"ui",
|
|
8
|
+
"components",
|
|
9
|
+
"spelyco",
|
|
10
|
+
"unistyles"
|
|
11
|
+
],
|
|
6
12
|
"license": "MIT",
|
|
7
|
-
"main": "./
|
|
8
|
-
"module": "./
|
|
13
|
+
"main": "./src/index.ts",
|
|
14
|
+
"module": "./src/index.ts",
|
|
15
|
+
"react-native": "./src/index.ts",
|
|
9
16
|
"types": "./dist/index.d.ts",
|
|
10
17
|
"exports": {
|
|
11
18
|
".": {
|
|
12
19
|
"types": "./dist/index.d.ts",
|
|
13
|
-
"
|
|
14
|
-
"
|
|
20
|
+
"react-native": "./src/index.ts",
|
|
21
|
+
"import": "./src/index.ts",
|
|
22
|
+
"require": "./src/index.ts"
|
|
15
23
|
}
|
|
16
24
|
},
|
|
17
|
-
"files": [
|
|
25
|
+
"files": [
|
|
26
|
+
"src",
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
18
29
|
"scripts": {
|
|
19
|
-
"build": "
|
|
20
|
-
"dev": "
|
|
30
|
+
"build": "tsc -p tsconfig.build.json",
|
|
31
|
+
"dev": "tsc -p tsconfig.build.json --watch",
|
|
21
32
|
"lint": "biome check src/",
|
|
22
33
|
"lint:fix": "biome check --write src/",
|
|
23
34
|
"test": "vitest run --passWithNoTests",
|
|
@@ -25,18 +36,34 @@
|
|
|
25
36
|
"clean": "rm -rf dist"
|
|
26
37
|
},
|
|
27
38
|
"peerDependencies": {
|
|
39
|
+
"@react-native-async-storage/async-storage": "*",
|
|
40
|
+
"@react-navigation/native": "^7",
|
|
41
|
+
"expo-status-bar": "*",
|
|
42
|
+
"expo-system-ui": "*",
|
|
28
43
|
"react": ">=18",
|
|
29
|
-
"react-native": ">=0.
|
|
44
|
+
"react-native": ">=0.76",
|
|
45
|
+
"react-native-edge-to-edge": "*",
|
|
46
|
+
"react-native-nitro-modules": "*",
|
|
47
|
+
"react-native-reanimated": "*",
|
|
48
|
+
"react-native-unistyles": "^3.2.4"
|
|
30
49
|
},
|
|
31
50
|
"dependencies": {
|
|
32
|
-
"@spelyco/react-lib": "
|
|
51
|
+
"@spelyco/react-lib": "1.3.0",
|
|
52
|
+
"zustand": "^5.0.2"
|
|
33
53
|
},
|
|
34
54
|
"devDependencies": {
|
|
35
|
-
"@
|
|
55
|
+
"@react-native-async-storage/async-storage": "^2.2.0",
|
|
56
|
+
"@react-navigation/native": "^7.2.2",
|
|
57
|
+
"@spelyco/tsconfig": "*",
|
|
36
58
|
"@types/react": "^19.2.14",
|
|
59
|
+
"expo-status-bar": "~55.0.6",
|
|
60
|
+
"expo-system-ui": "~55.0.18",
|
|
37
61
|
"react": "^19.2.4",
|
|
38
|
-
"react-native": "^0.
|
|
39
|
-
"
|
|
62
|
+
"react-native": "^0.83.6",
|
|
63
|
+
"react-native-edge-to-edge": "^1.8.1",
|
|
64
|
+
"react-native-nitro-modules": "^0.35.6",
|
|
65
|
+
"react-native-reanimated": "^4.2.1",
|
|
66
|
+
"react-native-unistyles": "^3.2.4",
|
|
40
67
|
"vitest": "^4.1.1"
|
|
41
68
|
}
|
|
42
|
-
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { DEFAULT_THEME } from "@spelyco/react-lib";
|
|
2
|
+
import { StyleSheet } from "react-native-unistyles";
|
|
3
|
+
import { toUnistylesTheme } from "./provider/toUnistylesTheme";
|
|
4
|
+
import "./types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Boots Unistyles with the default theme at module-load time.
|
|
8
|
+
*
|
|
9
|
+
* `StyleSheet.configure` must run before any `StyleSheet.create` call,
|
|
10
|
+
* otherwise Unistyles emits "StyleSheet.configure was not called" warnings.
|
|
11
|
+
* Component files (e.g. `Button.tsx`) invoke `StyleSheet.create` at module
|
|
12
|
+
* evaluation, so configuration cannot wait for `SpelycoProvider` to mount.
|
|
13
|
+
*
|
|
14
|
+
* The side-effect here is loaded by `src/index.ts` ahead of any component
|
|
15
|
+
* re-exports, guaranteeing the correct boot order.
|
|
16
|
+
*
|
|
17
|
+
* Theme overrides passed to `<SpelycoProvider theme={...}>` are applied later
|
|
18
|
+
* via `UnistylesRuntime.updateTheme()` once the provider mounts.
|
|
19
|
+
*/
|
|
20
|
+
StyleSheet.configure({
|
|
21
|
+
themes: {
|
|
22
|
+
light: toUnistylesTheme(DEFAULT_THEME, "light"),
|
|
23
|
+
dark: toUnistylesTheme(DEFAULT_THEME, "dark"),
|
|
24
|
+
},
|
|
25
|
+
breakpoints: DEFAULT_THEME.breakpoints,
|
|
26
|
+
settings: { initialTheme: "light" },
|
|
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";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("react-native", () => ({
|
|
4
|
+
Pressable: "Pressable",
|
|
5
|
+
Text: "Text",
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
vi.mock("react-native-unistyles", () => {
|
|
9
|
+
type StylesObject = Record<string, unknown>;
|
|
10
|
+
return {
|
|
11
|
+
StyleSheet: {
|
|
12
|
+
create: (stylesFn: ((theme: unknown) => StylesObject) | StylesObject) => {
|
|
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;
|
|
27
|
+
return Object.assign({}, styles, { useVariants: () => undefined });
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const { Button } = await import("./Button");
|
|
34
|
+
|
|
35
|
+
describe("Button", () => {
|
|
36
|
+
it("is a function (factory-wrapped component)", () => {
|
|
37
|
+
expect(typeof Button).toBe("function");
|
|
38
|
+
});
|
|
39
|
+
|
|
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);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { createComponent } from "@spelyco/react-lib";
|
|
2
|
+
import type { PressableProps } from "react-native";
|
|
3
|
+
import { Pressable, Text } from "react-native";
|
|
4
|
+
import { StyleSheet, type UnistylesVariants } from "react-native-unistyles";
|
|
5
|
+
|
|
6
|
+
type ButtonVariants = UnistylesVariants<typeof styles>;
|
|
7
|
+
|
|
8
|
+
export type ButtonProps = Omit<PressableProps, "children"> &
|
|
9
|
+
ButtonVariants & {
|
|
10
|
+
label: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
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
|
+
});
|
|
25
|
+
|
|
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
|
+
});
|
|
38
|
+
|
|
39
|
+
const styles = StyleSheet.create((theme) => ({
|
|
40
|
+
root: {
|
|
41
|
+
alignItems: "center",
|
|
42
|
+
justifyContent: "center",
|
|
43
|
+
borderRadius: theme.radius.md,
|
|
44
|
+
variants: {
|
|
45
|
+
variant: {
|
|
46
|
+
primary: {
|
|
47
|
+
backgroundColor: theme.primary,
|
|
48
|
+
},
|
|
49
|
+
secondary: {
|
|
50
|
+
backgroundColor: theme.colors.neutral[theme.colorScheme === "dark" ? 7 : 1],
|
|
51
|
+
},
|
|
52
|
+
ghost: {
|
|
53
|
+
backgroundColor: "transparent",
|
|
54
|
+
borderWidth: 1,
|
|
55
|
+
borderColor: theme.colors.neutral[theme.colorScheme === "dark" ? 6 : 3],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
size: {
|
|
59
|
+
sm: {
|
|
60
|
+
paddingHorizontal: theme.spacing.md,
|
|
61
|
+
paddingVertical: theme.spacing.xs,
|
|
62
|
+
},
|
|
63
|
+
md: {
|
|
64
|
+
paddingHorizontal: theme.spacing.lg,
|
|
65
|
+
paddingVertical: theme.spacing.sm,
|
|
66
|
+
},
|
|
67
|
+
lg: {
|
|
68
|
+
paddingHorizontal: theme.spacing.xl,
|
|
69
|
+
paddingVertical: theme.spacing.md,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
disabled: {
|
|
73
|
+
true: {
|
|
74
|
+
opacity: 0.5,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
label: {
|
|
80
|
+
fontWeight: "600",
|
|
81
|
+
variants: {
|
|
82
|
+
variant: {
|
|
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] },
|
|
86
|
+
},
|
|
87
|
+
size: {
|
|
88
|
+
sm: { fontSize: theme.fontSizes.sm },
|
|
89
|
+
md: { fontSize: theme.fontSizes.md },
|
|
90
|
+
lg: { fontSize: theme.fontSizes.lg },
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
}));
|
|
@@ -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";
|
|
@@ -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
ADDED
|
@@ -0,0 +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";
|
|
11
|
+
export * from "./components";
|
|
12
|
+
export * from "./hooks";
|
|
13
|
+
export * from "./provider";
|
|
14
|
+
export * from "./store";
|