@gryt/ui-native 0.1.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 +86 -0
- package/dist/components/Accordion/Accordion.d.ts +35 -0
- package/dist/components/Accordion/Accordion.d.ts.map +1 -0
- package/dist/components/Accordion/Accordion.js +66 -0
- package/dist/components/Alert/Alert.d.ts +21 -0
- package/dist/components/Alert/Alert.d.ts.map +1 -0
- package/dist/components/Alert/Alert.js +35 -0
- package/dist/components/AlertDialog/AlertDialog.d.ts +38 -0
- package/dist/components/AlertDialog/AlertDialog.d.ts.map +1 -0
- package/dist/components/AlertDialog/AlertDialog.js +15 -0
- package/dist/components/Avatar/Avatar.d.ts +10 -0
- package/dist/components/Avatar/Avatar.d.ts.map +1 -0
- package/dist/components/Avatar/Avatar.js +57 -0
- package/dist/components/Badge/Badge.d.ts +13 -0
- package/dist/components/Badge/Badge.d.ts.map +1 -0
- package/dist/components/Badge/Badge.js +28 -0
- package/dist/components/Button/Button.d.ts +24 -0
- package/dist/components/Button/Button.d.ts.map +1 -0
- package/dist/components/Button/Button.js +61 -0
- package/dist/components/Card/Card.d.ts +19 -0
- package/dist/components/Card/Card.d.ts.map +1 -0
- package/dist/components/Card/Card.js +45 -0
- package/dist/components/Checkbox/Checkbox.d.ts +24 -0
- package/dist/components/Checkbox/Checkbox.d.ts.map +1 -0
- package/dist/components/Checkbox/Checkbox.js +43 -0
- package/dist/components/Chip/Chip.d.ts +17 -0
- package/dist/components/Chip/Chip.d.ts.map +1 -0
- package/dist/components/Chip/Chip.js +38 -0
- package/dist/components/Collapsible/Collapsible.d.ts +31 -0
- package/dist/components/Collapsible/Collapsible.d.ts.map +1 -0
- package/dist/components/Collapsible/Collapsible.js +34 -0
- package/dist/components/Dialog/Dialog.d.ts +67 -0
- package/dist/components/Dialog/Dialog.d.ts.map +1 -0
- package/dist/components/Dialog/Dialog.js +112 -0
- package/dist/components/Divider/Divider.d.ts +16 -0
- package/dist/components/Divider/Divider.d.ts.map +1 -0
- package/dist/components/Divider/Divider.js +21 -0
- package/dist/components/Drawer/Drawer.d.ts +44 -0
- package/dist/components/Drawer/Drawer.d.ts.map +1 -0
- package/dist/components/Drawer/Drawer.js +83 -0
- package/dist/components/Menu/Menu.d.ts +56 -0
- package/dist/components/Menu/Menu.d.ts.map +1 -0
- package/dist/components/Menu/Menu.js +76 -0
- package/dist/components/Meter/Meter.d.ts +21 -0
- package/dist/components/Meter/Meter.d.ts.map +1 -0
- package/dist/components/Meter/Meter.js +32 -0
- package/dist/components/NumberField/NumberField.d.ts +23 -0
- package/dist/components/NumberField/NumberField.d.ts.map +1 -0
- package/dist/components/NumberField/NumberField.js +56 -0
- package/dist/components/OtpField/OtpField.d.ts +24 -0
- package/dist/components/OtpField/OtpField.d.ts.map +1 -0
- package/dist/components/OtpField/OtpField.js +49 -0
- package/dist/components/Popover/Popover.d.ts +48 -0
- package/dist/components/Popover/Popover.d.ts.map +1 -0
- package/dist/components/Popover/Popover.js +54 -0
- package/dist/components/Progress/Progress.d.ts +18 -0
- package/dist/components/Progress/Progress.d.ts.map +1 -0
- package/dist/components/Progress/Progress.js +30 -0
- package/dist/components/Radio/Radio.d.ts +28 -0
- package/dist/components/Radio/Radio.d.ts.map +1 -0
- package/dist/components/Radio/Radio.js +53 -0
- package/dist/components/ScrollArea/ScrollArea.d.ts +22 -0
- package/dist/components/ScrollArea/ScrollArea.d.ts.map +1 -0
- package/dist/components/ScrollArea/ScrollArea.js +17 -0
- package/dist/components/Select/Select.d.ts +34 -0
- package/dist/components/Select/Select.d.ts.map +1 -0
- package/dist/components/Select/Select.js +71 -0
- package/dist/components/Skeleton/Skeleton.d.ts +18 -0
- package/dist/components/Skeleton/Skeleton.d.ts.map +1 -0
- package/dist/components/Skeleton/Skeleton.js +28 -0
- package/dist/components/Slider/Slider.d.ts +29 -0
- package/dist/components/Slider/Slider.d.ts.map +1 -0
- package/dist/components/Slider/Slider.js +94 -0
- package/dist/components/Spinner/Spinner.d.ts +20 -0
- package/dist/components/Spinner/Spinner.d.ts.map +1 -0
- package/dist/components/Spinner/Spinner.js +18 -0
- package/dist/components/Surface/Surface.d.ts +17 -0
- package/dist/components/Surface/Surface.d.ts.map +1 -0
- package/dist/components/Surface/Surface.js +22 -0
- package/dist/components/Switch/Switch.d.ts +25 -0
- package/dist/components/Switch/Switch.d.ts.map +1 -0
- package/dist/components/Switch/Switch.js +65 -0
- package/dist/components/Tabs/Tabs.d.ts +51 -0
- package/dist/components/Tabs/Tabs.d.ts.map +1 -0
- package/dist/components/Tabs/Tabs.js +82 -0
- package/dist/components/TextField/TextField.d.ts +15 -0
- package/dist/components/TextField/TextField.d.ts.map +1 -0
- package/dist/components/TextField/TextField.js +47 -0
- package/dist/components/Toast/Toast.d.ts +31 -0
- package/dist/components/Toast/Toast.d.ts.map +1 -0
- package/dist/components/Toast/Toast.js +86 -0
- package/dist/components/Toggle/Toggle.d.ts +19 -0
- package/dist/components/Toggle/Toggle.d.ts.map +1 -0
- package/dist/components/Toggle/Toggle.js +41 -0
- package/dist/components/Toolbar/Toolbar.d.ts +25 -0
- package/dist/components/Toolbar/Toolbar.d.ts.map +1 -0
- package/dist/components/Toolbar/Toolbar.js +35 -0
- package/dist/components/Tooltip/Tooltip.d.ts +40 -0
- package/dist/components/Tooltip/Tooltip.d.ts.map +1 -0
- package/dist/components/Tooltip/Tooltip.js +52 -0
- package/dist/hooks/useReducedMotion.d.ts +10 -0
- package/dist/hooks/useReducedMotion.d.ts.map +1 -0
- package/dist/hooks/useReducedMotion.js +26 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +38 -0
- package/dist/overlay/AnchoredPopup.d.ts +26 -0
- package/dist/overlay/AnchoredPopup.d.ts.map +1 -0
- package/dist/overlay/AnchoredPopup.js +36 -0
- package/dist/overlay/placePopup.d.ts +51 -0
- package/dist/overlay/placePopup.d.ts.map +1 -0
- package/dist/overlay/placePopup.js +63 -0
- package/dist/overlay/useAnchoredPosition.d.ts +18 -0
- package/dist/overlay/useAnchoredPosition.d.ts.map +1 -0
- package/dist/overlay/useAnchoredPosition.js +25 -0
- package/dist/overlay/useOpenState.d.ts +17 -0
- package/dist/overlay/useOpenState.d.ts.map +1 -0
- package/dist/overlay/useOpenState.js +12 -0
- package/dist/theme/ThemeProvider.d.ts +17 -0
- package/dist/theme/ThemeProvider.d.ts.map +1 -0
- package/dist/theme/ThemeProvider.js +17 -0
- package/dist/theme/createNativeTheme.d.ts +82 -0
- package/dist/theme/createNativeTheme.d.ts.map +1 -0
- package/dist/theme/createNativeTheme.js +65 -0
- package/dist/theme/index.d.ts +6 -0
- package/dist/theme/index.d.ts.map +1 -0
- package/dist/theme/index.js +5 -0
- package/dist/theme/themeContext.d.ts +9 -0
- package/dist/theme/themeContext.d.ts.map +1 -0
- package/dist/theme/themeContext.js +9 -0
- package/dist/theme/tones.d.ts +15 -0
- package/dist/theme/tones.d.ts.map +1 -0
- package/dist/theme/tones.js +11 -0
- package/dist/theme/useTheme.d.ts +8 -0
- package/dist/theme/useTheme.d.ts.map +1 -0
- package/dist/theme/useTheme.js +10 -0
- package/package.json +46 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where to put a popup relative to the thing that opened it.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately free of React and React Native, so it can be tested as
|
|
5
|
+
* arithmetic. React Native's own source is Flow-typed and cannot be parsed by
|
|
6
|
+
* the test runner, so anything importing it is untestable here; keeping the
|
|
7
|
+
* geometry separate is what makes this the part with tests on it.
|
|
8
|
+
*
|
|
9
|
+
* This is also the piece with no React Native equivalent. On the web, Base UI
|
|
10
|
+
* hands positioning to Floating UI: it watches the reference element, flips the
|
|
11
|
+
* popup when it would overflow, shifts it to stay on screen, and keeps doing all
|
|
12
|
+
* of that while the page scrolls. None of that exists here, so this places the
|
|
13
|
+
* popup once against a measurement taken when it opened.
|
|
14
|
+
*
|
|
15
|
+
* It flips and clamps, which is the useful part of Floating UI. It does not
|
|
16
|
+
* follow: if the trigger moves while the popup is open, because a list scrolled
|
|
17
|
+
* underneath it, the popup stays where it was. Both are in the parity
|
|
18
|
+
* exceptions table.
|
|
19
|
+
*/
|
|
20
|
+
export type Side = "top" | "bottom" | "left" | "right";
|
|
21
|
+
export type Align = "start" | "center" | "end";
|
|
22
|
+
export interface AnchorRect {
|
|
23
|
+
x: number;
|
|
24
|
+
y: number;
|
|
25
|
+
width: number;
|
|
26
|
+
height: number;
|
|
27
|
+
}
|
|
28
|
+
export interface PopupPlacement {
|
|
29
|
+
top: number;
|
|
30
|
+
left: number;
|
|
31
|
+
/** The side actually used, which is not the one asked for if it had to flip. */
|
|
32
|
+
side: Side;
|
|
33
|
+
/** How much room the popup has, so it can cap its own height. */
|
|
34
|
+
maxHeight: number;
|
|
35
|
+
}
|
|
36
|
+
export interface AnchorOptions {
|
|
37
|
+
side?: Side;
|
|
38
|
+
align?: Align;
|
|
39
|
+
/** Gap between trigger and popup, in points. Base UI calls this sideOffset. */
|
|
40
|
+
sideOffset?: number;
|
|
41
|
+
/** Keep this much clear of the screen edge. */
|
|
42
|
+
screenPadding?: number;
|
|
43
|
+
}
|
|
44
|
+
export declare function placePopup(anchor: AnchorRect, popup: {
|
|
45
|
+
width: number;
|
|
46
|
+
height: number;
|
|
47
|
+
}, screen: {
|
|
48
|
+
width: number;
|
|
49
|
+
height: number;
|
|
50
|
+
}, { side, align, sideOffset, screenPadding }?: AnchorOptions): PopupPlacement;
|
|
51
|
+
//# sourceMappingURL=placePopup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"placePopup.d.ts","sourceRoot":"","sources":["../../src/overlay/placePopup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,MAAM,IAAI,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAC;AACvD,MAAM,MAAM,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAC;AAE/C,MAAM,WAAW,UAAU;IACzB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,gFAAgF;IAChF,IAAI,EAAE,IAAI,CAAC;IACX,iEAAiE;IACjE,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,+CAA+C;IAC/C,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,wBAAgB,UAAU,CACxB,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EACxC,MAAM,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EACzC,EAAE,IAAe,EAAE,KAAgB,EAAE,UAAc,EAAE,aAAiB,EAAE,GAAE,aAAkB,GAC3F,cAAc,CAmDhB"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where to put a popup relative to the thing that opened it.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately free of React and React Native, so it can be tested as
|
|
5
|
+
* arithmetic. React Native's own source is Flow-typed and cannot be parsed by
|
|
6
|
+
* the test runner, so anything importing it is untestable here; keeping the
|
|
7
|
+
* geometry separate is what makes this the part with tests on it.
|
|
8
|
+
*
|
|
9
|
+
* This is also the piece with no React Native equivalent. On the web, Base UI
|
|
10
|
+
* hands positioning to Floating UI: it watches the reference element, flips the
|
|
11
|
+
* popup when it would overflow, shifts it to stay on screen, and keeps doing all
|
|
12
|
+
* of that while the page scrolls. None of that exists here, so this places the
|
|
13
|
+
* popup once against a measurement taken when it opened.
|
|
14
|
+
*
|
|
15
|
+
* It flips and clamps, which is the useful part of Floating UI. It does not
|
|
16
|
+
* follow: if the trigger moves while the popup is open, because a list scrolled
|
|
17
|
+
* underneath it, the popup stays where it was. Both are in the parity
|
|
18
|
+
* exceptions table.
|
|
19
|
+
*/
|
|
20
|
+
export function placePopup(anchor, popup, screen, { side = "bottom", align = "center", sideOffset = 8, screenPadding = 8 } = {}) {
|
|
21
|
+
const spaceBelow = screen.height - (anchor.y + anchor.height) - sideOffset - screenPadding;
|
|
22
|
+
const spaceAbove = anchor.y - sideOffset - screenPadding;
|
|
23
|
+
// Flip only when the preferred side cannot hold it and the other side can do
|
|
24
|
+
// better. Flipping to something equally cramped just moves the problem.
|
|
25
|
+
let resolved = side;
|
|
26
|
+
if (side === "bottom" && popup.height > spaceBelow && spaceAbove > spaceBelow) {
|
|
27
|
+
resolved = "top";
|
|
28
|
+
}
|
|
29
|
+
else if (side === "top" && popup.height > spaceAbove && spaceBelow > spaceAbove) {
|
|
30
|
+
resolved = "bottom";
|
|
31
|
+
}
|
|
32
|
+
const maxHeight = Math.max(0, resolved === "top" ? spaceAbove : resolved === "bottom" ? spaceBelow : screen.height - screenPadding * 2);
|
|
33
|
+
let top;
|
|
34
|
+
if (resolved === "top") {
|
|
35
|
+
top = anchor.y - Math.min(popup.height, maxHeight) - sideOffset;
|
|
36
|
+
}
|
|
37
|
+
else if (resolved === "bottom") {
|
|
38
|
+
top = anchor.y + anchor.height + sideOffset;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
top = anchor.y + anchor.height / 2 - popup.height / 2;
|
|
42
|
+
}
|
|
43
|
+
let left;
|
|
44
|
+
if (resolved === "left") {
|
|
45
|
+
left = anchor.x - popup.width - sideOffset;
|
|
46
|
+
}
|
|
47
|
+
else if (resolved === "right") {
|
|
48
|
+
left = anchor.x + anchor.width + sideOffset;
|
|
49
|
+
}
|
|
50
|
+
else if (align === "start") {
|
|
51
|
+
left = anchor.x;
|
|
52
|
+
}
|
|
53
|
+
else if (align === "end") {
|
|
54
|
+
left = anchor.x + anchor.width - popup.width;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
left = anchor.x + anchor.width / 2 - popup.width / 2;
|
|
58
|
+
}
|
|
59
|
+
// Clamp last, so a popup wider than its anchor still stays on screen.
|
|
60
|
+
left = Math.min(Math.max(screenPadding, left), Math.max(screenPadding, screen.width - popup.width - screenPadding));
|
|
61
|
+
top = Math.min(Math.max(screenPadding, top), Math.max(screenPadding, screen.height - Math.min(popup.height, maxHeight) - screenPadding));
|
|
62
|
+
return { top, left, side: resolved, maxHeight };
|
|
63
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type View } from "react-native";
|
|
2
|
+
import type { AnchorRect } from "./placePopup.js";
|
|
3
|
+
/**
|
|
4
|
+
* Measures a trigger in window coordinates.
|
|
5
|
+
*
|
|
6
|
+
* `measureInWindow` is asynchronous and its callback can fire after the popup
|
|
7
|
+
* has closed again, so a stale result is dropped rather than applied.
|
|
8
|
+
*/
|
|
9
|
+
export declare function useAnchorMeasure(): {
|
|
10
|
+
anchor: AnchorRect | null;
|
|
11
|
+
measure: (node: View | null) => void;
|
|
12
|
+
clear: () => void;
|
|
13
|
+
};
|
|
14
|
+
export declare function screenSize(): {
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=useAnchoredPosition.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useAnchoredPosition.d.ts","sourceRoot":"","sources":["../../src/overlay/useAnchoredPosition.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,IAAI,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C;;;;;GAKG;AACH,wBAAgB,gBAAgB;;oBAGK,IAAI,GAAG,IAAI;;EAW/C;AAED,wBAAgB,UAAU;;;EAGzB"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useCallback, useState } from "react";
|
|
2
|
+
import { Dimensions } from "react-native";
|
|
3
|
+
/**
|
|
4
|
+
* Measures a trigger in window coordinates.
|
|
5
|
+
*
|
|
6
|
+
* `measureInWindow` is asynchronous and its callback can fire after the popup
|
|
7
|
+
* has closed again, so a stale result is dropped rather than applied.
|
|
8
|
+
*/
|
|
9
|
+
export function useAnchorMeasure() {
|
|
10
|
+
const [anchor, setAnchor] = useState(null);
|
|
11
|
+
const measure = useCallback((node) => {
|
|
12
|
+
if (!node) {
|
|
13
|
+
setAnchor(null);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
node.measureInWindow((x, y, width, height) => {
|
|
17
|
+
setAnchor({ x, y, width, height });
|
|
18
|
+
});
|
|
19
|
+
}, []);
|
|
20
|
+
return { anchor, measure, clear: useCallback(() => setAnchor(null), []) };
|
|
21
|
+
}
|
|
22
|
+
export function screenSize() {
|
|
23
|
+
const { width, height } = Dimensions.get("window");
|
|
24
|
+
return { width, height };
|
|
25
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Controlled or uncontrolled open state, matching Base UI's props.
|
|
3
|
+
*
|
|
4
|
+
* `open` with `onOpenChange` drives it from outside; `defaultOpen` alone lets it
|
|
5
|
+
* manage itself. Base UI takes both and so does everything here, so a call site
|
|
6
|
+
* moving between the two libraries does not change shape.
|
|
7
|
+
*/
|
|
8
|
+
export interface OpenStateProps {
|
|
9
|
+
open?: boolean;
|
|
10
|
+
defaultOpen?: boolean;
|
|
11
|
+
onOpenChange?: (open: boolean) => void;
|
|
12
|
+
}
|
|
13
|
+
export declare function useOpenState({ open: controlled, defaultOpen, onOpenChange, }: OpenStateProps): {
|
|
14
|
+
open: boolean;
|
|
15
|
+
setOpen: (next: boolean) => void;
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=useOpenState.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useOpenState.d.ts","sourceRoot":"","sources":["../../src/overlay/useOpenState.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;CACxC;AAED,wBAAgB,YAAY,CAAC,EAC3B,IAAI,EAAE,UAAU,EAChB,WAAmB,EACnB,YAAY,GACb,EAAE,cAAc;;oBAMN,OAAO;EAQjB"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useCallback, useState } from "react";
|
|
2
|
+
export function useOpenState({ open: controlled, defaultOpen = false, onOpenChange, }) {
|
|
3
|
+
const [uncontrolled, setUncontrolled] = useState(defaultOpen);
|
|
4
|
+
const isControlled = controlled !== undefined;
|
|
5
|
+
const open = isControlled ? controlled : uncontrolled;
|
|
6
|
+
const setOpen = useCallback((next) => {
|
|
7
|
+
if (!isControlled)
|
|
8
|
+
setUncontrolled(next);
|
|
9
|
+
onOpenChange?.(next);
|
|
10
|
+
}, [isControlled, onOpenChange]);
|
|
11
|
+
return { open, setOpen };
|
|
12
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type GrytAppearance, type NativeThemeOptions } from "./createNativeTheme.js";
|
|
3
|
+
export interface GrytThemeProviderProps {
|
|
4
|
+
appearance?: GrytAppearance;
|
|
5
|
+
color?: NativeThemeOptions["color"];
|
|
6
|
+
radius?: NativeThemeOptions["radius"];
|
|
7
|
+
/**
|
|
8
|
+
* Follow the OS setting when `appearance` is not given.
|
|
9
|
+
*
|
|
10
|
+
* The web equivalent is a `prefers-color-scheme` media query, which the
|
|
11
|
+
* client already honours.
|
|
12
|
+
*/
|
|
13
|
+
followSystemAppearance?: boolean;
|
|
14
|
+
children?: ReactNode;
|
|
15
|
+
}
|
|
16
|
+
export declare function GrytThemeProvider({ appearance, color, radius, followSystemAppearance, children, }: GrytThemeProviderProps): import("react").JSX.Element;
|
|
17
|
+
//# sourceMappingURL=ThemeProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ThemeProvider.d.ts","sourceRoot":"","sources":["../../src/theme/ThemeProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAGhD,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACxB,MAAM,qBAAqB,CAAC;AAG7B,MAAM,WAAW,sBAAsB;IACrC,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,KAAK,CAAC,EAAE,kBAAkB,CAAC,OAAO,CAAC,CAAC;IACpC,MAAM,CAAC,EAAE,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IACtC;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,wBAAgB,iBAAiB,CAAC,EAChC,UAAU,EACV,KAAK,EACL,MAAM,EACN,sBAA8B,EAC9B,QAAQ,GACT,EAAE,sBAAsB,+BAgBxB"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { useColorScheme } from "react-native";
|
|
4
|
+
import { createNativeTheme, } from "./createNativeTheme.js";
|
|
5
|
+
import { ThemeContext } from "./themeContext.js";
|
|
6
|
+
export function GrytThemeProvider({ appearance, color, radius, followSystemAppearance = false, children, }) {
|
|
7
|
+
const systemScheme = useColorScheme();
|
|
8
|
+
// Named props rather than a spread options object, so the dependencies are
|
|
9
|
+
// the actual inputs. Spreading meant either a stale theme or rebuilding every
|
|
10
|
+
// ramp on every render, and building a theme is real colour maths.
|
|
11
|
+
const theme = useMemo(() => {
|
|
12
|
+
const resolved = appearance ??
|
|
13
|
+
(followSystemAppearance && systemScheme === "light" ? "light" : undefined);
|
|
14
|
+
return createNativeTheme({ appearance: resolved, color, radius });
|
|
15
|
+
}, [appearance, color, radius, followSystemAppearance, systemScheme]);
|
|
16
|
+
return (_jsx(ThemeContext.Provider, { value: theme, children: children }));
|
|
17
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { grytTokens } from "@gryt/ui/theme";
|
|
2
|
+
/**
|
|
3
|
+
* The same theme, without CSS.
|
|
4
|
+
*
|
|
5
|
+
* `createGrytTheme` in `@gryt/ui` returns `CSSProperties` — a map of
|
|
6
|
+
* `--gryt-accent-9` style custom properties. React Native has no custom
|
|
7
|
+
* properties and no cascade, so it cannot consume that at all.
|
|
8
|
+
*
|
|
9
|
+
* What it can consume is the maths underneath. `neutralScale`, `hueScale` and
|
|
10
|
+
* `alphaScale` are exported from `@gryt/ui/theme` for exactly this, so the two
|
|
11
|
+
* renderers compute their ramps from one implementation of the OKLab
|
|
12
|
+
* conversions rather than two. A copy here would drift the moment somebody
|
|
13
|
+
* tunes a curve on the web.
|
|
14
|
+
*
|
|
15
|
+
* Same inputs as `createGrytTheme`, so a theme a person built in the web client
|
|
16
|
+
* — and which travels as an encoded document — produces the same colours here.
|
|
17
|
+
*/
|
|
18
|
+
export type GrytAppearance = "dark" | "light";
|
|
19
|
+
export interface NativeThemeOptions {
|
|
20
|
+
color?: Partial<Record<keyof typeof grytTokens.color, string>>;
|
|
21
|
+
radius?: Partial<Record<keyof typeof grytTokens.radius, number>>;
|
|
22
|
+
appearance?: GrytAppearance;
|
|
23
|
+
}
|
|
24
|
+
/** Twelve steps, the same shape `@gryt/ui` uses. Index 0 is the page. */
|
|
25
|
+
export type Ramp = readonly string[];
|
|
26
|
+
export interface NativeTheme {
|
|
27
|
+
appearance: GrytAppearance;
|
|
28
|
+
/** The semantic colours, resolved. What most components reach for. */
|
|
29
|
+
color: {
|
|
30
|
+
bg: string;
|
|
31
|
+
surface: string;
|
|
32
|
+
surfaceRaised: string;
|
|
33
|
+
surfaceHover: string;
|
|
34
|
+
border: string;
|
|
35
|
+
text: string;
|
|
36
|
+
muted: string;
|
|
37
|
+
accent: string;
|
|
38
|
+
accentLight: string;
|
|
39
|
+
secondary: string;
|
|
40
|
+
secondaryLight: string;
|
|
41
|
+
success: string;
|
|
42
|
+
danger: string;
|
|
43
|
+
dangerLight: string;
|
|
44
|
+
warning: string;
|
|
45
|
+
onAccent: string;
|
|
46
|
+
onSecondary: string;
|
|
47
|
+
onDanger: string;
|
|
48
|
+
};
|
|
49
|
+
/** The full ramps, for anything that needs a step rather than a role. */
|
|
50
|
+
scales: {
|
|
51
|
+
neutral: Ramp;
|
|
52
|
+
accent: Ramp;
|
|
53
|
+
secondary: Ramp;
|
|
54
|
+
success: Ramp;
|
|
55
|
+
danger: Ramp;
|
|
56
|
+
warning: Ramp;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Translucent neutral and accent, already composited over the background.
|
|
60
|
+
*
|
|
61
|
+
* The web gets these as real alpha. React Native's shadow and overlay
|
|
62
|
+
* handling is inconsistent enough across platforms that a pre-composited
|
|
63
|
+
* opaque colour is the predictable choice, which is what `alphaScale` returns.
|
|
64
|
+
*/
|
|
65
|
+
alpha: {
|
|
66
|
+
neutral: Ramp;
|
|
67
|
+
accent: Ramp;
|
|
68
|
+
};
|
|
69
|
+
radius: {
|
|
70
|
+
sm: number;
|
|
71
|
+
md: number;
|
|
72
|
+
lg: number;
|
|
73
|
+
xl: number;
|
|
74
|
+
full: number;
|
|
75
|
+
};
|
|
76
|
+
/** Multiples of 4, matching the Tailwind spacing the web components use. */
|
|
77
|
+
space: (steps: number) => number;
|
|
78
|
+
}
|
|
79
|
+
export declare function createNativeTheme(options?: NativeThemeOptions): NativeTheme;
|
|
80
|
+
export declare const darkTheme: NativeTheme;
|
|
81
|
+
export declare const lightTheme: NativeTheme;
|
|
82
|
+
//# sourceMappingURL=createNativeTheme.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createNativeTheme.d.ts","sourceRoot":"","sources":["../../src/theme/createNativeTheme.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,UAAU,EAKX,MAAM,gBAAgB,CAAC;AAExB;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,OAAO,CAAC;AAE9C,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,OAAO,UAAU,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC;IAC/D,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,OAAO,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACjE,UAAU,CAAC,EAAE,cAAc,CAAC;CAC7B;AAED,yEAAyE;AACzE,MAAM,MAAM,IAAI,GAAG,SAAS,MAAM,EAAE,CAAC;AAErC,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,cAAc,CAAC;IAC3B,sEAAsE;IACtE,KAAK,EAAE;QACL,EAAE,EAAE,MAAM,CAAC;QACX,OAAO,EAAE,MAAM,CAAC;QAChB,aAAa,EAAE,MAAM,CAAC;QACtB,YAAY,EAAE,MAAM,CAAC;QACrB,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,WAAW,EAAE,MAAM,CAAC;QACpB,SAAS,EAAE,MAAM,CAAC;QAClB,cAAc,EAAE,MAAM,CAAC;QACvB,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;QACf,WAAW,EAAE,MAAM,CAAC;QACpB,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,WAAW,EAAE,MAAM,CAAC;QACpB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,yEAAyE;IACzE,MAAM,EAAE;QACN,OAAO,EAAE,IAAI,CAAC;QACd,MAAM,EAAE,IAAI,CAAC;QACb,SAAS,EAAE,IAAI,CAAC;QAChB,OAAO,EAAE,IAAI,CAAC;QACd,MAAM,EAAE,IAAI,CAAC;QACb,OAAO,EAAE,IAAI,CAAC;KACf,CAAC;IACF;;;;;;OAMG;IACH,KAAK,EAAE;QACL,OAAO,EAAE,IAAI,CAAC;QACd,MAAM,EAAE,IAAI,CAAC;KACd,CAAC;IACF,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACzE,4EAA4E;IAC5E,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;CAClC;AAID,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,kBAAuB,GAAG,WAAW,CAkE/E;AAED,eAAO,MAAM,SAAS,aAAsB,CAAC;AAC7C,eAAO,MAAM,UAAU,aAA6C,CAAC"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { alphaScale, grytLightTokens, grytTokens, hueScale, hueScaleLight, neutralScale, neutralScaleLight, } from "@gryt/ui/theme";
|
|
2
|
+
const SPACE_UNIT = 4;
|
|
3
|
+
export function createNativeTheme(options = {}) {
|
|
4
|
+
const light = options.appearance === "light";
|
|
5
|
+
const color = {
|
|
6
|
+
...grytTokens.color,
|
|
7
|
+
...(light
|
|
8
|
+
? {
|
|
9
|
+
bg: grytLightTokens.bg,
|
|
10
|
+
surface: grytLightTokens.surface,
|
|
11
|
+
surfaceRaised: grytLightTokens.surfaceRaised,
|
|
12
|
+
border: grytLightTokens.border,
|
|
13
|
+
muted: grytLightTokens.muted,
|
|
14
|
+
text: grytLightTokens.text,
|
|
15
|
+
}
|
|
16
|
+
: {}),
|
|
17
|
+
...options.color,
|
|
18
|
+
};
|
|
19
|
+
const neutral = light
|
|
20
|
+
? neutralScaleLight({
|
|
21
|
+
bg: color.bg,
|
|
22
|
+
surface: color.surface,
|
|
23
|
+
surfaceRaised: color.surfaceRaised,
|
|
24
|
+
border: color.border,
|
|
25
|
+
muted: color.muted,
|
|
26
|
+
text: color.text,
|
|
27
|
+
})
|
|
28
|
+
: neutralScale({
|
|
29
|
+
bg: color.bg,
|
|
30
|
+
surface: color.surface,
|
|
31
|
+
surfaceRaised: color.surfaceRaised,
|
|
32
|
+
border: color.border,
|
|
33
|
+
muted: color.muted,
|
|
34
|
+
text: color.text,
|
|
35
|
+
});
|
|
36
|
+
const ramp = (solid, solidHover) => light ? hueScaleLight(solid) : hueScale(solid, solidHover);
|
|
37
|
+
const scales = {
|
|
38
|
+
neutral,
|
|
39
|
+
accent: ramp(color.accent, color.accentLight),
|
|
40
|
+
secondary: ramp(color.secondary, color.secondaryLight),
|
|
41
|
+
success: ramp(color.success, color.success),
|
|
42
|
+
danger: ramp(color.danger, color.dangerLight),
|
|
43
|
+
warning: ramp(color.warning, color.warning),
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
appearance: light ? "light" : "dark",
|
|
47
|
+
color: {
|
|
48
|
+
...color,
|
|
49
|
+
// `.light` never set a hover fill on the web either, and a neutral
|
|
50
|
+
// component hovering to the dark slate on a white panel was the bug that
|
|
51
|
+
// came from it. Step 4 is "component background, hovered", so take it
|
|
52
|
+
// from the ramp rather than writing a second literal.
|
|
53
|
+
surfaceHover: light ? scales.neutral[3] : color.surfaceHover,
|
|
54
|
+
},
|
|
55
|
+
scales,
|
|
56
|
+
alpha: {
|
|
57
|
+
neutral: alphaScale(scales.neutral, color.bg),
|
|
58
|
+
accent: alphaScale(scales.accent, color.bg),
|
|
59
|
+
},
|
|
60
|
+
radius: { ...grytTokens.radius, ...options.radius },
|
|
61
|
+
space: (steps) => steps * SPACE_UNIT,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export const darkTheme = createNativeTheme();
|
|
65
|
+
export const lightTheme = createNativeTheme({ appearance: "light" });
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { createNativeTheme, darkTheme, lightTheme, type GrytAppearance, type NativeTheme, type NativeThemeOptions, type Ramp, } from "./createNativeTheme.js";
|
|
2
|
+
export { GrytThemeProvider, type GrytThemeProviderProps } from "./ThemeProvider.js";
|
|
3
|
+
export { ThemeContext } from "./themeContext.js";
|
|
4
|
+
export { toneRamp, type ComponentTone } from "./tones.js";
|
|
5
|
+
export { useTheme } from "./useTheme.js";
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/theme/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,SAAS,EACT,UAAU,EACV,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,IAAI,GACV,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,iBAAiB,EAAE,KAAK,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AACjF,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AACvD,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createNativeTheme, darkTheme, lightTheme, } from "./createNativeTheme.js";
|
|
2
|
+
export { GrytThemeProvider } from "./ThemeProvider.js";
|
|
3
|
+
export { ThemeContext } from "./themeContext.js";
|
|
4
|
+
export { toneRamp } from "./tones.js";
|
|
5
|
+
export { useTheme } from "./useTheme.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type NativeTheme } from "./createNativeTheme.js";
|
|
2
|
+
/**
|
|
3
|
+
* Defaults to dark with no provider mounted, matching `@gryt/ui`, which ships
|
|
4
|
+
* its dark tokens on `:root`. A component rendered outside a provider is
|
|
5
|
+
* therefore themed rather than blank — the failure mode stays "wrong palette"
|
|
6
|
+
* instead of "no colours at all".
|
|
7
|
+
*/
|
|
8
|
+
export declare const ThemeContext: import("react").Context<NativeTheme>;
|
|
9
|
+
//# sourceMappingURL=themeContext.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"themeContext.d.ts","sourceRoot":"","sources":["../../src/theme/themeContext.ts"],"names":[],"mappings":"AAEA,OAAO,EAAa,KAAK,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAElE;;;;;GAKG;AACH,eAAO,MAAM,YAAY,sCAAwC,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createContext } from "react";
|
|
2
|
+
import { darkTheme } from "./createNativeTheme.js";
|
|
3
|
+
/**
|
|
4
|
+
* Defaults to dark with no provider mounted, matching `@gryt/ui`, which ships
|
|
5
|
+
* its dark tokens on `:root`. A component rendered outside a provider is
|
|
6
|
+
* therefore themed rather than blank — the failure mode stays "wrong palette"
|
|
7
|
+
* instead of "no colours at all".
|
|
8
|
+
*/
|
|
9
|
+
export const ThemeContext = createContext(darkTheme);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { NativeTheme, Ramp } from "./createNativeTheme.js";
|
|
2
|
+
/**
|
|
3
|
+
* Roles to ramps.
|
|
4
|
+
*
|
|
5
|
+
* `@gryt/ui` names its component tones by role — primary, secondary, neutral,
|
|
6
|
+
* danger — while the ramps are named by hue. On the web the mapping is buried in
|
|
7
|
+
* Tailwind classes, one component at a time: Button says
|
|
8
|
+
* `primary: "bg-gryt-accent"`, Checkbox says the same thing again.
|
|
9
|
+
*
|
|
10
|
+
* Written once here, because four copies of "primary means accent" is how one of
|
|
11
|
+
* them eventually stops meaning it.
|
|
12
|
+
*/
|
|
13
|
+
export type ComponentTone = "primary" | "secondary" | "neutral" | "danger" | "success" | "warning";
|
|
14
|
+
export declare function toneRamp(theme: NativeTheme, tone: ComponentTone): Ramp;
|
|
15
|
+
//# sourceMappingURL=tones.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tones.d.ts","sourceRoot":"","sources":["../../src/theme/tones.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAC;AAE7D;;;;;;;;;;GAUG;AACH,MAAM,MAAM,aAAa,GACrB,SAAS,GACT,WAAW,GACX,SAAS,GACT,QAAQ,GACR,SAAS,GACT,SAAS,CAAC;AAWd,wBAAgB,QAAQ,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,aAAa,GAAG,IAAI,CAEtE"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { NativeTheme } from "./createNativeTheme.js";
|
|
2
|
+
/**
|
|
3
|
+
* The web reads tokens off the cascade, so a component never asks for a theme —
|
|
4
|
+
* it inherits one. React Native has no cascade, so components ask, and this is
|
|
5
|
+
* what they ask.
|
|
6
|
+
*/
|
|
7
|
+
export declare function useTheme(): NativeTheme;
|
|
8
|
+
//# sourceMappingURL=useTheme.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useTheme.d.ts","sourceRoot":"","sources":["../../src/theme/useTheme.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEvD;;;;GAIG;AACH,wBAAgB,QAAQ,IAAI,WAAW,CAEtC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { useContext } from "react";
|
|
2
|
+
import { ThemeContext } from "./themeContext.js";
|
|
3
|
+
/**
|
|
4
|
+
* The web reads tokens off the cascade, so a component never asks for a theme —
|
|
5
|
+
* it inherits one. React Native has no cascade, so components ask, and this is
|
|
6
|
+
* what they ask.
|
|
7
|
+
*/
|
|
8
|
+
export function useTheme() {
|
|
9
|
+
return useContext(ThemeContext);
|
|
10
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gryt/ui-native",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"description": "Gryt's design system on React Native. Same tokens as @gryt/ui, a different renderer.",
|
|
7
|
+
"homepage": "https://gryt.chat",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Gryt-chat/ui.git",
|
|
11
|
+
"directory": "packages/ui-native"
|
|
12
|
+
},
|
|
13
|
+
"sideEffects": false,
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc -p tsconfig.build.json && node scripts/add-esm-extensions.mjs",
|
|
29
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
30
|
+
"test": "vitest run",
|
|
31
|
+
"prepublishOnly": "bun run typecheck && bun run build"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"react": ">=18.3.0 || >=19.0.0",
|
|
35
|
+
"react-native": ">=0.76.0"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@gryt/ui": "workspace:*"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/react": "^19.0.0",
|
|
42
|
+
"react": "^19.0.0",
|
|
43
|
+
"react-native": "^0.76.0",
|
|
44
|
+
"vitest": "^3.0.0"
|
|
45
|
+
}
|
|
46
|
+
}
|