@xpufx/paseo-top 0.4.4 → 0.4.7

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 (130) hide show
  1. package/OVERVIEW.md +18 -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 +692 -616
  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/npm-shrinkwrap.json +44 -0
  49. package/package.json +8 -4
  50. package/paseo-plugin.json +2 -0
  51. package/screenshots/1-paseo-top-modal-system.jpg +0 -0
  52. package/screenshots/2-paseo-top-modal-workspace.jpg +0 -0
  53. package/screenshots/3-paseo-top-modal-settings.jpg +0 -0
  54. package/screenshots/4-paseo-top-modal-about.jpg +0 -0
  55. package/screenshots/paseo-top-dashboard.jpg +0 -0
  56. package/screenshots/paseo-top-general-view.jpg +0 -0
  57. package/server/resources.ts +98 -0
  58. package/server/vendor/paseo-plugin-helper/custom-pills.ts +70 -0
  59. package/server/vendor/paseo-plugin-helper/index.ts +2 -0
  60. package/server/vendor/paseo-plugin-helper/logger.ts +5 -16
  61. package/server/vendor/paseo-plugin-helper/rpc-guard.ts +2 -2
  62. package/server/vendor/paseo-plugin-helper/storage-guard.ts +294 -0
  63. package/server/vendor/paseo-plugin-helper/storage.ts +36 -2
  64. package/server/vendor/paseo-plugin-helper/telemetry.ts +486 -0
  65. package/server/vendor/paseo-plugin-helper/tickets.ts +18 -67
  66. package/server/vendor/permission-audit/index.ts +1 -0
  67. package/server/vendor/permission-audit/server.ts +839 -0
  68. package/shared/helper-version.ts +1 -1
  69. package/shared/multi-host.ts +153 -159
  70. package/shared/resources.ts +145 -0
  71. package/shared/vendor/paseo-plugin-helper/async.ts +14 -7
  72. package/shared/vendor/paseo-plugin-helper/features.ts +244 -0
  73. package/shared/vendor/paseo-plugin-helper/forge.ts +27 -0
  74. package/shared/vendor/paseo-plugin-helper/index.ts +1 -0
  75. package/shared/vendor/paseo-plugin-helper/tickets.ts +122 -28
  76. package/shared/vendor/permission-audit/filter.ts +61 -0
  77. package/shared/vendor/permission-audit/index.ts +2 -0
  78. package/shared/vendor/permission-audit/shared.ts +119 -0
  79. package/shared/version.ts +1 -1
  80. package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +0 -493
  81. package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +0 -250
  82. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +0 -168
  83. package/client/vendor/paseo-plugin-helper/components/Button.tsx +0 -204
  84. package/client/vendor/paseo-plugin-helper/components/Card.tsx +0 -257
  85. package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +0 -196
  86. package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +0 -277
  87. package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +0 -172
  88. package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +0 -180
  89. package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +0 -97
  90. package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +0 -70
  91. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +0 -88
  92. package/client/vendor/paseo-plugin-helper/components/InteractiveRow.tsx +0 -142
  93. package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +0 -446
  94. package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +0 -247
  95. package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +0 -117
  96. package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +0 -53
  97. package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +0 -132
  98. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +0 -87
  99. package/client/vendor/paseo-plugin-helper/components/Select.tsx +0 -283
  100. package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +0 -80
  101. package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +0 -319
  102. package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +0 -150
  103. package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +0 -163
  104. package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +0 -157
  105. package/client/vendor/paseo-plugin-helper/components/index.ts +0 -26
  106. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +0 -225
  107. package/client/vendor/paseo-plugin-helper/forge-icon.tsx +0 -79
  108. package/client/vendor/paseo-plugin-helper/icon.tsx +0 -39
  109. package/client/vendor/paseo-plugin-helper/index.ts +0 -30
  110. package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +0 -49
  111. package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +0 -103
  112. package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +0 -65
  113. package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +0 -368
  114. package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +0 -51
  115. package/client/vendor/paseo-plugin-helper/layout/Row.tsx +0 -39
  116. package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +0 -39
  117. package/client/vendor/paseo-plugin-helper/layout/index.ts +0 -7
  118. package/client/vendor/paseo-plugin-helper/styles/index.ts +0 -1
  119. package/client/vendor/paseo-plugin-helper/styles/recipes.ts +0 -550
  120. package/client/vendor/paseo-plugin-helper/theme/flair.ts +0 -76
  121. package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +0 -121
  122. package/client/vendor/paseo-plugin-helper/theme/index.ts +0 -7
  123. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +0 -178
  124. package/client/vendor/paseo-plugin-helper/theme/responsive.ts +0 -213
  125. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +0 -200
  126. package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +0 -57
  127. package/client/vendor/paseo-plugin-helper/tickets.tsx +0 -766
  128. /package/client/vendor/paseo-plugin-helper/{command-center.ts → core/command-center.ts} +0 -0
  129. /package/client/vendor/paseo-plugin-helper/{snapshot.ts → core/snapshot.ts} +0 -0
  130. /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
- });