@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,250 +0,0 @@
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
- });
@@ -1,168 +0,0 @@
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
- accessibilityLabel={label}
116
- numberOfLines={1}
117
- ellipsizeMode="tail"
118
- style={[
119
- styles.text,
120
- {
121
- color: textColor,
122
- fontSize,
123
- lineHeight,
124
- textTransform: flair.headingTransform === "uppercase" ? "uppercase" : "none",
125
- },
126
- textStyle,
127
- ]}
128
- >
129
- {highlightQuery ? (
130
- <HighlightedText
131
- text={label}
132
- query={highlightQuery}
133
- fuzzyFallback={highlightFuzzyFallback}
134
- numberOfLines={1}
135
- />
136
- ) : (
137
- label
138
- )}
139
- </Text>
140
- </View>
141
- );
142
- }
143
-
144
- const styles = StyleSheet.create({
145
- badge: {
146
- flexDirection: "row",
147
- alignItems: "center",
148
- alignSelf: "flex-start",
149
- borderWidth: 1,
150
- gap: 4,
151
- // A badge carries operator-supplied strings (ids, paths, refs, model names)
152
- // of unbounded length. Yoga defaults `flexShrink` to 0, so without this the
153
- // chip insists on its full text width and pushes its row past the viewport
154
- // instead of truncating.
155
- flexShrink: 1,
156
- maxWidth: "100%",
157
- },
158
- text: {
159
- fontWeight: "600",
160
- // Truncation only engages if the label itself is allowed to compress.
161
- flexShrink: 1,
162
- },
163
- dot: {
164
- width: 6,
165
- height: 6,
166
- borderRadius: 3,
167
- },
168
- });
@@ -1,204 +0,0 @@
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
- children?: ReactNode;
37
- variant?: ButtonVariant;
38
- size?: ButtonSize;
39
- icon?: string | ReactNode;
40
- iconPosition?: "left" | "right";
41
- onPress?: () => void | Promise<void>;
42
- disabled?: boolean;
43
- loading?: boolean;
44
- style?: StyleProp<ViewStyle>;
45
- textStyle?: StyleProp<TextStyle>;
46
- accessibilityLabel?: string;
47
- accessibilityRole?: "button" | "link";
48
- attention?: ButtonAttention;
49
- }
50
-
51
- export function Button({
52
- label,
53
- children,
54
- variant = "secondary",
55
- size = "md",
56
- icon,
57
- iconPosition = "left",
58
- onPress,
59
- disabled = false,
60
- loading = false,
61
- style,
62
- textStyle,
63
- accessibilityLabel,
64
- accessibilityRole = "button",
65
- attention,
66
- }: ButtonProps) {
67
- const { Icon } = getClientHost();
68
- const { colors, resolveRadius, touchTargetMin, isCompact, alpha } = usePluginTheme();
69
-
70
- const radius = resolveRadius(size === "sm" ? "sm" : size === "lg" ? "lg" : "md");
71
-
72
- // Determine sizing
73
- const py = size === "sm" ? (isCompact ? 5 : 6) : size === "lg" ? 12 : isCompact ? 8 : 10;
74
- const px = size === "sm" ? (isCompact ? 8 : 10) : size === "lg" ? 18 : isCompact ? 12 : 14;
75
- const fontSize = size === "sm" ? 12 : size === "lg" ? 15 : 13;
76
- const iconSize = size === "sm" ? 12 : size === "lg" ? 16 : 14;
77
-
78
- // Determine colors by variant
79
- let bg = "transparent";
80
- let border = "transparent";
81
- let textColor = colors.foreground;
82
-
83
- switch (variant) {
84
- case "primary":
85
- bg = colors.accent;
86
- textColor = colors.accentForeground || FALLBACK_ACCENT_FOREGROUND;
87
- break;
88
- case "danger":
89
- bg = alpha(colors.statusDanger, 0.15);
90
- border = alpha(colors.statusDanger, 0.4);
91
- textColor = colors.statusDanger;
92
- break;
93
- case "ghost":
94
- bg = "transparent";
95
- textColor = colors.foregroundMuted;
96
- break;
97
- case "secondary":
98
- default:
99
- bg = colors.surface1;
100
- border = colors.border;
101
- textColor = colors.foreground;
102
- break;
103
- }
104
-
105
- const renderIcon = () => {
106
- if (!icon) return null;
107
- if (typeof icon === "string") {
108
- return <Icon name={icon} size={iconSize} color={textColor} />;
109
- }
110
- return icon;
111
- };
112
-
113
- const attentionMode = resolveButtonAttentionMode(attention);
114
-
115
- // Accessibility touch targets are met with `hitSlop`, which expands the
116
- // tappable area without moving a single neighbouring pixel. They are NOT met
117
- // by inflating the painted box: a `minHeight` floor forced `size="sm"` to
118
- // render 40px tall on mobile against a ~26px recipe, because the floor was
119
- // computed from the touch target rather than from the size (#647).
120
- //
121
- // The slop has to be derived from this button's own height, not a fixed 32 —
122
- // a 32px baseline silently under-serves every smaller recipe, so `sm` got
123
- // both a too-tall box and too little slop.
124
- const contentHeight = Math.round(fontSize * 1.2) + py * 2;
125
- const hitSlopSide = Math.max(0, (touchTargetMin - contentHeight) / 2);
126
-
127
- const pressable = (
128
- <Pressable
129
- onPress={onPress}
130
- disabled={disabled || loading}
131
- accessibilityRole={accessibilityRole}
132
- accessibilityLabel={accessibilityLabel || label}
133
- hitSlop={hitSlopSide}
134
- style={({ pressed }) => [
135
- styles.base,
136
- {
137
- backgroundColor: pressed && !disabled ? alpha(bg, 0.8) : bg,
138
- borderColor: border,
139
- borderWidth: border !== "transparent" ? 1 : 0,
140
- borderRadius: radius,
141
- paddingVertical: py,
142
- paddingHorizontal: px,
143
- opacity: disabled ? 0.45 : 1,
144
- },
145
- style,
146
- ]}
147
- >
148
- {loading ? (
149
- <ActivityIndicator size="small" color={textColor} />
150
- ) : children ? (
151
- children
152
- ) : (
153
- <>
154
- {iconPosition === "left" && renderIcon()}
155
- {label ? (
156
- <Text
157
- numberOfLines={1}
158
- ellipsizeMode="tail"
159
- style={[
160
- styles.text,
161
- {
162
- color: textColor,
163
- fontSize,
164
- },
165
- textStyle,
166
- ]}
167
- >
168
- {label}
169
- </Text>
170
- ) : null}
171
- {iconPosition === "right" && renderIcon()}
172
- </>
173
- )}
174
- </Pressable>
175
- );
176
-
177
- if (!attentionMode) return pressable;
178
-
179
- return (
180
- <AttentionBeacon mode={attentionMode} tone={resolveButtonAttentionTone(variant)}>
181
- {pressable}
182
- </AttentionBeacon>
183
- );
184
- }
185
-
186
- const styles = StyleSheet.create({
187
- base: {
188
- flexDirection: "row",
189
- alignItems: "center",
190
- justifyContent: "center",
191
- gap: 6,
192
- // Labels are caller-supplied and unbounded (issue titles, branch names,
193
- // sort labels). Yoga defaults `flexShrink` to 0, so an unconstrained
194
- // button claims its full label width and overflows narrow viewports.
195
- flexShrink: 1,
196
- maxWidth: "100%",
197
- },
198
- text: {
199
- fontWeight: "600",
200
- textAlign: "center",
201
- // The label must be allowed to compress for `numberOfLines` to engage.
202
- flexShrink: 1,
203
- },
204
- });