@xpufx/paseo-top 0.4.4 → 0.4.5

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 (129) hide show
  1. package/OVERVIEW.md +17 -0
  2. package/client/host-ui.tsx +184 -0
  3. package/client/multi-host-view.tsx +57 -43
  4. package/client/multi-host.ts +36 -13
  5. package/client/pill-labels.ts +94 -1
  6. package/client/pill.tsx +690 -614
  7. package/client/resources-query.ts +1 -1
  8. package/client/settings-events.ts +1 -1
  9. package/client/settings-ui.tsx +86 -9
  10. package/client/surface.tsx +308 -276
  11. package/client/telemetry-copy.ts +8 -2
  12. package/client/telemetry.tsx +372 -321
  13. package/client/turn-panel.tsx +126 -104
  14. package/client/vendor/paseo-plugin-helper/{host.ts → core/host.ts} +9 -3
  15. package/client/vendor/paseo-plugin-helper/core/index.ts +76 -0
  16. package/client/vendor/paseo-plugin-helper/core/plugin-load-count.ts +89 -0
  17. package/client/vendor/paseo-plugin-helper/{query-refresh.ts → core/query-refresh.ts} +2 -2
  18. package/client/vendor/paseo-plugin-helper/{query.ts → core/query.ts} +2 -2
  19. package/client/vendor/paseo-plugin-helper/{settings.ts → core/settings.ts} +1 -1
  20. package/client/vendor/paseo-plugin-helper/{shared-settings.ts → core/shared-settings.ts} +2 -2
  21. package/client/vendor/paseo-plugin-helper/{utils → lifecycle}/clipboard.ts +2 -2
  22. package/client/vendor/paseo-plugin-helper/lifecycle/flair.ts +26 -0
  23. package/client/vendor/paseo-plugin-helper/{theme/color-utils.ts → lifecycle/host-color.ts} +95 -38
  24. package/client/vendor/paseo-plugin-helper/lifecycle/host-theme.tsx +109 -0
  25. package/client/vendor/paseo-plugin-helper/lifecycle/index.ts +23 -0
  26. package/client/vendor/paseo-plugin-helper/lifecycle/multi-host.ts +66 -0
  27. package/client/vendor/paseo-plugin-helper/{panel.tsx → lifecycle/panel.tsx} +7 -7
  28. package/client/vendor/paseo-plugin-helper/{pill.tsx → lifecycle/pill.tsx} +51 -18
  29. package/client/vendor/paseo-plugin-helper/lifecycle/providers.tsx +46 -0
  30. package/client/vendor/paseo-plugin-helper/lifecycle/scroll-owner.ts +26 -0
  31. package/client/vendor/paseo-plugin-helper/{surface.tsx → lifecycle/surface.tsx} +6 -6
  32. package/client/vendor/paseo-plugin-helper/ui/color.ts +1 -0
  33. package/client/vendor/paseo-plugin-helper/ui/content.tsx +1326 -0
  34. package/client/vendor/paseo-plugin-helper/ui/controls.tsx +1018 -0
  35. package/client/vendor/paseo-plugin-helper/{components/DataTable.tsx → ui/data.tsx} +50 -58
  36. package/client/vendor/paseo-plugin-helper/ui/index.ts +44 -0
  37. package/client/vendor/paseo-plugin-helper/ui/layout.tsx +263 -0
  38. package/client/vendor/paseo-plugin-helper/ui/modal.tsx +111 -0
  39. package/client/vendor/paseo-plugin-helper/ui/plugin-reload-banner.tsx +119 -0
  40. package/client/vendor/paseo-plugin-helper/{settings-screen.tsx → ui/settings-screen.tsx} +3 -3
  41. package/client/vendor/paseo-plugin-helper/ui/settings.ts +36 -0
  42. package/client/vendor/paseo-plugin-helper/ui/text.tsx +821 -0
  43. package/client/vendor/paseo-plugin-helper/ui/theme.tsx +1 -0
  44. package/client/vendor/permission-audit/client.tsx +706 -0
  45. package/client/vendor/permission-audit/index.tsx +1 -0
  46. package/index.client.tsx +58 -37
  47. package/index.server.ts +256 -234
  48. package/package.json +7 -4
  49. package/paseo-plugin.json +2 -0
  50. package/screenshots/1-paseo-top-modal-system.jpg +0 -0
  51. package/screenshots/2-paseo-top-modal-workspace.jpg +0 -0
  52. package/screenshots/3-paseo-top-modal-settings.jpg +0 -0
  53. package/screenshots/4-paseo-top-modal-about.jpg +0 -0
  54. package/screenshots/paseo-top-dashboard.jpg +0 -0
  55. package/screenshots/paseo-top-general-view.jpg +0 -0
  56. package/server/resources.ts +98 -0
  57. package/server/vendor/paseo-plugin-helper/custom-pills.ts +70 -0
  58. package/server/vendor/paseo-plugin-helper/index.ts +2 -0
  59. package/server/vendor/paseo-plugin-helper/logger.ts +5 -16
  60. package/server/vendor/paseo-plugin-helper/rpc-guard.ts +2 -2
  61. package/server/vendor/paseo-plugin-helper/storage-guard.ts +294 -0
  62. package/server/vendor/paseo-plugin-helper/storage.ts +36 -2
  63. package/server/vendor/paseo-plugin-helper/telemetry.ts +486 -0
  64. package/server/vendor/paseo-plugin-helper/tickets.ts +18 -67
  65. package/server/vendor/permission-audit/index.ts +1 -0
  66. package/server/vendor/permission-audit/server.ts +839 -0
  67. package/shared/helper-version.ts +1 -1
  68. package/shared/multi-host.ts +153 -159
  69. package/shared/resources.ts +145 -0
  70. package/shared/vendor/paseo-plugin-helper/async.ts +14 -7
  71. package/shared/vendor/paseo-plugin-helper/features.ts +244 -0
  72. package/shared/vendor/paseo-plugin-helper/forge.ts +27 -0
  73. package/shared/vendor/paseo-plugin-helper/index.ts +1 -0
  74. package/shared/vendor/paseo-plugin-helper/tickets.ts +122 -28
  75. package/shared/vendor/permission-audit/filter.ts +61 -0
  76. package/shared/vendor/permission-audit/index.ts +2 -0
  77. package/shared/vendor/permission-audit/shared.ts +119 -0
  78. package/shared/version.ts +1 -1
  79. package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +0 -493
  80. package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +0 -250
  81. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +0 -168
  82. package/client/vendor/paseo-plugin-helper/components/Button.tsx +0 -204
  83. package/client/vendor/paseo-plugin-helper/components/Card.tsx +0 -257
  84. package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +0 -196
  85. package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +0 -277
  86. package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +0 -172
  87. package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +0 -180
  88. package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +0 -97
  89. package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +0 -70
  90. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +0 -88
  91. package/client/vendor/paseo-plugin-helper/components/InteractiveRow.tsx +0 -142
  92. package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +0 -446
  93. package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +0 -247
  94. package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +0 -117
  95. package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +0 -53
  96. package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +0 -132
  97. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +0 -87
  98. package/client/vendor/paseo-plugin-helper/components/Select.tsx +0 -283
  99. package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +0 -80
  100. package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +0 -319
  101. package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +0 -150
  102. package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +0 -163
  103. package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +0 -157
  104. package/client/vendor/paseo-plugin-helper/components/index.ts +0 -26
  105. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +0 -225
  106. package/client/vendor/paseo-plugin-helper/forge-icon.tsx +0 -79
  107. package/client/vendor/paseo-plugin-helper/icon.tsx +0 -39
  108. package/client/vendor/paseo-plugin-helper/index.ts +0 -30
  109. package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +0 -49
  110. package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +0 -103
  111. package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +0 -65
  112. package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +0 -368
  113. package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +0 -51
  114. package/client/vendor/paseo-plugin-helper/layout/Row.tsx +0 -39
  115. package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +0 -39
  116. package/client/vendor/paseo-plugin-helper/layout/index.ts +0 -7
  117. package/client/vendor/paseo-plugin-helper/styles/index.ts +0 -1
  118. package/client/vendor/paseo-plugin-helper/styles/recipes.ts +0 -550
  119. package/client/vendor/paseo-plugin-helper/theme/flair.ts +0 -76
  120. package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +0 -121
  121. package/client/vendor/paseo-plugin-helper/theme/index.ts +0 -7
  122. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +0 -178
  123. package/client/vendor/paseo-plugin-helper/theme/responsive.ts +0 -213
  124. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +0 -200
  125. package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +0 -57
  126. package/client/vendor/paseo-plugin-helper/tickets.tsx +0 -766
  127. /package/client/vendor/paseo-plugin-helper/{command-center.ts → core/command-center.ts} +0 -0
  128. /package/client/vendor/paseo-plugin-helper/{snapshot.ts → core/snapshot.ts} +0 -0
  129. /package/client/vendor/paseo-plugin-helper/{utils → lifecycle}/haptics.ts +0 -0
