@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,250 @@
1
+ import React, { useEffect, useRef, type ReactNode } from "react";
2
+ import {
3
+ Animated,
4
+ Easing,
5
+ StyleSheet,
6
+ View,
7
+ type StyleProp,
8
+ type ViewStyle,
9
+ } from "react-native";
10
+ import { getClientHost } from "../host";
11
+ import { usePluginTheme } from "../theme/provider";
12
+ import { FALLBACK_ACCENT_FOREGROUND } from "../theme/tokens";
13
+ import type { ThemeColors } from "../../../../shared/vendor/paseo-plugin-helper/types";
14
+
15
+ export type AttentionBeaconMode = "radar" | "ring" | "glow" | "badge" | "bounce" | "pulse";
16
+ export type AttentionBeaconTone = "warning" | "accent" | "danger";
17
+
18
+ export interface AttentionBeaconProps {
19
+ children: ReactNode;
20
+ mode?: AttentionBeaconMode;
21
+ tone?: AttentionBeaconTone;
22
+ color?: string;
23
+ active?: boolean;
24
+ style?: StyleProp<ViewStyle>;
25
+ haloStyle?: StyleProp<ViewStyle>;
26
+ badgeStyle?: StyleProp<ViewStyle>;
27
+ accessibilityLabel?: string;
28
+ testID?: string;
29
+ badgeIcon?: string | ReactNode;
30
+ duration?: number;
31
+ easing?: (value: number) => number;
32
+ }
33
+
34
+ export function normalizeBeaconMode(mode?: AttentionBeaconMode): "radar" | "glow" | "badge" | "bounce" | "pulse" {
35
+ if (mode === "ring") return "radar";
36
+ if (mode === "glow" || mode === "badge" || mode === "bounce" || mode === "pulse" || mode === "radar") return mode;
37
+ return "radar";
38
+ }
39
+
40
+ export function resolveBeaconToneColor(
41
+ colors: ThemeColors,
42
+ tone: AttentionBeaconTone = "warning",
43
+ customColor?: string,
44
+ ): string {
45
+ if (customColor) return customColor;
46
+ if (tone === "danger") return colors.statusDanger;
47
+ if (tone === "accent") return colors.accent;
48
+ return colors.statusWarning;
49
+ }
50
+
51
+ function useLoop(driver: Animated.Value, toValue: number, duration: number, active: boolean): void {
52
+ useEffect(() => {
53
+ if (!active) return;
54
+ driver.setValue(0);
55
+ const animation = Animated.loop(
56
+ Animated.sequence([
57
+ Animated.timing(driver, { toValue, duration, easing: Easing.linear, useNativeDriver: true }),
58
+ Animated.timing(driver, { toValue: 0, duration: 0, useNativeDriver: true }),
59
+ ]),
60
+ );
61
+ animation.start();
62
+ return () => animation.stop();
63
+ }, [driver, toValue, duration, active]);
64
+ }
65
+
66
+ function usePingPong(
67
+ driver: Animated.Value,
68
+ duration: number,
69
+ active: boolean,
70
+ easing: (value: number) => number = Easing.linear,
71
+ ): void {
72
+ useEffect(() => {
73
+ if (!active) return;
74
+ driver.setValue(0);
75
+ const animation = Animated.loop(
76
+ Animated.sequence([
77
+ Animated.timing(driver, { toValue: 1, duration, easing, useNativeDriver: true }),
78
+ Animated.timing(driver, { toValue: 0, duration, easing, useNativeDriver: true }),
79
+ ]),
80
+ );
81
+ animation.start();
82
+ return () => animation.stop();
83
+ }, [driver, duration, active, easing]);
84
+ }
85
+
86
+ export function AttentionBeacon({
87
+ children,
88
+ mode = "radar",
89
+ tone = "warning",
90
+ color,
91
+ active = true,
92
+ style,
93
+ haloStyle,
94
+ badgeStyle,
95
+ accessibilityLabel,
96
+ testID,
97
+ badgeIcon,
98
+ duration = 900,
99
+ easing = Easing.linear,
100
+ }: AttentionBeaconProps) {
101
+ const { Icon } = getClientHost();
102
+ const { colors } = usePluginTheme();
103
+ const resolved = normalizeBeaconMode(mode);
104
+ const beaconColor = resolveBeaconToneColor(colors, tone, color);
105
+
106
+ const radar = useRef(new Animated.Value(0)).current;
107
+ const breath = useRef(new Animated.Value(0)).current;
108
+ const pip = useRef(new Animated.Value(0)).current;
109
+ const jiggle = useRef(new Animated.Value(0)).current;
110
+ const pulse = useRef(new Animated.Value(0)).current;
111
+
112
+ const isRadar = resolved === "radar" && active;
113
+ const isGlow = resolved === "glow" && active;
114
+ const isBadge = resolved === "badge" && active;
115
+ const isBounce = resolved === "bounce" && active;
116
+ const isPulse = resolved === "pulse" && active;
117
+
118
+ useLoop(radar, 1, 1600, isRadar);
119
+ usePingPong(breath, 900, isGlow);
120
+ usePingPong(pip, 900, isBadge);
121
+ usePingPong(jiggle, 350, isBounce);
122
+ usePingPong(pulse, duration, isPulse, easing);
123
+
124
+ if (!active) {
125
+ return <View style={[styles.wrapper, style]}>{children}</View>;
126
+ }
127
+
128
+ if (resolved === "glow") {
129
+ const opacity = breath.interpolate({ inputRange: [0, 1], outputRange: [0.35, 1] });
130
+ return (
131
+ <View style={[styles.wrapper, style]} accessibilityLabel={accessibilityLabel} testID={testID}>
132
+ {children}
133
+ <Animated.View
134
+ pointerEvents="none"
135
+ testID={testID ? `${testID}-glow` : undefined}
136
+ style={[styles.glowHalo, { borderColor: beaconColor, opacity }, haloStyle]}
137
+ />
138
+ </View>
139
+ );
140
+ }
141
+
142
+ if (resolved === "badge") {
143
+ const opacity = pip.interpolate({ inputRange: [0, 1], outputRange: [0.5, 1] });
144
+ const scale = pip.interpolate({ inputRange: [0, 1], outputRange: [0.85, 1.15] });
145
+ const hasIcon = badgeIcon !== undefined && badgeIcon !== null && badgeIcon !== "";
146
+ const iconColor = colors.accentForeground || FALLBACK_ACCENT_FOREGROUND;
147
+ return (
148
+ <View style={[styles.wrapper, style]} accessibilityLabel={accessibilityLabel} testID={testID}>
149
+ {children}
150
+ <Animated.View
151
+ pointerEvents="none"
152
+ testID={testID ? `${testID}-badge` : undefined}
153
+ style={[
154
+ styles.pip,
155
+ { backgroundColor: beaconColor, opacity, transform: [{ scale }] },
156
+ hasIcon && styles.pipWithIcon,
157
+ badgeStyle,
158
+ ]}
159
+ >
160
+ {hasIcon ? (
161
+ typeof badgeIcon === "string" ? (
162
+ <Icon name={badgeIcon} size={10} color={iconColor} />
163
+ ) : (
164
+ badgeIcon
165
+ )
166
+ ) : null}
167
+ </Animated.View>
168
+ </View>
169
+ );
170
+ }
171
+
172
+ if (resolved === "bounce") {
173
+ const translateY = jiggle.interpolate({ inputRange: [0, 1], outputRange: [0, -5] });
174
+ return (
175
+ <View style={[styles.wrapper, style]} accessibilityLabel={accessibilityLabel} testID={testID}>
176
+ <Animated.View style={{ transform: [{ translateY }] }}>{children}</Animated.View>
177
+ </View>
178
+ );
179
+ }
180
+
181
+ if (resolved === "pulse") {
182
+ const opacity = pulse.interpolate({ inputRange: [0, 1], outputRange: [0.35, 1] });
183
+ return (
184
+ <View style={[styles.wrapper, style]} accessibilityLabel={accessibilityLabel} testID={testID}>
185
+ <Animated.View
186
+ testID={testID ? `${testID}-pulse` : undefined}
187
+ style={{ opacity }}
188
+ >
189
+ {children}
190
+ </Animated.View>
191
+ </View>
192
+ );
193
+ }
194
+
195
+ const scale = radar.interpolate({ inputRange: [0, 1], outputRange: [1, 1.5] });
196
+ const opacity = radar.interpolate({ inputRange: [0, 1], outputRange: [0.55, 0] });
197
+ return (
198
+ <View style={[styles.wrapper, style]} accessibilityLabel={accessibilityLabel} testID={testID}>
199
+ <Animated.View
200
+ pointerEvents="none"
201
+ testID={testID ? `${testID}-halo` : undefined}
202
+ style={[
203
+ styles.radarHalo,
204
+ { borderColor: beaconColor, opacity, transform: [{ scale }] },
205
+ haloStyle,
206
+ ]}
207
+ />
208
+ {children}
209
+ </View>
210
+ );
211
+ }
212
+
213
+ const styles = StyleSheet.create({
214
+ wrapper: {
215
+ position: "relative",
216
+ },
217
+ radarHalo: {
218
+ position: "absolute",
219
+ top: -4,
220
+ left: -4,
221
+ right: -4,
222
+ bottom: -4,
223
+ borderWidth: 2,
224
+ borderRadius: 12,
225
+ },
226
+ glowHalo: {
227
+ position: "absolute",
228
+ top: -2,
229
+ left: -2,
230
+ right: -2,
231
+ bottom: -2,
232
+ borderWidth: 2,
233
+ borderRadius: 10,
234
+ },
235
+ pip: {
236
+ position: "absolute",
237
+ top: -4,
238
+ right: -4,
239
+ width: 10,
240
+ height: 10,
241
+ borderRadius: 5,
242
+ },
243
+ pipWithIcon: {
244
+ width: 16,
245
+ height: 16,
246
+ borderRadius: 8,
247
+ alignItems: "center",
248
+ justifyContent: "center",
249
+ },
250
+ });
@@ -0,0 +1,156 @@
1
+ import React, { type ReactNode } from "react";
2
+ import { StyleSheet, Text, View, type StyleProp, type TextStyle, type ViewStyle } from "react-native";
3
+ import { getClientHost } from "../host";
4
+ import { usePluginTheme } from "../theme/provider";
5
+ import { FALLBACK_ACCENT_FOREGROUND } from "../theme/tokens";
6
+ import { HighlightedText } from "./HighlightedText";
7
+ import type { StatusVariant } from "../../../../shared/vendor/paseo-plugin-helper/types";
8
+
9
+ export type BadgeStyle = "tinted" | "outline" | "solid";
10
+ export type BadgeSize = "sm" | "md";
11
+
12
+ export interface BadgeProps {
13
+ label: string;
14
+ variant?: StatusVariant;
15
+ styleVariant?: BadgeStyle;
16
+ size?: BadgeSize;
17
+ icon?: string | ReactNode;
18
+ dot?: boolean;
19
+ style?: StyleProp<ViewStyle>;
20
+ textStyle?: StyleProp<TextStyle>;
21
+ /**
22
+ * When set, every case-insensitive occurrence of the query inside `label` is
23
+ * painted with the accent highlight. The query is matched literally, never as
24
+ * a regular expression.
25
+ */
26
+ highlightQuery?: string;
27
+ /**
28
+ * With `highlightQuery`, marks the whole label when the query has neither a
29
+ * literal nor a token hit — for a single primary chip, not a chip list.
30
+ */
31
+ highlightFuzzyFallback?: boolean;
32
+ }
33
+
34
+ export function Badge({
35
+ label,
36
+ variant = "neutral",
37
+ styleVariant = "tinted",
38
+ size = "md",
39
+ icon,
40
+ dot = false,
41
+ style,
42
+ textStyle,
43
+ highlightQuery,
44
+ highlightFuzzyFallback,
45
+ }: BadgeProps) {
46
+ const { Icon } = getClientHost();
47
+ const { colors, flair, resolveRadius, getVariantPalette, getStatusColor, typography } =
48
+ usePluginTheme();
49
+ const caption = typography?.caption ?? {
50
+ fontSize: 11,
51
+ lineHeight: 15,
52
+ fontWeight: "400" as const,
53
+ };
54
+
55
+ const fontSize = size === "sm" ? 10 : caption.fontSize;
56
+ const lineHeight = size === "sm" ? 12 : caption.lineHeight;
57
+ const paddingVertical = size === "sm" ? 1 : Math.max(2, Math.floor(caption.lineHeight / 5));
58
+ const paddingHorizontal = size === "sm" ? 5 : caption.fontSize < 11 ? 6 : 8;
59
+ const iconSize = size === "sm" ? 10 : caption.fontSize < 11 ? 10 : 11;
60
+
61
+ const radius = resolveRadius("pill");
62
+ const palette = getVariantPalette(variant);
63
+ const solidColor = getStatusColor(variant);
64
+
65
+ let bg = palette.bg;
66
+ let border = palette.border;
67
+ let textColor = palette.text;
68
+
69
+ if (styleVariant === "outline") {
70
+ bg = "transparent";
71
+ border = palette.border;
72
+ textColor = palette.text;
73
+ } else if (styleVariant === "solid") {
74
+ bg = solidColor;
75
+ border = "transparent";
76
+ textColor = colors.accentForeground || FALLBACK_ACCENT_FOREGROUND;
77
+ }
78
+
79
+ const renderIcon = () => {
80
+ if (dot) {
81
+ return (
82
+ <View
83
+ style={[
84
+ styles.dot,
85
+ {
86
+ backgroundColor: textColor,
87
+ },
88
+ ]}
89
+ />
90
+ );
91
+ }
92
+ if (!icon) return null;
93
+ if (typeof icon === "string") {
94
+ return <Icon name={icon} size={iconSize} color={textColor} />;
95
+ }
96
+ return icon;
97
+ };
98
+
99
+ return (
100
+ <View
101
+ style={[
102
+ styles.badge,
103
+ {
104
+ backgroundColor: bg,
105
+ borderColor: border,
106
+ borderRadius: radius,
107
+ paddingVertical,
108
+ paddingHorizontal,
109
+ },
110
+ style,
111
+ ]}
112
+ >
113
+ {renderIcon()}
114
+ <Text
115
+ style={[
116
+ styles.text,
117
+ {
118
+ color: textColor,
119
+ fontSize,
120
+ lineHeight,
121
+ textTransform: flair.headingTransform === "uppercase" ? "uppercase" : "none",
122
+ },
123
+ textStyle,
124
+ ]}
125
+ >
126
+ {highlightQuery ? (
127
+ <HighlightedText
128
+ text={label}
129
+ query={highlightQuery}
130
+ fuzzyFallback={highlightFuzzyFallback}
131
+ />
132
+ ) : (
133
+ label
134
+ )}
135
+ </Text>
136
+ </View>
137
+ );
138
+ }
139
+
140
+ const styles = StyleSheet.create({
141
+ badge: {
142
+ flexDirection: "row",
143
+ alignItems: "center",
144
+ alignSelf: "flex-start",
145
+ borderWidth: 1,
146
+ gap: 4,
147
+ },
148
+ text: {
149
+ fontWeight: "600",
150
+ },
151
+ dot: {
152
+ width: 6,
153
+ height: 6,
154
+ borderRadius: 3,
155
+ },
156
+ });
@@ -0,0 +1,178 @@
1
+ import React, { type ReactNode } from "react";
2
+ import {
3
+ ActivityIndicator,
4
+ Pressable,
5
+ StyleSheet,
6
+ Text,
7
+ type StyleProp,
8
+ type TextStyle,
9
+ type ViewStyle,
10
+ } from "react-native";
11
+ import { getClientHost } from "../host";
12
+ import { usePluginTheme } from "../theme/provider";
13
+ import { FALLBACK_ACCENT_FOREGROUND } from "../theme/tokens";
14
+ import { AttentionBeacon, type AttentionBeaconMode, type AttentionBeaconTone } from "./AttentionBeacon";
15
+
16
+ export type ButtonVariant = "primary" | "secondary" | "danger" | "ghost";
17
+ export type ButtonSize = "sm" | "md" | "lg";
18
+ export type ButtonAttention = boolean | "radar" | "glow" | "bounce";
19
+
20
+ export function resolveButtonAttentionMode(
21
+ attention?: ButtonAttention,
22
+ ): AttentionBeaconMode | null {
23
+ if (!attention) return null;
24
+ if (attention === true) return "radar";
25
+ return attention;
26
+ }
27
+
28
+ export function resolveButtonAttentionTone(variant: ButtonVariant): AttentionBeaconTone {
29
+ if (variant === "danger") return "danger";
30
+ if (variant === "primary") return "accent";
31
+ return "warning";
32
+ }
33
+
34
+ export interface ButtonProps {
35
+ label?: string;
36
+ variant?: ButtonVariant;
37
+ size?: ButtonSize;
38
+ icon?: string | ReactNode;
39
+ iconPosition?: "left" | "right";
40
+ onPress?: () => void | Promise<void>;
41
+ disabled?: boolean;
42
+ loading?: boolean;
43
+ style?: StyleProp<ViewStyle>;
44
+ textStyle?: StyleProp<TextStyle>;
45
+ accessibilityLabel?: string;
46
+ attention?: ButtonAttention;
47
+ }
48
+
49
+ export function Button({
50
+ label,
51
+ variant = "secondary",
52
+ size = "md",
53
+ icon,
54
+ iconPosition = "left",
55
+ onPress,
56
+ disabled = false,
57
+ loading = false,
58
+ style,
59
+ textStyle,
60
+ accessibilityLabel,
61
+ attention,
62
+ }: ButtonProps) {
63
+ const { Icon } = getClientHost();
64
+ const { colors, resolveRadius, touchTargetMin, isCompact, alpha } = usePluginTheme();
65
+
66
+ const radius = resolveRadius(size === "sm" ? "sm" : size === "lg" ? "lg" : "md");
67
+
68
+ // Determine sizing
69
+ const py = size === "sm" ? (isCompact ? 5 : 6) : size === "lg" ? 12 : isCompact ? 8 : 10;
70
+ const px = size === "sm" ? (isCompact ? 8 : 10) : size === "lg" ? 18 : isCompact ? 12 : 14;
71
+ const fontSize = size === "sm" ? 12 : size === "lg" ? 15 : 13;
72
+ const iconSize = size === "sm" ? 12 : size === "lg" ? 16 : 14;
73
+
74
+ // Determine colors by variant
75
+ let bg = "transparent";
76
+ let border = "transparent";
77
+ let textColor = colors.foreground;
78
+
79
+ switch (variant) {
80
+ case "primary":
81
+ bg = colors.accent;
82
+ textColor = colors.accentForeground || FALLBACK_ACCENT_FOREGROUND;
83
+ break;
84
+ case "danger":
85
+ bg = alpha(colors.statusDanger, 0.15);
86
+ border = alpha(colors.statusDanger, 0.4);
87
+ textColor = colors.statusDanger;
88
+ break;
89
+ case "ghost":
90
+ bg = "transparent";
91
+ textColor = colors.foregroundMuted;
92
+ break;
93
+ case "secondary":
94
+ default:
95
+ bg = colors.surface1;
96
+ border = colors.border;
97
+ textColor = colors.foreground;
98
+ break;
99
+ }
100
+
101
+ const renderIcon = () => {
102
+ if (!icon) return null;
103
+ if (typeof icon === "string") {
104
+ return <Icon name={icon} size={iconSize} color={textColor} />;
105
+ }
106
+ return icon;
107
+ };
108
+
109
+ const attentionMode = resolveButtonAttentionMode(attention);
110
+
111
+ const pressable = (
112
+ <Pressable
113
+ onPress={onPress}
114
+ disabled={disabled || loading}
115
+ accessibilityRole="button"
116
+ accessibilityLabel={accessibilityLabel || label}
117
+ hitSlop={Math.max(0, (touchTargetMin - 32) / 2)}
118
+ style={({ pressed }) => [
119
+ styles.base,
120
+ {
121
+ backgroundColor: pressed && !disabled ? alpha(bg, 0.8) : bg,
122
+ borderColor: border,
123
+ borderWidth: border !== "transparent" ? 1 : 0,
124
+ borderRadius: radius,
125
+ paddingVertical: py,
126
+ paddingHorizontal: px,
127
+ minHeight: Math.max(30, touchTargetMin - 4),
128
+ opacity: disabled ? 0.45 : 1,
129
+ },
130
+ style,
131
+ ]}
132
+ >
133
+ {loading ? (
134
+ <ActivityIndicator size="small" color={textColor} />
135
+ ) : (
136
+ <>
137
+ {iconPosition === "left" && renderIcon()}
138
+ {label ? (
139
+ <Text
140
+ style={[
141
+ styles.text,
142
+ {
143
+ color: textColor,
144
+ fontSize,
145
+ },
146
+ textStyle,
147
+ ]}
148
+ >
149
+ {label}
150
+ </Text>
151
+ ) : null}
152
+ {iconPosition === "right" && renderIcon()}
153
+ </>
154
+ )}
155
+ </Pressable>
156
+ );
157
+
158
+ if (!attentionMode) return pressable;
159
+
160
+ return (
161
+ <AttentionBeacon mode={attentionMode} tone={resolveButtonAttentionTone(variant)}>
162
+ {pressable}
163
+ </AttentionBeacon>
164
+ );
165
+ }
166
+
167
+ const styles = StyleSheet.create({
168
+ base: {
169
+ flexDirection: "row",
170
+ alignItems: "center",
171
+ justifyContent: "center",
172
+ gap: 6,
173
+ },
174
+ text: {
175
+ fontWeight: "600",
176
+ textAlign: "center",
177
+ },
178
+ });