@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.
Files changed (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -0
  3. package/client/approvals.tsx +1525 -0
  4. package/client/error-boundary.tsx +28 -0
  5. package/client/vendor/paseo-plugin-helper/command-center.ts +43 -0
  6. package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +493 -0
  7. package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +250 -0
  8. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +156 -0
  9. package/client/vendor/paseo-plugin-helper/components/Button.tsx +178 -0
  10. package/client/vendor/paseo-plugin-helper/components/Card.tsx +225 -0
  11. package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +196 -0
  12. package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +277 -0
  13. package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +172 -0
  14. package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +180 -0
  15. package/client/vendor/paseo-plugin-helper/components/DataTable.tsx +200 -0
  16. package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +97 -0
  17. package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +70 -0
  18. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +73 -0
  19. package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +446 -0
  20. package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +247 -0
  21. package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +117 -0
  22. package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +53 -0
  23. package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +118 -0
  24. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +80 -0
  25. package/client/vendor/paseo-plugin-helper/components/Select.tsx +215 -0
  26. package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +80 -0
  27. package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +319 -0
  28. package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +150 -0
  29. package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +163 -0
  30. package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +157 -0
  31. package/client/vendor/paseo-plugin-helper/components/index.ts +25 -0
  32. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +224 -0
  33. package/client/vendor/paseo-plugin-helper/forge-icon.tsx +79 -0
  34. package/client/vendor/paseo-plugin-helper/host.ts +277 -0
  35. package/client/vendor/paseo-plugin-helper/icon.tsx +39 -0
  36. package/client/vendor/paseo-plugin-helper/index.ts +28 -0
  37. package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +49 -0
  38. package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +103 -0
  39. package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +65 -0
  40. package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +378 -0
  41. package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +49 -0
  42. package/client/vendor/paseo-plugin-helper/layout/Row.tsx +39 -0
  43. package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +39 -0
  44. package/client/vendor/paseo-plugin-helper/layout/index.ts +7 -0
  45. package/client/vendor/paseo-plugin-helper/panel.tsx +81 -0
  46. package/client/vendor/paseo-plugin-helper/pill.tsx +884 -0
  47. package/client/vendor/paseo-plugin-helper/query-refresh.ts +79 -0
  48. package/client/vendor/paseo-plugin-helper/query.ts +66 -0
  49. package/client/vendor/paseo-plugin-helper/settings-screen.tsx +372 -0
  50. package/client/vendor/paseo-plugin-helper/settings.ts +181 -0
  51. package/client/vendor/paseo-plugin-helper/shared-settings.ts +46 -0
  52. package/client/vendor/paseo-plugin-helper/snapshot.ts +68 -0
  53. package/client/vendor/paseo-plugin-helper/surface.tsx +80 -0
  54. package/client/vendor/paseo-plugin-helper/theme/color-utils.ts +118 -0
  55. package/client/vendor/paseo-plugin-helper/theme/flair.ts +76 -0
  56. package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +121 -0
  57. package/client/vendor/paseo-plugin-helper/theme/index.ts +7 -0
  58. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +214 -0
  59. package/client/vendor/paseo-plugin-helper/theme/responsive.ts +213 -0
  60. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +161 -0
  61. package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +57 -0
  62. package/client/vendor/paseo-plugin-helper/utils/clipboard.ts +149 -0
  63. package/client/vendor/paseo-plugin-helper/utils/haptics.ts +34 -0
  64. package/package.json +36 -0
  65. package/paseo-plugin.json +6 -0
  66. package/server/twofado.ts +536 -0
  67. package/server/vendor/paseo-plugin-helper/agent.ts +85 -0
  68. package/server/vendor/paseo-plugin-helper/custom-pills.ts +344 -0
  69. package/server/vendor/paseo-plugin-helper/index.ts +18 -0
  70. package/server/vendor/paseo-plugin-helper/jsonc.ts +78 -0
  71. package/server/vendor/paseo-plugin-helper/logger.ts +210 -0
  72. package/server/vendor/paseo-plugin-helper/mcp-config.ts +367 -0
  73. package/server/vendor/paseo-plugin-helper/mcp-injection.ts +85 -0
  74. package/server/vendor/paseo-plugin-helper/network.ts +91 -0
  75. package/server/vendor/paseo-plugin-helper/plugins.ts +160 -0
  76. package/server/vendor/paseo-plugin-helper/process.ts +186 -0
  77. package/server/vendor/paseo-plugin-helper/redact.ts +86 -0
  78. package/server/vendor/paseo-plugin-helper/rpc-guard.ts +77 -0
  79. package/server/vendor/paseo-plugin-helper/settings.ts +97 -0
  80. package/server/vendor/paseo-plugin-helper/shared-settings.ts +243 -0
  81. package/server/vendor/paseo-plugin-helper/storage.ts +244 -0
  82. package/server/vendor/paseo-plugin-helper/system.ts +128 -0
  83. package/server/vendor/paseo-plugin-helper/task.ts +116 -0
  84. package/server/vendor/paseo-plugin-helper/version.ts +153 -0
  85. package/server/vendor/paseo-plugin-helper/workspace-beacon.ts +418 -0
  86. package/shared/approval.ts +254 -0
  87. package/shared/vendor/paseo-plugin-helper/async.ts +35 -0
  88. package/shared/vendor/paseo-plugin-helper/custom-pills.ts +169 -0
  89. package/shared/vendor/paseo-plugin-helper/forge.ts +110 -0
  90. package/shared/vendor/paseo-plugin-helper/formatters.ts +271 -0
  91. package/shared/vendor/paseo-plugin-helper/highlight.ts +184 -0
  92. package/shared/vendor/paseo-plugin-helper/index.ts +10 -0
  93. package/shared/vendor/paseo-plugin-helper/rpc.ts +72 -0
  94. package/shared/vendor/paseo-plugin-helper/settings.ts +138 -0
  95. package/shared/vendor/paseo-plugin-helper/suite-settings.ts +17 -0
  96. package/shared/vendor/paseo-plugin-helper/suppressed.ts +31 -0
  97. 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
+ });