@@ -1,277 +0,0 @@
1
- import React, { useState } from "react";
2
- import {
3
- Pressable,
4
- StyleSheet,
5
- Text,
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 type { SurfaceStyle } from "../theme/flair";
13
- import type { ThemeColors } from "../../../../shared/vendor/paseo-plugin-helper/types";
14
-
15
- export interface CollapsibleProps {
16
- title?: string | React.ReactNode;
17
- subtitle?: string | React.ReactNode;
18
- children: React.ReactNode;
19
- initiallyExpanded?: boolean;
20
- isExpanded?: boolean;
21
- onToggle?: (expanded: boolean) => void;
22
- badge?: React.ReactNode;
23
- headerRight?: React.ReactNode;
24
- summary?: React.ReactNode;
25
- icon?: string;
26
- style?: StyleProp<ViewStyle>;
27
- headerStyle?: StyleProp<ViewStyle>;
28
- contentStyle?: StyleProp<ViewStyle>;
29
- variant?: SurfaceStyle;
30
- }
31
-
32
- export function resolveCollapsibleChevron(expanded: boolean): string {
33
- return expanded ? "ChevronDown" : "ChevronRight";
34
- }
35
-
36
- /**
37
- * Container background for a Collapsible, honoring the same `SurfaceStyle`
38
- * contract as `Card`:
39
- * - "flat" (default): surface0, the normal page surface.
40
- * - "elevated": surface1, so the card visibly lifts off the page.
41
- * - "tinted": a faint accent wash.
42
- *
43
- * Before this existed, `variant="elevated"` only changed the border radius and
44
- * the container stayed `surface0` — so a card placed on an already-`surface0`
45
- * timeline read as a bleeding shaded band with no elevation (#208).
46
- */
47
- export function resolveCollapsibleSurface(
48
- colors: ThemeColors,
49
- alpha: (color: string, opacity: number) => string,
50
- variant: SurfaceStyle = "flat",
51
- ): { backgroundColor: string; borderColor: string } {
52
- switch (variant) {
53
- case "elevated":
54
- return { backgroundColor: colors.surface1, borderColor: colors.border };
55
- case "tinted":
56
- return {
57
- backgroundColor: alpha(colors.accent, 0.04),
58
- borderColor: alpha(colors.accent, 0.2),
59
- };
60
- case "flat":
61
- default:
62
- return { backgroundColor: colors.surface0, borderColor: colors.border };
63
- }
64
- }
65
-
66
- /**
67
- * Header stripe background. Kept for backwards compatibility; prefer
68
- * {@link resolveCollapsibleSurface} for the container and use this only for the
69
- * pressed/unpressed header delta.
70
- */
71
- export function resolveCollapsibleHeaderBackground(
72
- colors: ThemeColors,
73
- pressed: boolean,
74
- ): string {
75
- return pressed ? colors.surface1 : colors.surface0;
76
- }
77
-
78
- function renderTitle(title: string | React.ReactNode, titleStyle: object) {
79
- if (typeof title === "string" || title === undefined) {
80
- return title ? <Text style={titleStyle}>{title}</Text> : null;
81
- }
82
- return <View style={styles.titleSlot}>{title}</View>;
83
- }
84
-
85
- function renderSubtitle(subtitle: string | React.ReactNode, subtitleStyle: object) {
86
- if (typeof subtitle === "string") {
87
- return <Text style={subtitleStyle}>{subtitle}</Text>;
88
- }
89
- return subtitle ?? null;
90
- }
91
-
92
- export function Collapsible({
93
- title,
94
- subtitle,
95
- children,
96
- initiallyExpanded = false,
97
- isExpanded: controlledExpanded,
98
- onToggle,
99
- badge,
100
- headerRight,
101
- summary,
102
- icon,
103
- style,
104
- headerStyle,
105
- contentStyle,
106
- variant,
107
- }: CollapsibleProps) {
108
- const { Icon } = getClientHost();
109
- const { colors, resolveRadius, isCompact, touchTargetMin, alpha } = usePluginTheme();
110
- const [internalExpanded, setInternalExpanded] = useState(initiallyExpanded);
111
-
112
- const isExpanded = controlledExpanded !== undefined ? controlledExpanded : internalExpanded;
113
- const radius = resolveRadius(variant === "elevated" ? "lg" : "md");
114
- const surface = resolveCollapsibleSurface(colors, alpha, variant);
115
-
116
- const handlePress = () => {
117
- const next = !isExpanded;
118
- if (controlledExpanded === undefined) {
119
- setInternalExpanded(next);
120
- }
121
- onToggle?.(next);
122
- };
123
-
124
- const titleStyle = {
125
- color: colors.foreground,
126
- fontSize: isCompact ? 12 : 13,
127
- };
128
- const subtitleStyle = {
129
- color: colors.foregroundMuted,
130
- fontSize: isCompact ? 11 : 12,
131
- };
132
-
133
- return (
134
- <View
135
- style={[
136
- styles.container,
137
- {
138
- borderColor: surface.borderColor,
139
- borderRadius: radius,
140
- backgroundColor: surface.backgroundColor,
141
- },
142
- style,
143
- ]}
144
- >
145
- <Pressable
146
- onPress={handlePress}
147
- accessibilityRole="button"
148
- accessibilityState={{ expanded: isExpanded }}
149
- style={({ pressed }) => [
150
- styles.header,
151
- {
152
- minHeight: Math.max(touchTargetMin, 36),
153
- // Transparent by default: the container already carries the surface
154
- // color. Painting the header with an opaque surface color is what
155
- // made the first line read as a shaded band (#208). Only the
156
- // pressed state tints, as interaction feedback.
157
- backgroundColor: pressed ? alpha(colors.foreground, 0.06) : "transparent",
158
- cursor: "pointer" as never,
159
- },
160
- headerStyle,
161
- ]}
162
- >
163
- <View style={styles.headerLeft}>
164
- <View
165
- style={[
166
- styles.chevronBadge,
167
- {
168
- backgroundColor: alpha(colors.accent, 0.12),
169
- borderColor: alpha(colors.accent, 0.25),
170
- },
171
- ]}
172
- >
173
- <Icon
174
- name={resolveCollapsibleChevron(isExpanded)}
175
- size={14}
176
- color={colors.foregroundMuted}
177
- />
178
- </View>
179
- {icon && <Icon name={icon} size={14} color={colors.accent} />}
180
- <View style={styles.titleBlock}>
181
- <View style={styles.titleRow}>
182
- {renderTitle(title, [styles.title, titleStyle])}
183
- {badge && <View style={styles.badgeSlot}>{badge}</View>}
184
- </View>
185
- {subtitle ? (
186
- <View style={styles.subtitleSlot}>
187
- {renderSubtitle(subtitle, [styles.subtitle, subtitleStyle])}
188
- </View>
189
- ) : null}
190
- {summary ? <View style={styles.summarySlot}>{summary}</View> : null}
191
- </View>
192
- </View>
193
-
194
- {(headerRight || summary === undefined) && (
195
- <View style={styles.headerRight}>{headerRight}</View>
196
- )}
197
- </Pressable>
198
-
199
- {isExpanded && (
200
- <View
201
- style={[
202
- styles.content,
203
- { borderTopColor: colors.border, borderTopWidth: 1 },
204
- contentStyle,
205
- ]}
206
- >
207
- {children}
208
- </View>
209
- )}
210
- </View>
211
- );
212
- }
213
-
214
- const styles = StyleSheet.create({
215
- container: {
216
- borderWidth: 1,
217
- overflow: "hidden",
218
- },
219
- header: {
220
- flexDirection: "row",
221
- alignItems: "center",
222
- justifyContent: "space-between",
223
- paddingHorizontal: 12,
224
- paddingVertical: 8,
225
- },
226
- headerLeft: {
227
- flexDirection: "row",
228
- alignItems: "center",
229
- gap: 8,
230
- flex: 1,
231
- },
232
- chevronBadge: {
233
- width: 22,
234
- height: 22,
235
- borderRadius: 7,
236
- borderWidth: 1,
237
- alignItems: "center",
238
- justifyContent: "center",
239
- },
240
- titleBlock: {
241
- flex: 1,
242
- },
243
- titleRow: {
244
- flexDirection: "row",
245
- alignItems: "center",
246
- gap: 8,
247
- },
248
- titleSlot: {
249
- flexDirection: "row",
250
- alignItems: "center",
251
- },
252
- badgeSlot: {
253
- flexDirection: "row",
254
- alignItems: "center",
255
- },
256
- subtitleSlot: {
257
- marginTop: 2,
258
- },
259
- summarySlot: {
260
- marginTop: 4,
261
- flexDirection: "row",
262
- },
263
- headerRight: {
264
- flexDirection: "row",
265
- alignItems: "center",
266
- marginLeft: 8,
267
- },
268
- title: {
269
- fontWeight: "600",
270
- },
271
- subtitle: {
272
- fontWeight: "400",
273
- },
274
- content: {
275
- padding: 12,
276
- },
277
- });
@@ -1,172 +0,0 @@
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
- });
@@ -1,180 +0,0 @@
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
- });