@xpufx/paseo-twofado 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/LICENSE +21 -0
- package/README.md +62 -0
- package/client/approvals.tsx +1525 -0
- package/client/error-boundary.tsx +28 -0
- package/client/vendor/paseo-plugin-helper/command-center.ts +43 -0
- package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +493 -0
- package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +250 -0
- package/client/vendor/paseo-plugin-helper/components/Badge.tsx +156 -0
- package/client/vendor/paseo-plugin-helper/components/Button.tsx +178 -0
- package/client/vendor/paseo-plugin-helper/components/Card.tsx +225 -0
- package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +196 -0
- package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +277 -0
- package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +172 -0
- package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +180 -0
- package/client/vendor/paseo-plugin-helper/components/DataTable.tsx +200 -0
- package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +97 -0
- package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +70 -0
- package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +73 -0
- package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +446 -0
- package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +247 -0
- package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +117 -0
- package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +53 -0
- package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +118 -0
- package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/components/Select.tsx +215 -0
- package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +319 -0
- package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +150 -0
- package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +163 -0
- package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +157 -0
- package/client/vendor/paseo-plugin-helper/components/index.ts +25 -0
- package/client/vendor/paseo-plugin-helper/custom-pills.tsx +224 -0
- package/client/vendor/paseo-plugin-helper/forge-icon.tsx +79 -0
- package/client/vendor/paseo-plugin-helper/host.ts +277 -0
- package/client/vendor/paseo-plugin-helper/icon.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/index.ts +28 -0
- package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +49 -0
- package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +103 -0
- package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +65 -0
- package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +378 -0
- package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +49 -0
- package/client/vendor/paseo-plugin-helper/layout/Row.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/layout/index.ts +7 -0
- package/client/vendor/paseo-plugin-helper/panel.tsx +81 -0
- package/client/vendor/paseo-plugin-helper/pill.tsx +884 -0
- package/client/vendor/paseo-plugin-helper/query-refresh.ts +79 -0
- package/client/vendor/paseo-plugin-helper/query.ts +66 -0
- package/client/vendor/paseo-plugin-helper/settings-screen.tsx +372 -0
- package/client/vendor/paseo-plugin-helper/settings.ts +181 -0
- package/client/vendor/paseo-plugin-helper/shared-settings.ts +46 -0
- package/client/vendor/paseo-plugin-helper/snapshot.ts +68 -0
- package/client/vendor/paseo-plugin-helper/surface.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/theme/color-utils.ts +118 -0
- package/client/vendor/paseo-plugin-helper/theme/flair.ts +76 -0
- package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +121 -0
- package/client/vendor/paseo-plugin-helper/theme/index.ts +7 -0
- package/client/vendor/paseo-plugin-helper/theme/provider.tsx +214 -0
- package/client/vendor/paseo-plugin-helper/theme/responsive.ts +213 -0
- package/client/vendor/paseo-plugin-helper/theme/tokens.ts +161 -0
- package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +57 -0
- package/client/vendor/paseo-plugin-helper/utils/clipboard.ts +149 -0
- package/client/vendor/paseo-plugin-helper/utils/haptics.ts +34 -0
- package/package.json +36 -0
- package/paseo-plugin.json +6 -0
- package/server/twofado.ts +536 -0
- package/server/vendor/paseo-plugin-helper/agent.ts +85 -0
- package/server/vendor/paseo-plugin-helper/custom-pills.ts +344 -0
- package/server/vendor/paseo-plugin-helper/index.ts +18 -0
- package/server/vendor/paseo-plugin-helper/jsonc.ts +78 -0
- package/server/vendor/paseo-plugin-helper/logger.ts +210 -0
- package/server/vendor/paseo-plugin-helper/mcp-config.ts +367 -0
- package/server/vendor/paseo-plugin-helper/mcp-injection.ts +85 -0
- package/server/vendor/paseo-plugin-helper/network.ts +91 -0
- package/server/vendor/paseo-plugin-helper/plugins.ts +160 -0
- package/server/vendor/paseo-plugin-helper/process.ts +186 -0
- package/server/vendor/paseo-plugin-helper/redact.ts +86 -0
- package/server/vendor/paseo-plugin-helper/rpc-guard.ts +77 -0
- package/server/vendor/paseo-plugin-helper/settings.ts +97 -0
- package/server/vendor/paseo-plugin-helper/shared-settings.ts +243 -0
- package/server/vendor/paseo-plugin-helper/storage.ts +244 -0
- package/server/vendor/paseo-plugin-helper/system.ts +128 -0
- package/server/vendor/paseo-plugin-helper/task.ts +116 -0
- package/server/vendor/paseo-plugin-helper/version.ts +153 -0
- package/server/vendor/paseo-plugin-helper/workspace-beacon.ts +418 -0
- package/shared/approval.ts +254 -0
- package/shared/vendor/paseo-plugin-helper/async.ts +35 -0
- package/shared/vendor/paseo-plugin-helper/custom-pills.ts +169 -0
- package/shared/vendor/paseo-plugin-helper/forge.ts +110 -0
- package/shared/vendor/paseo-plugin-helper/formatters.ts +271 -0
- package/shared/vendor/paseo-plugin-helper/highlight.ts +184 -0
- package/shared/vendor/paseo-plugin-helper/index.ts +10 -0
- package/shared/vendor/paseo-plugin-helper/rpc.ts +72 -0
- package/shared/vendor/paseo-plugin-helper/settings.ts +138 -0
- package/shared/vendor/paseo-plugin-helper/suite-settings.ts +17 -0
- package/shared/vendor/paseo-plugin-helper/suppressed.ts +31 -0
- package/shared/vendor/paseo-plugin-helper/types.ts +36 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Platform,
|
|
4
|
+
Pressable,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
Text,
|
|
7
|
+
View,
|
|
8
|
+
type StyleProp,
|
|
9
|
+
type TextStyle,
|
|
10
|
+
type ViewStyle,
|
|
11
|
+
} from "react-native";
|
|
12
|
+
import { getClientHost } from "../host";
|
|
13
|
+
import { usePluginTheme } from "../theme/provider";
|
|
14
|
+
import { copyToClipboard } from "../utils/clipboard";
|
|
15
|
+
|
|
16
|
+
export interface CommandBoxProps {
|
|
17
|
+
/** argv array — program is rendered bold, args muted */
|
|
18
|
+
argv?: string[];
|
|
19
|
+
/** Optional override to display a pre-joined string instead of argv */
|
|
20
|
+
command?: string;
|
|
21
|
+
style?: StyleProp<ViewStyle>;
|
|
22
|
+
textStyle?: StyleProp<TextStyle>;
|
|
23
|
+
/** Accessibility label override for the copy button */
|
|
24
|
+
copyLabel?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function formatCommandLine(argv: string[]): string {
|
|
28
|
+
return argv.map((arg) => (arg.includes(" ") ? JSON.stringify(arg) : arg)).join(" ");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function CommandBox({
|
|
32
|
+
argv = [],
|
|
33
|
+
command,
|
|
34
|
+
style,
|
|
35
|
+
textStyle,
|
|
36
|
+
copyLabel = "Copy command",
|
|
37
|
+
}: CommandBoxProps): React.ReactElement | null {
|
|
38
|
+
const { colors, resolveRadius } = usePluginTheme();
|
|
39
|
+
const { Icon, useToast } = getClientHost();
|
|
40
|
+
const toast = useToast();
|
|
41
|
+
const [copied, setCopied] = useState(false);
|
|
42
|
+
|
|
43
|
+
const fullCommand = command ?? formatCommandLine(argv);
|
|
44
|
+
const [prog, ...rest] = argv;
|
|
45
|
+
|
|
46
|
+
const handleCopy = async () => {
|
|
47
|
+
if (!fullCommand) return;
|
|
48
|
+
const ok = await copyToClipboard(fullCommand, {
|
|
49
|
+
toast,
|
|
50
|
+
toastMessage: "Command",
|
|
51
|
+
});
|
|
52
|
+
if (ok) {
|
|
53
|
+
setCopied(true);
|
|
54
|
+
setTimeout(() => setCopied(false), 2000);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const fontFamily = Platform.select({
|
|
59
|
+
ios: "Menlo",
|
|
60
|
+
android: "monospace",
|
|
61
|
+
default: "monospace",
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const radius = resolveRadius("sm");
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<View
|
|
68
|
+
style={[
|
|
69
|
+
styles.container,
|
|
70
|
+
{
|
|
71
|
+
backgroundColor: colors.surface2,
|
|
72
|
+
borderColor: colors.border,
|
|
73
|
+
borderRadius: radius,
|
|
74
|
+
},
|
|
75
|
+
style,
|
|
76
|
+
]}
|
|
77
|
+
>
|
|
78
|
+
<View style={styles.textContainer}>
|
|
79
|
+
<Text
|
|
80
|
+
style={[
|
|
81
|
+
styles.prompt,
|
|
82
|
+
{
|
|
83
|
+
color: colors.statusWarning,
|
|
84
|
+
fontFamily,
|
|
85
|
+
},
|
|
86
|
+
]}
|
|
87
|
+
>
|
|
88
|
+
$
|
|
89
|
+
</Text>
|
|
90
|
+
<Text
|
|
91
|
+
selectable
|
|
92
|
+
numberOfLines={2}
|
|
93
|
+
style={[
|
|
94
|
+
styles.commandText,
|
|
95
|
+
{
|
|
96
|
+
color: colors.foreground,
|
|
97
|
+
fontFamily,
|
|
98
|
+
},
|
|
99
|
+
textStyle,
|
|
100
|
+
]}
|
|
101
|
+
>
|
|
102
|
+
{prog ? (
|
|
103
|
+
<>
|
|
104
|
+
<Text style={styles.bold}>{prog}</Text>
|
|
105
|
+
{rest.length > 0 ? (
|
|
106
|
+
<Text style={{ color: colors.foregroundMuted }}>
|
|
107
|
+
{" " + rest.map((a) => (a.includes(" ") ? JSON.stringify(a) : a)).join(" ")}
|
|
108
|
+
</Text>
|
|
109
|
+
) : null}
|
|
110
|
+
</>
|
|
111
|
+
) : (
|
|
112
|
+
command ?? ""
|
|
113
|
+
)}
|
|
114
|
+
</Text>
|
|
115
|
+
</View>
|
|
116
|
+
<Pressable
|
|
117
|
+
onPress={handleCopy}
|
|
118
|
+
accessibilityRole="button"
|
|
119
|
+
accessibilityLabel={copyLabel}
|
|
120
|
+
hitSlop={8}
|
|
121
|
+
style={({ pressed }) => [
|
|
122
|
+
styles.copyButton,
|
|
123
|
+
{
|
|
124
|
+
backgroundColor: pressed ? colors.surface1 : "transparent",
|
|
125
|
+
borderRadius: Math.max(2, radius - 2),
|
|
126
|
+
},
|
|
127
|
+
]}
|
|
128
|
+
>
|
|
129
|
+
<Icon
|
|
130
|
+
name={copied ? "Check" : "Copy"}
|
|
131
|
+
size={13}
|
|
132
|
+
color={copied ? colors.statusSuccess : colors.foregroundMuted}
|
|
133
|
+
/>
|
|
134
|
+
</Pressable>
|
|
135
|
+
</View>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const styles = StyleSheet.create({
|
|
140
|
+
container: {
|
|
141
|
+
flexDirection: "row",
|
|
142
|
+
alignItems: "center",
|
|
143
|
+
justifyContent: "space-between",
|
|
144
|
+
borderWidth: 1,
|
|
145
|
+
paddingHorizontal: 10,
|
|
146
|
+
paddingVertical: 6,
|
|
147
|
+
gap: 8,
|
|
148
|
+
},
|
|
149
|
+
textContainer: {
|
|
150
|
+
flexDirection: "row",
|
|
151
|
+
alignItems: "center",
|
|
152
|
+
flex: 1,
|
|
153
|
+
overflow: "hidden",
|
|
154
|
+
gap: 6,
|
|
155
|
+
},
|
|
156
|
+
prompt: {
|
|
157
|
+
fontWeight: "700",
|
|
158
|
+
fontSize: 12,
|
|
159
|
+
},
|
|
160
|
+
commandText: {
|
|
161
|
+
fontSize: 12,
|
|
162
|
+
flex: 1,
|
|
163
|
+
},
|
|
164
|
+
bold: {
|
|
165
|
+
fontWeight: "700",
|
|
166
|
+
},
|
|
167
|
+
copyButton: {
|
|
168
|
+
padding: 4,
|
|
169
|
+
alignItems: "center",
|
|
170
|
+
justifyContent: "center",
|
|
171
|
+
},
|
|
172
|
+
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Pressable,
|
|
4
|
+
StyleSheet,
|
|
5
|
+
Text,
|
|
6
|
+
type StyleProp,
|
|
7
|
+
type TextStyle,
|
|
8
|
+
type ViewStyle,
|
|
9
|
+
} from "react-native";
|
|
10
|
+
import { getClientHost } from "../host";
|
|
11
|
+
import { usePluginTheme } from "../theme/provider";
|
|
12
|
+
import { copyToClipboard } from "../utils/clipboard";
|
|
13
|
+
|
|
14
|
+
export type CopyButtonSize = "sm" | "md";
|
|
15
|
+
export type CopyButtonVariant = "ghost" | "secondary";
|
|
16
|
+
|
|
17
|
+
export interface CopyButtonFeedback {
|
|
18
|
+
icon: string;
|
|
19
|
+
label: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Pure idle/copied visual state, split out so the feedback contract is
|
|
24
|
+
* testable without a renderer or a clipboard. Callers may override the idle
|
|
25
|
+
* `icon`/`label`; the copied state is always the Check/"Copied!" affordance.
|
|
26
|
+
*/
|
|
27
|
+
export function resolveCopyButtonFeedback(
|
|
28
|
+
copied: boolean,
|
|
29
|
+
options: { icon?: string; label?: string; copiedLabel?: string } = {},
|
|
30
|
+
): CopyButtonFeedback {
|
|
31
|
+
const idleIcon = options.icon ?? "Copy";
|
|
32
|
+
const idleLabel = options.label ?? "Copy";
|
|
33
|
+
const copiedLabel = options.copiedLabel ?? "Copied!";
|
|
34
|
+
return copied ? { icon: "Check", label: copiedLabel } : { icon: idleIcon, label: idleLabel };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CopyButtonProps {
|
|
38
|
+
/** Literal text to copy. Ignored when `getText` is provided. */
|
|
39
|
+
text?: string;
|
|
40
|
+
/** Lazy/Promise text source, resolved at press time. Takes precedence over `text`. */
|
|
41
|
+
getText?: () => string | Promise<string>;
|
|
42
|
+
/** Idle button label. Defaults to "Copy". */
|
|
43
|
+
label?: string;
|
|
44
|
+
/** Label shown after a successful copy. Defaults to "Copied!". */
|
|
45
|
+
copiedLabel?: string;
|
|
46
|
+
/** Icon name for the idle state. Defaults to "Copy". */
|
|
47
|
+
icon?: string;
|
|
48
|
+
size?: CopyButtonSize;
|
|
49
|
+
variant?: CopyButtonVariant;
|
|
50
|
+
accessibilityLabel?: string;
|
|
51
|
+
/** Forwarded to the clipboard toast (e.g. "timeline card"). */
|
|
52
|
+
toastMessage?: string;
|
|
53
|
+
/** How long the Check/"Copied!" feedback stays up. Defaults to 2000ms. */
|
|
54
|
+
feedbackDurationMs?: number;
|
|
55
|
+
disabled?: boolean;
|
|
56
|
+
style?: StyleProp<ViewStyle>;
|
|
57
|
+
textStyle?: StyleProp<TextStyle>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Explicit copy affordance for styled/plugin surfaces.
|
|
62
|
+
*
|
|
63
|
+
* Web's selection-copy handler only rebuilds clipboard content for
|
|
64
|
+
* `[data-testid="assistant-message"]` selections, so styled timeline, telemetry,
|
|
65
|
+
* and panel content copies nothing (xpufx-org/paseo#278). This bypasses that
|
|
66
|
+
* gate with the helper's host `copyText` / `copyToClipboard` path and shows
|
|
67
|
+
* Check/"Copied!" feedback, matching CodeBlock's pattern.
|
|
68
|
+
*/
|
|
69
|
+
export function CopyButton({
|
|
70
|
+
text,
|
|
71
|
+
getText,
|
|
72
|
+
label,
|
|
73
|
+
copiedLabel,
|
|
74
|
+
icon,
|
|
75
|
+
size = "sm",
|
|
76
|
+
variant = "ghost",
|
|
77
|
+
accessibilityLabel,
|
|
78
|
+
toastMessage,
|
|
79
|
+
feedbackDurationMs = 2000,
|
|
80
|
+
disabled = false,
|
|
81
|
+
style,
|
|
82
|
+
textStyle,
|
|
83
|
+
}: CopyButtonProps): React.ReactElement | null {
|
|
84
|
+
const { Icon, useToast } = getClientHost();
|
|
85
|
+
const { colors, resolveRadius, touchTargetMin, alpha } = usePluginTheme();
|
|
86
|
+
const toast = useToast();
|
|
87
|
+
const [copied, setCopied] = useState(false);
|
|
88
|
+
const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
89
|
+
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
return () => {
|
|
92
|
+
if (timer.current) clearTimeout(timer.current);
|
|
93
|
+
};
|
|
94
|
+
}, []);
|
|
95
|
+
|
|
96
|
+
const hasSource = getText !== undefined || text !== undefined;
|
|
97
|
+
if (!hasSource) return null;
|
|
98
|
+
|
|
99
|
+
const feedback = resolveCopyButtonFeedback(copied, { icon, label, copiedLabel });
|
|
100
|
+
const iconNode = (
|
|
101
|
+
<Icon
|
|
102
|
+
name={feedback.icon}
|
|
103
|
+
size={size === "sm" ? 12 : 14}
|
|
104
|
+
color={copied ? colors.statusSuccess : colors.foregroundMuted}
|
|
105
|
+
/>
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const secondary = variant === "secondary";
|
|
109
|
+
const bg = secondary ? colors.surface1 : "transparent";
|
|
110
|
+
const border = secondary ? colors.border : "transparent";
|
|
111
|
+
const radius = resolveRadius(size === "sm" ? "sm" : "md");
|
|
112
|
+
|
|
113
|
+
const handleCopy = async () => {
|
|
114
|
+
if (disabled) return;
|
|
115
|
+
let value: string | undefined;
|
|
116
|
+
try {
|
|
117
|
+
value = getText ? await getText() : text;
|
|
118
|
+
} catch {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (value === undefined || value === null) return;
|
|
122
|
+
|
|
123
|
+
const ok = await copyToClipboard(String(value), { toast, toastMessage });
|
|
124
|
+
if (!ok) return;
|
|
125
|
+
|
|
126
|
+
setCopied(true);
|
|
127
|
+
if (timer.current) clearTimeout(timer.current);
|
|
128
|
+
timer.current = setTimeout(() => setCopied(false), feedbackDurationMs);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<Pressable
|
|
133
|
+
onPress={handleCopy}
|
|
134
|
+
disabled={disabled}
|
|
135
|
+
accessibilityRole="button"
|
|
136
|
+
accessibilityLabel={accessibilityLabel || label || "Copy"}
|
|
137
|
+
hitSlop={Math.max(0, (touchTargetMin - 28) / 2)}
|
|
138
|
+
style={({ pressed }) => [
|
|
139
|
+
styles.base,
|
|
140
|
+
{
|
|
141
|
+
backgroundColor: pressed && !disabled ? alpha(colors.surface2, secondary ? 1 : 0.7) : bg,
|
|
142
|
+
borderColor: border,
|
|
143
|
+
borderWidth: border !== "transparent" ? 1 : 0,
|
|
144
|
+
borderRadius: radius,
|
|
145
|
+
paddingVertical: size === "sm" ? 3 : 5,
|
|
146
|
+
paddingHorizontal: size === "sm" ? 8 : 10,
|
|
147
|
+
opacity: disabled ? 0.45 : 1,
|
|
148
|
+
},
|
|
149
|
+
style,
|
|
150
|
+
]}
|
|
151
|
+
>
|
|
152
|
+
{iconNode}
|
|
153
|
+
{feedback.label !== "" ? (
|
|
154
|
+
<Text
|
|
155
|
+
style={[
|
|
156
|
+
styles.text,
|
|
157
|
+
{
|
|
158
|
+
fontSize: size === "sm" ? 11 : 12,
|
|
159
|
+
color: copied ? colors.statusSuccess : colors.foregroundMuted,
|
|
160
|
+
},
|
|
161
|
+
textStyle,
|
|
162
|
+
]}
|
|
163
|
+
>
|
|
164
|
+
{feedback.label}
|
|
165
|
+
</Text>
|
|
166
|
+
) : null}
|
|
167
|
+
</Pressable>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const styles = StyleSheet.create({
|
|
172
|
+
base: {
|
|
173
|
+
flexDirection: "row",
|
|
174
|
+
alignItems: "center",
|
|
175
|
+
gap: 4,
|
|
176
|
+
},
|
|
177
|
+
text: {
|
|
178
|
+
fontWeight: "500",
|
|
179
|
+
},
|
|
180
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import React, { type ReactNode } from "react";
|
|
2
|
+
import { StyleSheet, Text, View, type StyleProp, type ViewStyle } from "react-native";
|
|
3
|
+
import { usePluginTheme } from "../theme/provider";
|
|
4
|
+
|
|
5
|
+
export interface DataColumn<T> {
|
|
6
|
+
key: string;
|
|
7
|
+
header: string;
|
|
8
|
+
flex?: number;
|
|
9
|
+
width?: number;
|
|
10
|
+
align?: "left" | "center" | "right";
|
|
11
|
+
render: (item: T) => ReactNode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface DataTableProps<T> {
|
|
15
|
+
data: T[];
|
|
16
|
+
columns: DataColumn<T>[];
|
|
17
|
+
keyExtractor: (item: T, index: number) => string;
|
|
18
|
+
emptyState?: ReactNode;
|
|
19
|
+
style?: StyleProp<ViewStyle>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Responsive data table that automatically reflows between a traditional table
|
|
24
|
+
* on desktop and structured card list on mobile / compact viewports.
|
|
25
|
+
*/
|
|
26
|
+
export function DataTable<T>({
|
|
27
|
+
data,
|
|
28
|
+
columns,
|
|
29
|
+
keyExtractor,
|
|
30
|
+
emptyState,
|
|
31
|
+
style,
|
|
32
|
+
}: DataTableProps<T>) {
|
|
33
|
+
const { colors, resolveRadius, isCompact } = usePluginTheme();
|
|
34
|
+
const radius = resolveRadius("sm");
|
|
35
|
+
|
|
36
|
+
if (!data || data.length === 0) {
|
|
37
|
+
return emptyState ? <View style={style}>{emptyState}</View> : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Mobile / compact card-list layout
|
|
41
|
+
if (isCompact) {
|
|
42
|
+
return (
|
|
43
|
+
<View style={[styles.compactContainer, style]}>
|
|
44
|
+
{data.map((item, idx) => (
|
|
45
|
+
<View
|
|
46
|
+
key={keyExtractor(item, idx)}
|
|
47
|
+
style={[
|
|
48
|
+
styles.compactCard,
|
|
49
|
+
{
|
|
50
|
+
backgroundColor: colors.surface1,
|
|
51
|
+
borderColor: colors.border,
|
|
52
|
+
borderRadius: radius,
|
|
53
|
+
},
|
|
54
|
+
]}
|
|
55
|
+
>
|
|
56
|
+
{columns.map((col) => (
|
|
57
|
+
<View key={col.key} style={styles.compactRow}>
|
|
58
|
+
<Text style={[styles.compactHeader, { color: colors.foregroundMuted }]}>
|
|
59
|
+
{col.header}
|
|
60
|
+
</Text>
|
|
61
|
+
<View style={styles.compactValue}>{col.render(item)}</View>
|
|
62
|
+
</View>
|
|
63
|
+
))}
|
|
64
|
+
</View>
|
|
65
|
+
))}
|
|
66
|
+
</View>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Desktop tabular layout
|
|
71
|
+
return (
|
|
72
|
+
<View
|
|
73
|
+
style={[
|
|
74
|
+
styles.table,
|
|
75
|
+
{
|
|
76
|
+
borderColor: colors.border,
|
|
77
|
+
borderRadius: radius,
|
|
78
|
+
backgroundColor: colors.surface0,
|
|
79
|
+
},
|
|
80
|
+
style,
|
|
81
|
+
]}
|
|
82
|
+
>
|
|
83
|
+
{/* Header Row */}
|
|
84
|
+
<View
|
|
85
|
+
style={[
|
|
86
|
+
styles.headerRow,
|
|
87
|
+
{
|
|
88
|
+
backgroundColor: colors.surface1,
|
|
89
|
+
borderBottomColor: colors.border,
|
|
90
|
+
},
|
|
91
|
+
]}
|
|
92
|
+
>
|
|
93
|
+
{columns.map((col) => (
|
|
94
|
+
<View
|
|
95
|
+
key={col.key}
|
|
96
|
+
style={[
|
|
97
|
+
styles.cell,
|
|
98
|
+
col.flex !== undefined ? { flex: col.flex } : { flex: 1 },
|
|
99
|
+
col.width !== undefined ? { width: col.width } : undefined,
|
|
100
|
+
col.align === "right"
|
|
101
|
+
? styles.alignRight
|
|
102
|
+
: col.align === "center"
|
|
103
|
+
? styles.alignCenter
|
|
104
|
+
: styles.alignLeft,
|
|
105
|
+
]}
|
|
106
|
+
>
|
|
107
|
+
<Text style={[styles.headerText, { color: colors.foregroundMuted }]}>
|
|
108
|
+
{col.header}
|
|
109
|
+
</Text>
|
|
110
|
+
</View>
|
|
111
|
+
))}
|
|
112
|
+
</View>
|
|
113
|
+
|
|
114
|
+
{/* Rows */}
|
|
115
|
+
{data.map((item, idx) => (
|
|
116
|
+
<View
|
|
117
|
+
key={keyExtractor(item, idx)}
|
|
118
|
+
style={[
|
|
119
|
+
styles.row,
|
|
120
|
+
idx < data.length - 1 && { borderBottomColor: colors.border, borderBottomWidth: 1 },
|
|
121
|
+
]}
|
|
122
|
+
>
|
|
123
|
+
{columns.map((col) => (
|
|
124
|
+
<View
|
|
125
|
+
key={col.key}
|
|
126
|
+
style={[
|
|
127
|
+
styles.cell,
|
|
128
|
+
col.flex !== undefined ? { flex: col.flex } : { flex: 1 },
|
|
129
|
+
col.width !== undefined ? { width: col.width } : undefined,
|
|
130
|
+
col.align === "right"
|
|
131
|
+
? styles.alignRight
|
|
132
|
+
: col.align === "center"
|
|
133
|
+
? styles.alignCenter
|
|
134
|
+
: styles.alignLeft,
|
|
135
|
+
]}
|
|
136
|
+
>
|
|
137
|
+
{col.render(item)}
|
|
138
|
+
</View>
|
|
139
|
+
))}
|
|
140
|
+
</View>
|
|
141
|
+
))}
|
|
142
|
+
</View>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const styles = StyleSheet.create({
|
|
147
|
+
table: {
|
|
148
|
+
borderWidth: 1,
|
|
149
|
+
overflow: "hidden",
|
|
150
|
+
},
|
|
151
|
+
headerRow: {
|
|
152
|
+
flexDirection: "row",
|
|
153
|
+
borderBottomWidth: 1,
|
|
154
|
+
paddingVertical: 8,
|
|
155
|
+
paddingHorizontal: 12,
|
|
156
|
+
},
|
|
157
|
+
row: {
|
|
158
|
+
flexDirection: "row",
|
|
159
|
+
paddingVertical: 10,
|
|
160
|
+
paddingHorizontal: 12,
|
|
161
|
+
alignItems: "center",
|
|
162
|
+
},
|
|
163
|
+
cell: {
|
|
164
|
+
justifyContent: "center",
|
|
165
|
+
},
|
|
166
|
+
alignLeft: {
|
|
167
|
+
alignItems: "flex-start",
|
|
168
|
+
},
|
|
169
|
+
alignCenter: {
|
|
170
|
+
alignItems: "center",
|
|
171
|
+
},
|
|
172
|
+
alignRight: {
|
|
173
|
+
alignItems: "flex-end",
|
|
174
|
+
},
|
|
175
|
+
headerText: {
|
|
176
|
+
fontSize: 11,
|
|
177
|
+
fontWeight: "600",
|
|
178
|
+
textTransform: "uppercase",
|
|
179
|
+
letterSpacing: 0.5,
|
|
180
|
+
},
|
|
181
|
+
compactContainer: {
|
|
182
|
+
gap: 8,
|
|
183
|
+
},
|
|
184
|
+
compactCard: {
|
|
185
|
+
borderWidth: 1,
|
|
186
|
+
padding: 12,
|
|
187
|
+
gap: 6,
|
|
188
|
+
},
|
|
189
|
+
compactRow: {
|
|
190
|
+
flexDirection: "row",
|
|
191
|
+
justifyContent: "space-between",
|
|
192
|
+
alignItems: "center",
|
|
193
|
+
},
|
|
194
|
+
compactHeader: {
|
|
195
|
+
fontSize: 12,
|
|
196
|
+
},
|
|
197
|
+
compactValue: {
|
|
198
|
+
alignItems: "flex-end",
|
|
199
|
+
},
|
|
200
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import React, { type ReactNode } from "react";
|
|
2
|
+
import { StyleSheet, Text, View, type StyleProp, type ViewStyle } from "react-native";
|
|
3
|
+
import { getClientHost } from "../host";
|
|
4
|
+
import { usePluginTheme } from "../theme/provider";
|
|
5
|
+
import { Button, type ButtonProps } from "./Button";
|
|
6
|
+
|
|
7
|
+
export interface EmptyStateProps {
|
|
8
|
+
icon?: string | ReactNode;
|
|
9
|
+
title: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
action?: ButtonProps;
|
|
12
|
+
actionLabel?: string;
|
|
13
|
+
onAction?: () => void;
|
|
14
|
+
style?: StyleProp<ViewStyle>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function EmptyState({
|
|
18
|
+
icon = "Inbox",
|
|
19
|
+
title,
|
|
20
|
+
description,
|
|
21
|
+
action,
|
|
22
|
+
actionLabel,
|
|
23
|
+
onAction,
|
|
24
|
+
style,
|
|
25
|
+
}: EmptyStateProps) {
|
|
26
|
+
const { Icon } = getClientHost();
|
|
27
|
+
const { colors, isCompact, typography, padding } = usePluginTheme();
|
|
28
|
+
|
|
29
|
+
const resolvedAction: ButtonProps | undefined = action
|
|
30
|
+
? action
|
|
31
|
+
: actionLabel && onAction
|
|
32
|
+
? { label: actionLabel, onPress: onAction, variant: "secondary" }
|
|
33
|
+
: undefined;
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<View style={[styles.container, { padding: isCompact ? padding.horizontal + 8 : padding.horizontal * 2 }, style]}>
|
|
37
|
+
{icon ? (
|
|
38
|
+
typeof icon === "string" ? (
|
|
39
|
+
<View style={[styles.iconWrapper, { backgroundColor: colors.surface1 }]}>
|
|
40
|
+
<Icon name={icon} size={isCompact ? 24 : 32} color={colors.foregroundMuted} />
|
|
41
|
+
</View>
|
|
42
|
+
) : (
|
|
43
|
+
icon
|
|
44
|
+
)
|
|
45
|
+
) : null}
|
|
46
|
+
|
|
47
|
+
<Text style={[styles.title, { color: colors.foreground, ...typography.title }]}>
|
|
48
|
+
{title}
|
|
49
|
+
</Text>
|
|
50
|
+
|
|
51
|
+
{description ? (
|
|
52
|
+
<Text
|
|
53
|
+
style={[
|
|
54
|
+
styles.description,
|
|
55
|
+
{ color: colors.foregroundMuted, ...typography.body },
|
|
56
|
+
]}
|
|
57
|
+
>
|
|
58
|
+
{description}
|
|
59
|
+
</Text>
|
|
60
|
+
) : null}
|
|
61
|
+
|
|
62
|
+
{resolvedAction ? (
|
|
63
|
+
<View style={styles.actionRow}>
|
|
64
|
+
<Button size={isCompact ? "sm" : "md"} {...resolvedAction} />
|
|
65
|
+
</View>
|
|
66
|
+
) : null}
|
|
67
|
+
</View>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const styles = StyleSheet.create({
|
|
72
|
+
container: {
|
|
73
|
+
alignItems: "center",
|
|
74
|
+
justifyContent: "center",
|
|
75
|
+
gap: 8,
|
|
76
|
+
},
|
|
77
|
+
iconWrapper: {
|
|
78
|
+
width: 56,
|
|
79
|
+
height: 56,
|
|
80
|
+
borderRadius: 28,
|
|
81
|
+
alignItems: "center",
|
|
82
|
+
justifyContent: "center",
|
|
83
|
+
marginBottom: 4,
|
|
84
|
+
},
|
|
85
|
+
title: {
|
|
86
|
+
fontWeight: "600",
|
|
87
|
+
textAlign: "center",
|
|
88
|
+
},
|
|
89
|
+
description: {
|
|
90
|
+
textAlign: "center",
|
|
91
|
+
maxWidth: 280,
|
|
92
|
+
lineHeight: 18,
|
|
93
|
+
},
|
|
94
|
+
actionRow: {
|
|
95
|
+
marginTop: 8,
|
|
96
|
+
},
|
|
97
|
+
});
|