@streamplace/components 0.7.9 → 0.7.13
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/dist/assets/emoji-data.json +19371 -0
- package/dist/components/chat/chat-box.js +19 -2
- package/dist/components/chat/chat-message.js +12 -4
- package/dist/components/chat/chat.js +15 -4
- package/dist/components/chat/mod-view.js +15 -8
- package/dist/components/dashboard/chat-panel.js +38 -0
- package/dist/components/dashboard/header.js +80 -0
- package/dist/components/dashboard/index.js +14 -0
- package/dist/components/dashboard/information-widget.js +234 -0
- package/dist/components/dashboard/mod-actions.js +71 -0
- package/dist/components/dashboard/problems.js +74 -0
- package/dist/components/mobile-player/ui/viewer-context-menu.js +15 -6
- package/dist/components/ui/button.js +2 -2
- package/dist/components/ui/dropdown.js +20 -1
- package/dist/components/ui/index.js +2 -0
- package/dist/components/ui/info-box.js +31 -0
- package/dist/components/ui/info-row.js +23 -0
- package/dist/components/ui/toast.js +43 -0
- package/dist/index.js +3 -1
- package/dist/lib/theme/atoms.js +66 -45
- package/dist/lib/theme/tokens.js +285 -12
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/56540125 +0 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/67b1eb60 +0 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/7c275f90 +0 -0
- package/package.json +2 -2
- package/src/assets/emoji-data.json +19371 -0
- package/src/components/chat/chat-box.tsx +19 -1
- package/src/components/chat/chat-message.tsx +22 -14
- package/src/components/chat/chat.tsx +21 -6
- package/src/components/chat/mod-view.tsx +24 -6
- package/src/components/dashboard/chat-panel.tsx +80 -0
- package/src/components/dashboard/header.tsx +170 -0
- package/src/components/dashboard/index.tsx +5 -0
- package/src/components/dashboard/information-widget.tsx +526 -0
- package/src/components/dashboard/mod-actions.tsx +133 -0
- package/src/components/dashboard/problems.tsx +151 -0
- package/src/components/mobile-player/ui/viewer-context-menu.tsx +67 -38
- package/src/components/ui/button.tsx +2 -2
- package/src/components/ui/dropdown.tsx +38 -3
- package/src/components/ui/index.ts +2 -0
- package/src/components/ui/info-box.tsx +60 -0
- package/src/components/ui/info-row.tsx +48 -0
- package/src/components/ui/toast.tsx +110 -0
- package/src/index.tsx +3 -0
- package/src/lib/theme/atoms.ts +97 -43
- package/src/lib/theme/tokens.ts +285 -12
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { ExternalLink } from "lucide-react-native";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Linking, Pressable, Text, View } from "react-native";
|
|
4
|
+
import { useLivestreamStore } from "../../livestream-store";
|
|
5
|
+
import { LivestreamProblem } from "../../livestream-store/livestream-state";
|
|
6
|
+
import * as zero from "../../ui";
|
|
7
|
+
|
|
8
|
+
const { bg, r, borders, p, text, layout, gap } = zero;
|
|
9
|
+
|
|
10
|
+
const Problems = ({
|
|
11
|
+
probs,
|
|
12
|
+
onIgnore,
|
|
13
|
+
}: {
|
|
14
|
+
probs: LivestreamProblem[];
|
|
15
|
+
onIgnore: () => void;
|
|
16
|
+
}) => {
|
|
17
|
+
return (
|
|
18
|
+
<View style={[gap.all[3]]}>
|
|
19
|
+
<View>
|
|
20
|
+
<Text style={[text.white, { fontSize: 24, fontWeight: "bold" }]}>
|
|
21
|
+
Optimize Your Stream
|
|
22
|
+
</Text>
|
|
23
|
+
<Text style={[text.gray[300]]}>
|
|
24
|
+
We've found a few things that could improve your stream's reliability.
|
|
25
|
+
</Text>
|
|
26
|
+
</View>
|
|
27
|
+
{probs.map((p) => (
|
|
28
|
+
<View key={p.message}>
|
|
29
|
+
<View
|
|
30
|
+
style={[
|
|
31
|
+
gap.all[2],
|
|
32
|
+
layout.flex.row,
|
|
33
|
+
layout.flex.alignCenter,
|
|
34
|
+
{ gap: 8, alignItems: "flex-start" },
|
|
35
|
+
]}
|
|
36
|
+
>
|
|
37
|
+
<Text
|
|
38
|
+
style={[
|
|
39
|
+
r.sm,
|
|
40
|
+
p[2],
|
|
41
|
+
{
|
|
42
|
+
width: 82,
|
|
43
|
+
textAlign: "center",
|
|
44
|
+
backgroundColor:
|
|
45
|
+
p.severity === "error"
|
|
46
|
+
? "#7f1d1d"
|
|
47
|
+
: p.severity === "warning"
|
|
48
|
+
? "#7c2d12"
|
|
49
|
+
: "#1e3a8a",
|
|
50
|
+
color: "white",
|
|
51
|
+
fontSize: 12,
|
|
52
|
+
},
|
|
53
|
+
]}
|
|
54
|
+
>
|
|
55
|
+
{p.severity}
|
|
56
|
+
</Text>
|
|
57
|
+
<View style={[{ flex: 1 }, gap.all[1]]}>
|
|
58
|
+
<Text style={[text.white, { fontWeight: "600" }]}>{p.code}</Text>
|
|
59
|
+
<Text style={[text.gray[400], { fontSize: 14 }]}>
|
|
60
|
+
{p.message}
|
|
61
|
+
</Text>
|
|
62
|
+
{p.link && (
|
|
63
|
+
<Pressable onPress={() => p.link && Linking.openURL(p.link)}>
|
|
64
|
+
<View
|
|
65
|
+
style={[
|
|
66
|
+
layout.flex.row,
|
|
67
|
+
layout.flex.alignCenter,
|
|
68
|
+
gap.all[2],
|
|
69
|
+
]}
|
|
70
|
+
>
|
|
71
|
+
<Text style={[{ color: "#3b82f6", fontSize: 14 }]}>
|
|
72
|
+
Learn More
|
|
73
|
+
</Text>
|
|
74
|
+
<ExternalLink size={12} color="#3b82f6" />
|
|
75
|
+
</View>
|
|
76
|
+
</Pressable>
|
|
77
|
+
)}
|
|
78
|
+
</View>
|
|
79
|
+
</View>
|
|
80
|
+
</View>
|
|
81
|
+
))}
|
|
82
|
+
|
|
83
|
+
<Pressable
|
|
84
|
+
onPress={onIgnore}
|
|
85
|
+
style={[
|
|
86
|
+
bg.blue[600],
|
|
87
|
+
r.md,
|
|
88
|
+
p[3],
|
|
89
|
+
layout.flex.center,
|
|
90
|
+
{ marginTop: 16 },
|
|
91
|
+
]}
|
|
92
|
+
>
|
|
93
|
+
<Text style={[text.white, { fontWeight: "600" }]}>Ignore</Text>
|
|
94
|
+
</Pressable>
|
|
95
|
+
</View>
|
|
96
|
+
);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const ProblemsWrapper = ({
|
|
100
|
+
children,
|
|
101
|
+
}: {
|
|
102
|
+
children: React.ReactElement;
|
|
103
|
+
}) => {
|
|
104
|
+
const problems = useLivestreamStore((x) => x.problems);
|
|
105
|
+
const [dismiss, setDismiss] = useState(false);
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<View
|
|
109
|
+
style={[
|
|
110
|
+
{ position: "relative", flex: 1 },
|
|
111
|
+
layout.flex.center,
|
|
112
|
+
{ flexBasis: 0 },
|
|
113
|
+
]}
|
|
114
|
+
>
|
|
115
|
+
{children}
|
|
116
|
+
{problems.length > 0 && !dismiss && (
|
|
117
|
+
<View
|
|
118
|
+
style={[
|
|
119
|
+
{
|
|
120
|
+
position: "absolute",
|
|
121
|
+
top: 0,
|
|
122
|
+
left: 0,
|
|
123
|
+
right: 0,
|
|
124
|
+
bottom: 0,
|
|
125
|
+
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
|
126
|
+
zIndex: 100,
|
|
127
|
+
},
|
|
128
|
+
layout.flex.center,
|
|
129
|
+
{ justifyContent: "flex-start" },
|
|
130
|
+
p[8],
|
|
131
|
+
]}
|
|
132
|
+
>
|
|
133
|
+
<View
|
|
134
|
+
style={[
|
|
135
|
+
bg.gray[900],
|
|
136
|
+
borders.color.gray[700],
|
|
137
|
+
borders.width.thin,
|
|
138
|
+
r.lg,
|
|
139
|
+
p[4],
|
|
140
|
+
{ maxWidth: 700, width: "100%" },
|
|
141
|
+
]}
|
|
142
|
+
>
|
|
143
|
+
<Problems probs={problems} onIgnore={() => setDismiss(true)} />
|
|
144
|
+
</View>
|
|
145
|
+
</View>
|
|
146
|
+
)}
|
|
147
|
+
</View>
|
|
148
|
+
);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export default Problems;
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { useRootContext } from "@rn-primitives/dropdown-menu";
|
|
2
2
|
import { Settings } from "lucide-react-native";
|
|
3
|
+
import { Platform, View } from "react-native";
|
|
3
4
|
import { colors } from "../../../lib/theme";
|
|
4
5
|
import { useLivestreamStore } from "../../../livestream-store";
|
|
5
6
|
import { PlayerProtocol, usePlayerStore } from "../../../player-store/";
|
|
6
7
|
import {
|
|
7
8
|
DropdownMenu,
|
|
8
9
|
DropdownMenuCheckboxItem,
|
|
10
|
+
DropdownMenuContentWithoutPortal,
|
|
9
11
|
DropdownMenuGroup,
|
|
10
12
|
DropdownMenuInfo,
|
|
11
13
|
DropdownMenuItem,
|
|
14
|
+
DropdownMenuPortal,
|
|
12
15
|
DropdownMenuRadioGroup,
|
|
13
16
|
DropdownMenuRadioItem,
|
|
14
17
|
DropdownMenuTrigger,
|
|
@@ -16,7 +19,11 @@ import {
|
|
|
16
19
|
Text,
|
|
17
20
|
} from "../../ui";
|
|
18
21
|
|
|
19
|
-
export function ContextMenu(
|
|
22
|
+
export function ContextMenu({
|
|
23
|
+
dropdownPortalContainer,
|
|
24
|
+
}: {
|
|
25
|
+
dropdownPortalContainer?: any;
|
|
26
|
+
}) {
|
|
20
27
|
const quality = usePlayerStore((x) => x.selectedRendition);
|
|
21
28
|
const setQuality = usePlayerStore((x) => x.setSelectedRendition);
|
|
22
29
|
const qualities = useLivestreamStore((x) => x.renditions);
|
|
@@ -27,58 +34,80 @@ export function ContextMenu() {
|
|
|
27
34
|
const debugInfo = usePlayerStore((x) => x.showDebugInfo);
|
|
28
35
|
const setShowDebugInfo = usePlayerStore((x) => x.setShowDebugInfo);
|
|
29
36
|
|
|
37
|
+
const livestream = useLivestreamStore((x) => x.livestream);
|
|
38
|
+
const setReportModalOpen = usePlayerStore((x) => x.setReportModalOpen);
|
|
39
|
+
const setReportSubject = usePlayerStore((x) => x.setReportSubject);
|
|
40
|
+
|
|
30
41
|
const lowLatency = protocol === "webrtc";
|
|
31
42
|
const setLowLatency = (value: boolean) => {
|
|
32
43
|
setProtocol(value ? PlayerProtocol.WEBRTC : PlayerProtocol.HLS);
|
|
33
44
|
};
|
|
34
45
|
|
|
46
|
+
// are we on mobile? then do dropdowns
|
|
47
|
+
const isMobile = Platform.OS === "ios" || Platform.OS === "android";
|
|
48
|
+
|
|
49
|
+
// dummy portal for mobile
|
|
50
|
+
const Portal = isMobile ? View : DropdownMenuPortal;
|
|
51
|
+
|
|
52
|
+
// render the responsive version on mobile as we can't fullscreen there
|
|
53
|
+
const DropdownMenuContent = isMobile
|
|
54
|
+
? ResponsiveDropdownMenuContent
|
|
55
|
+
: DropdownMenuContentWithoutPortal;
|
|
56
|
+
|
|
35
57
|
return (
|
|
36
58
|
<DropdownMenu>
|
|
37
59
|
<DropdownMenuTrigger>
|
|
38
60
|
<Settings color={colors.gray[200]} />
|
|
39
61
|
</DropdownMenuTrigger>
|
|
40
|
-
<
|
|
41
|
-
<
|
|
42
|
-
<
|
|
43
|
-
<
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
{qualities.map((r) => (
|
|
47
|
-
<DropdownMenuRadioItem value={r.name}>
|
|
48
|
-
<Text>{r.name}</Text>
|
|
62
|
+
<Portal container={dropdownPortalContainer}>
|
|
63
|
+
<DropdownMenuContent side="top" align="end">
|
|
64
|
+
<DropdownMenuGroup title="Resolution">
|
|
65
|
+
<DropdownMenuRadioGroup value={quality} onValueChange={setQuality}>
|
|
66
|
+
<DropdownMenuRadioItem value="source">
|
|
67
|
+
<Text>Source (Original Quality)</Text>
|
|
49
68
|
</DropdownMenuRadioItem>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
>
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
>
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
{qualities.map((r) => (
|
|
70
|
+
<DropdownMenuRadioItem value={r.name}>
|
|
71
|
+
<Text>{r.name}</Text>
|
|
72
|
+
</DropdownMenuRadioItem>
|
|
73
|
+
))}
|
|
74
|
+
</DropdownMenuRadioGroup>
|
|
75
|
+
</DropdownMenuGroup>
|
|
76
|
+
<DropdownMenuGroup title="Advanced">
|
|
77
|
+
<DropdownMenuCheckboxItem
|
|
78
|
+
checked={lowLatency}
|
|
79
|
+
onCheckedChange={() => setLowLatency(!lowLatency)}
|
|
80
|
+
>
|
|
81
|
+
<Text>Low Latency</Text>
|
|
82
|
+
</DropdownMenuCheckboxItem>
|
|
83
|
+
</DropdownMenuGroup>
|
|
84
|
+
<DropdownMenuInfo description="Reduces the delay between video and chat for a more real-time experience." />
|
|
85
|
+
<DropdownMenuGroup>
|
|
86
|
+
<DropdownMenuCheckboxItem
|
|
87
|
+
checked={debugInfo}
|
|
88
|
+
onCheckedChange={() => setShowDebugInfo(!debugInfo)}
|
|
89
|
+
>
|
|
90
|
+
<Text>Show Debug Info</Text>
|
|
91
|
+
</DropdownMenuCheckboxItem>
|
|
92
|
+
</DropdownMenuGroup>
|
|
93
|
+
<DropdownMenuGroup title="Report">
|
|
94
|
+
<ReportButton
|
|
95
|
+
livestream={livestream}
|
|
96
|
+
setReportModalOpen={setReportModalOpen}
|
|
97
|
+
setReportSubject={setReportSubject}
|
|
98
|
+
/>
|
|
99
|
+
</DropdownMenuGroup>
|
|
100
|
+
</DropdownMenuContent>
|
|
101
|
+
</Portal>
|
|
74
102
|
</DropdownMenu>
|
|
75
103
|
);
|
|
76
104
|
}
|
|
77
105
|
|
|
78
|
-
export function ReportButton(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
106
|
+
export function ReportButton({
|
|
107
|
+
livestream,
|
|
108
|
+
setReportModalOpen,
|
|
109
|
+
setReportSubject,
|
|
110
|
+
}) {
|
|
82
111
|
const { onOpenChange } = useRootContext();
|
|
83
112
|
return (
|
|
84
113
|
<DropdownMenuItem
|
|
@@ -231,8 +231,8 @@ function createStyles(theme: any) {
|
|
|
231
231
|
},
|
|
232
232
|
|
|
233
233
|
pillButton: {
|
|
234
|
-
paddingHorizontal: theme.spacing[
|
|
235
|
-
paddingVertical: theme.spacing[
|
|
234
|
+
paddingHorizontal: theme.spacing[2],
|
|
235
|
+
paddingVertical: theme.spacing[1],
|
|
236
236
|
borderRadius: tokens.borderRadius.full,
|
|
237
237
|
minHeight: tokens.touchTargets.minimum / 2,
|
|
238
238
|
},
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
ChevronUp,
|
|
9
9
|
Circle,
|
|
10
10
|
} from "lucide-react-native";
|
|
11
|
-
import { forwardRef, ReactNode, useMemo, useRef } from "react";
|
|
11
|
+
import React, { forwardRef, ReactNode, useMemo, useRef } from "react";
|
|
12
12
|
import {
|
|
13
13
|
Platform,
|
|
14
14
|
Pressable,
|
|
@@ -200,6 +200,43 @@ export const DropdownMenuContent = forwardRef<
|
|
|
200
200
|
);
|
|
201
201
|
});
|
|
202
202
|
|
|
203
|
+
export const DropdownMenuContentWithoutPortal = forwardRef<
|
|
204
|
+
any,
|
|
205
|
+
DropdownMenuPrimitive.ContentProps & {
|
|
206
|
+
overlayStyle?: any;
|
|
207
|
+
}
|
|
208
|
+
>(({ overlayStyle, ...props }, ref) => {
|
|
209
|
+
return (
|
|
210
|
+
<DropdownMenuPrimitive.Overlay
|
|
211
|
+
style={[
|
|
212
|
+
Platform.OS !== "web" ? StyleSheet.absoluteFill : undefined,
|
|
213
|
+
overlayStyle,
|
|
214
|
+
]}
|
|
215
|
+
>
|
|
216
|
+
<DropdownMenuPrimitive.Content
|
|
217
|
+
ref={ref}
|
|
218
|
+
style={
|
|
219
|
+
[
|
|
220
|
+
{ zIndex: 999999 },
|
|
221
|
+
a.sizes.minWidth[32],
|
|
222
|
+
a.sizes.maxWidth[64],
|
|
223
|
+
a.overflow.hidden,
|
|
224
|
+
a.radius.all.md,
|
|
225
|
+
a.borders.width.thin,
|
|
226
|
+
a.borders.color.gray[800],
|
|
227
|
+
bg.gray[950],
|
|
228
|
+
p[2],
|
|
229
|
+
a.shadows.md,
|
|
230
|
+
] as any
|
|
231
|
+
}
|
|
232
|
+
{...props}
|
|
233
|
+
/>
|
|
234
|
+
</DropdownMenuPrimitive.Overlay>
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
/// Responsive Dropdown Menu Content. On mobile this will render a *bottom sheet* that is **portaled to the root of the app**.
|
|
239
|
+
/// Prefer passing scoped content in as **otherwise it may crash the app**.
|
|
203
240
|
export const ResponsiveDropdownMenuContent = forwardRef<any, any>(
|
|
204
241
|
({ children, ...props }, ref) => {
|
|
205
242
|
const { width } = useWindowDimensions();
|
|
@@ -222,8 +259,6 @@ export const ResponsiveDropdownMenuContent = forwardRef<any, any>(
|
|
|
222
259
|
},
|
|
223
260
|
);
|
|
224
261
|
|
|
225
|
-
import React from "react";
|
|
226
|
-
|
|
227
262
|
export const DropdownMenuItem = forwardRef<
|
|
228
263
|
any,
|
|
229
264
|
DropdownMenuPrimitive.ItemProps & { inset?: boolean; disabled?: boolean }
|
|
@@ -9,6 +9,8 @@ export * from "./button";
|
|
|
9
9
|
export * from "./dialog";
|
|
10
10
|
export * from "./dropdown";
|
|
11
11
|
export * from "./icons";
|
|
12
|
+
export * from "./info-box";
|
|
13
|
+
export * from "./info-row";
|
|
12
14
|
export * from "./input";
|
|
13
15
|
export * from "./loader";
|
|
14
16
|
export * from "./resizeable";
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Text, View } from "react-native";
|
|
2
|
+
import * as zero from "../../ui";
|
|
3
|
+
|
|
4
|
+
const { bg, r, p, text, layout, gap, flex } = zero;
|
|
5
|
+
|
|
6
|
+
interface InfoBoxProps {
|
|
7
|
+
icon: any;
|
|
8
|
+
label: string;
|
|
9
|
+
value: string;
|
|
10
|
+
status?: "good" | "warning" | "error" | "neutral";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function InfoBox({
|
|
14
|
+
icon: Icon,
|
|
15
|
+
label,
|
|
16
|
+
value,
|
|
17
|
+
status = "neutral",
|
|
18
|
+
}: InfoBoxProps) {
|
|
19
|
+
const statusColors = {
|
|
20
|
+
good: text.green[400],
|
|
21
|
+
warning: text.yellow[400],
|
|
22
|
+
error: text.red[400],
|
|
23
|
+
neutral: text.white,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const statusColor = statusColors[status];
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<View
|
|
30
|
+
style={[
|
|
31
|
+
flex.values[1],
|
|
32
|
+
layout.flex.column,
|
|
33
|
+
layout.flex.spaceBetween,
|
|
34
|
+
layout.flex.alignCenter,
|
|
35
|
+
bg.neutral[700],
|
|
36
|
+
r.sm,
|
|
37
|
+
p[2],
|
|
38
|
+
]}
|
|
39
|
+
>
|
|
40
|
+
<View
|
|
41
|
+
style={[
|
|
42
|
+
layout.flex.row,
|
|
43
|
+
layout.flex.spaceBetween,
|
|
44
|
+
gap.all[3],
|
|
45
|
+
zero.w.percent[100],
|
|
46
|
+
]}
|
|
47
|
+
>
|
|
48
|
+
<Text style={[text.gray[100], { fontSize: 13, fontWeight: "500" }]}>
|
|
49
|
+
{label}
|
|
50
|
+
</Text>
|
|
51
|
+
<Icon size={16} color="#9ca3af" />
|
|
52
|
+
</View>
|
|
53
|
+
<View style={[layout.flex.align.end, zero.w.percent[100]]}>
|
|
54
|
+
<Text style={[statusColor, { fontSize: 26, fontWeight: "600" }]}>
|
|
55
|
+
{value}
|
|
56
|
+
</Text>
|
|
57
|
+
</View>
|
|
58
|
+
</View>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Text, View } from "react-native";
|
|
2
|
+
import * as zero from "../../ui";
|
|
3
|
+
|
|
4
|
+
const { text, layout, py, gap } = zero;
|
|
5
|
+
|
|
6
|
+
interface InfoRowProps {
|
|
7
|
+
icon: any;
|
|
8
|
+
label: string;
|
|
9
|
+
value: string;
|
|
10
|
+
status?: "good" | "warning" | "error" | "neutral";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function InfoRow({
|
|
14
|
+
icon: Icon,
|
|
15
|
+
label,
|
|
16
|
+
value,
|
|
17
|
+
status = "neutral",
|
|
18
|
+
}: InfoRowProps) {
|
|
19
|
+
const statusColors = {
|
|
20
|
+
good: text.green[400],
|
|
21
|
+
warning: text.yellow[400],
|
|
22
|
+
error: text.red[400],
|
|
23
|
+
neutral: text.white,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const statusColor = statusColors[status];
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<View
|
|
30
|
+
style={[
|
|
31
|
+
layout.flex.row,
|
|
32
|
+
layout.flex.spaceBetween,
|
|
33
|
+
layout.flex.alignCenter,
|
|
34
|
+
py[2],
|
|
35
|
+
]}
|
|
36
|
+
>
|
|
37
|
+
<View style={[layout.flex.row, layout.flex.alignCenter, gap.all[3]]}>
|
|
38
|
+
<Icon size={16} color="#9ca3af" />
|
|
39
|
+
<Text style={[text.gray[300], { fontSize: 13, fontWeight: "500" }]}>
|
|
40
|
+
{label}
|
|
41
|
+
</Text>
|
|
42
|
+
</View>
|
|
43
|
+
<Text style={[statusColor, { fontSize: 13, fontWeight: "600" }]}>
|
|
44
|
+
{value}
|
|
45
|
+
</Text>
|
|
46
|
+
</View>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -13,6 +13,116 @@ import {
|
|
|
13
13
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
14
14
|
import { useTheme } from "../../lib/theme/theme";
|
|
15
15
|
|
|
16
|
+
import { useCallback } from "react";
|
|
17
|
+
|
|
18
|
+
type ToastController = {
|
|
19
|
+
show: (
|
|
20
|
+
title: string,
|
|
21
|
+
description?: string,
|
|
22
|
+
options?: {
|
|
23
|
+
duration?: number;
|
|
24
|
+
actionLabel?: string;
|
|
25
|
+
onAction?: () => void;
|
|
26
|
+
},
|
|
27
|
+
) => void;
|
|
28
|
+
hide: () => void;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type UseToastReturn = {
|
|
32
|
+
open: boolean;
|
|
33
|
+
title: string;
|
|
34
|
+
description?: string;
|
|
35
|
+
actionLabel?: string;
|
|
36
|
+
onAction?: () => void;
|
|
37
|
+
duration?: number;
|
|
38
|
+
setOpen: (open: boolean) => void;
|
|
39
|
+
setTitle: (title: string) => void;
|
|
40
|
+
setDescription: (description: string) => void;
|
|
41
|
+
setActionLabel: (label: string) => void;
|
|
42
|
+
setOnAction: (cb?: () => void) => void;
|
|
43
|
+
setDuration: (duration: number) => void;
|
|
44
|
+
toastController: ToastController;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* useToast - a hook to manage Toast state and provide a toastController.
|
|
49
|
+
* Returns a ready-to-render ToastComponent.
|
|
50
|
+
*/
|
|
51
|
+
export function useToast(
|
|
52
|
+
initial: {
|
|
53
|
+
title?: string;
|
|
54
|
+
description?: string;
|
|
55
|
+
duration?: number;
|
|
56
|
+
actionLabel?: string;
|
|
57
|
+
onAction?: () => void;
|
|
58
|
+
} = {},
|
|
59
|
+
) {
|
|
60
|
+
const [open, setOpen] = useState(false);
|
|
61
|
+
const [title, setTitle] = useState(initial.title ?? "");
|
|
62
|
+
const [description, setDescription] = useState(initial.description ?? "");
|
|
63
|
+
const [duration, setDuration] = useState(initial.duration ?? 3);
|
|
64
|
+
const [actionLabel, setActionLabel] = useState(
|
|
65
|
+
initial.actionLabel ?? "Action",
|
|
66
|
+
);
|
|
67
|
+
const [onAction, setOnAction] = useState<(() => void) | undefined>(
|
|
68
|
+
initial.onAction,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const show = useCallback(
|
|
72
|
+
(
|
|
73
|
+
toastTitle: string,
|
|
74
|
+
toastDescription?: string,
|
|
75
|
+
options?: {
|
|
76
|
+
duration?: number;
|
|
77
|
+
actionLabel?: string;
|
|
78
|
+
onAction?: () => void;
|
|
79
|
+
},
|
|
80
|
+
) => {
|
|
81
|
+
setTitle(toastTitle);
|
|
82
|
+
setDescription(toastDescription ?? "");
|
|
83
|
+
setDuration(options?.duration ?? 3);
|
|
84
|
+
setActionLabel(options?.actionLabel ?? "Action");
|
|
85
|
+
setOnAction(options?.onAction);
|
|
86
|
+
setOpen(true);
|
|
87
|
+
},
|
|
88
|
+
[],
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const hide = useCallback(() => {
|
|
92
|
+
setOpen(false);
|
|
93
|
+
}, []);
|
|
94
|
+
|
|
95
|
+
// Ready-to-render Toast component
|
|
96
|
+
const ToastComponent = (
|
|
97
|
+
<Toast
|
|
98
|
+
open={open}
|
|
99
|
+
onOpenChange={setOpen}
|
|
100
|
+
title={title}
|
|
101
|
+
description={description}
|
|
102
|
+
actionLabel={actionLabel}
|
|
103
|
+
onAction={onAction}
|
|
104
|
+
duration={duration}
|
|
105
|
+
/>
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
open,
|
|
110
|
+
title,
|
|
111
|
+
description,
|
|
112
|
+
actionLabel,
|
|
113
|
+
onAction,
|
|
114
|
+
duration,
|
|
115
|
+
setOpen,
|
|
116
|
+
setTitle,
|
|
117
|
+
setDescription,
|
|
118
|
+
setActionLabel,
|
|
119
|
+
setOnAction,
|
|
120
|
+
setDuration,
|
|
121
|
+
toastController: { show, hide },
|
|
122
|
+
ToastComponent,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
16
126
|
type ToastProps = {
|
|
17
127
|
open: boolean;
|
|
18
128
|
onOpenChange: (open: boolean) => void;
|