@xpufx/paseo-x-comms 0.3.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 (152) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +149 -0
  3. package/client/attribution.ts +7 -0
  4. package/client/conversations.ts +229 -0
  5. package/client/main.tsx +885 -0
  6. package/client/peer-label.ts +98 -0
  7. package/client/peer-status.tsx +201 -0
  8. package/client/settings-prototype.tsx +395 -0
  9. package/client/tool-call.ts +115 -0
  10. package/client/vendor/paseo-plugin-helper/command-center.ts +43 -0
  11. package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +493 -0
  12. package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +250 -0
  13. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +156 -0
  14. package/client/vendor/paseo-plugin-helper/components/Button.tsx +178 -0
  15. package/client/vendor/paseo-plugin-helper/components/Card.tsx +225 -0
  16. package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +196 -0
  17. package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +277 -0
  18. package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +172 -0
  19. package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +180 -0
  20. package/client/vendor/paseo-plugin-helper/components/DataTable.tsx +200 -0
  21. package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +97 -0
  22. package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +70 -0
  23. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +73 -0
  24. package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +446 -0
  25. package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +247 -0
  26. package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +117 -0
  27. package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +53 -0
  28. package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +118 -0
  29. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +80 -0
  30. package/client/vendor/paseo-plugin-helper/components/Select.tsx +215 -0
  31. package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +80 -0
  32. package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +319 -0
  33. package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +150 -0
  34. package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +163 -0
  35. package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +157 -0
  36. package/client/vendor/paseo-plugin-helper/components/index.ts +25 -0
  37. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +224 -0
  38. package/client/vendor/paseo-plugin-helper/forge-icon.tsx +79 -0
  39. package/client/vendor/paseo-plugin-helper/host.ts +277 -0
  40. package/client/vendor/paseo-plugin-helper/icon.tsx +39 -0
  41. package/client/vendor/paseo-plugin-helper/index.ts +28 -0
  42. package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +49 -0
  43. package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +103 -0
  44. package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +65 -0
  45. package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +378 -0
  46. package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +49 -0
  47. package/client/vendor/paseo-plugin-helper/layout/Row.tsx +39 -0
  48. package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +39 -0
  49. package/client/vendor/paseo-plugin-helper/layout/index.ts +7 -0
  50. package/client/vendor/paseo-plugin-helper/panel.tsx +81 -0
  51. package/client/vendor/paseo-plugin-helper/pill.tsx +884 -0
  52. package/client/vendor/paseo-plugin-helper/query-refresh.ts +79 -0
  53. package/client/vendor/paseo-plugin-helper/query.ts +66 -0
  54. package/client/vendor/paseo-plugin-helper/settings-screen.tsx +372 -0
  55. package/client/vendor/paseo-plugin-helper/settings.ts +181 -0
  56. package/client/vendor/paseo-plugin-helper/shared-settings.ts +46 -0
  57. package/client/vendor/paseo-plugin-helper/snapshot.ts +68 -0
  58. package/client/vendor/paseo-plugin-helper/surface.tsx +80 -0
  59. package/client/vendor/paseo-plugin-helper/theme/color-utils.ts +118 -0
  60. package/client/vendor/paseo-plugin-helper/theme/flair.ts +76 -0
  61. package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +121 -0
  62. package/client/vendor/paseo-plugin-helper/theme/index.ts +7 -0
  63. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +214 -0
  64. package/client/vendor/paseo-plugin-helper/theme/responsive.ts +213 -0
  65. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +161 -0
  66. package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +57 -0
  67. package/client/vendor/paseo-plugin-helper/utils/clipboard.ts +149 -0
  68. package/client/vendor/paseo-plugin-helper/utils/haptics.ts +34 -0
  69. package/client/via-x-comms.tsx +15 -0
  70. package/client/x-comms-conversation.tsx +449 -0
  71. package/client/x-comms-panel.tsx +6 -0
  72. package/client/x-comms-pill.tsx +193 -0
  73. package/client/x-comms-timeline.tsx +176 -0
  74. package/client/x-comms-tool-call.tsx +85 -0
  75. package/docs/mesh.md +115 -0
  76. package/mcp/LICENSE +202 -0
  77. package/mcp/README.md +287 -0
  78. package/mcp/mcp-config.example.json +8 -0
  79. package/mcp/package-lock.json +1186 -0
  80. package/mcp/package.json +43 -0
  81. package/mcp/paseo-cross-daemon-comms.example.json +5 -0
  82. package/mcp/paseo-x-comms.bundled.mjs +36964 -0
  83. package/mcp/paseo-x-comms.mjs +630 -0
  84. package/mcp/test/fixtures/extensions/block/10-block.mjs +4 -0
  85. package/mcp/test/fixtures/extensions/broken-hook/10-thrower.mjs +6 -0
  86. package/mcp/test/fixtures/extensions/broken-hook/20-good.mjs +4 -0
  87. package/mcp/test/fixtures/extensions/broken-load/10-thrower.mjs +4 -0
  88. package/mcp/test/fixtures/extensions/broken-load/20-good.mjs +4 -0
  89. package/mcp/test/fixtures/extensions/custom-tool/10-tool.mjs +12 -0
  90. package/mcp/test/fixtures/extensions/tool-block/10-block.mjs +8 -0
  91. package/mcp/test/fixtures/extensions/transform/10-transform.mjs +15 -0
  92. package/mcp/test/fixtures/fake-paseo.mjs +92 -0
  93. package/mcp/test/register-ts-hooks.mjs +6 -0
  94. package/mcp/test/resolve-ts-hooks.mjs +46 -0
  95. package/package.json +58 -0
  96. package/paseo-plugin.json +7 -0
  97. package/server/conversations-snapshot.ts +249 -0
  98. package/server/handlers.ts +1081 -0
  99. package/server/injection.ts +152 -0
  100. package/server/local-send.ts +153 -0
  101. package/server/mcp-client.ts +47 -0
  102. package/server/outbox.ts +233 -0
  103. package/server/peer-channel.ts +151 -0
  104. package/server/peer-status.ts +233 -0
  105. package/server/presence.ts +204 -0
  106. package/server/registry.ts +215 -0
  107. package/server/relay-status.ts +9 -0
  108. package/server/server-status.ts +138 -0
  109. package/server/settings.ts +81 -0
  110. package/server/snapshot.ts +176 -0
  111. package/server/vendor/paseo-plugin-helper/agent.ts +85 -0
  112. package/server/vendor/paseo-plugin-helper/custom-pills.ts +344 -0
  113. package/server/vendor/paseo-plugin-helper/index.ts +18 -0
  114. package/server/vendor/paseo-plugin-helper/jsonc.ts +78 -0
  115. package/server/vendor/paseo-plugin-helper/logger.ts +210 -0
  116. package/server/vendor/paseo-plugin-helper/mcp/client.ts +349 -0
  117. package/server/vendor/paseo-plugin-helper/mcp/http-client.ts +399 -0
  118. package/server/vendor/paseo-plugin-helper/mcp/index.ts +5 -0
  119. package/server/vendor/paseo-plugin-helper/mcp/process-killer.ts +65 -0
  120. package/server/vendor/paseo-plugin-helper/mcp/ring-buffer.ts +29 -0
  121. package/server/vendor/paseo-plugin-helper/mcp/types.ts +41 -0
  122. package/server/vendor/paseo-plugin-helper/mcp-config.ts +367 -0
  123. package/server/vendor/paseo-plugin-helper/mcp-injection.ts +85 -0
  124. package/server/vendor/paseo-plugin-helper/network.ts +91 -0
  125. package/server/vendor/paseo-plugin-helper/plugins.ts +160 -0
  126. package/server/vendor/paseo-plugin-helper/process.ts +186 -0
  127. package/server/vendor/paseo-plugin-helper/redact.ts +86 -0
  128. package/server/vendor/paseo-plugin-helper/rpc-guard.ts +77 -0
  129. package/server/vendor/paseo-plugin-helper/settings.ts +97 -0
  130. package/server/vendor/paseo-plugin-helper/shared-settings.ts +243 -0
  131. package/server/vendor/paseo-plugin-helper/storage.ts +244 -0
  132. package/server/vendor/paseo-plugin-helper/system.ts +128 -0
  133. package/server/vendor/paseo-plugin-helper/task.ts +116 -0
  134. package/server/vendor/paseo-plugin-helper/version.ts +153 -0
  135. package/server/vendor/paseo-plugin-helper/workspace-beacon.ts +418 -0
  136. package/shared/conversations-snapshot.ts +39 -0
  137. package/shared/envelope.ts +92 -0
  138. package/shared/outbox.ts +21 -0
  139. package/shared/registry.ts +366 -0
  140. package/shared/vendor/paseo-plugin-helper/README.md +11 -0
  141. package/shared/vendor/paseo-plugin-helper/async.ts +35 -0
  142. package/shared/vendor/paseo-plugin-helper/custom-pills.ts +169 -0
  143. package/shared/vendor/paseo-plugin-helper/forge.ts +110 -0
  144. package/shared/vendor/paseo-plugin-helper/formatters.ts +271 -0
  145. package/shared/vendor/paseo-plugin-helper/highlight.ts +184 -0
  146. package/shared/vendor/paseo-plugin-helper/index.ts +10 -0
  147. package/shared/vendor/paseo-plugin-helper/rpc.ts +72 -0
  148. package/shared/vendor/paseo-plugin-helper/settings.ts +138 -0
  149. package/shared/vendor/paseo-plugin-helper/suite-settings.ts +17 -0
  150. package/shared/vendor/paseo-plugin-helper/suppressed.ts +31 -0
  151. package/shared/vendor/paseo-plugin-helper/types.ts +36 -0
  152. package/shared/version.ts +2 -0
@@ -0,0 +1,247 @@
1
+ import React, { type ReactNode } from "react";
2
+ import { Platform, StyleSheet, Text, View, type StyleProp, type ViewStyle } from "react-native";
3
+ import { usePluginTheme } from "../theme/provider";
4
+ import { resolveMetricStatus, type MetricThresholds } from "../../../../shared/vendor/paseo-plugin-helper/formatters";
5
+
6
+ export interface MetricGaugeProps {
7
+ value: number; // 0 - 100
8
+ size?: number;
9
+ strokeWidth?: number;
10
+ thresholds?: MetricThresholds;
11
+ color?: string;
12
+ autoStatusColor?: boolean;
13
+ label?: string;
14
+ showPercent?: boolean;
15
+ centerSlot?: ReactNode;
16
+ style?: StyleProp<ViewStyle>;
17
+ }
18
+
19
+ /**
20
+ * Clean circular metric gauge.
21
+ * Displays a proportional percentage ring with automated threshold coloring and center slot.
22
+ */
23
+ export function MetricGauge({
24
+ value,
25
+ size = 76,
26
+ strokeWidth = 7,
27
+ thresholds,
28
+ color,
29
+ autoStatusColor = true,
30
+ label,
31
+ showPercent = true,
32
+ centerSlot,
33
+ style,
34
+ }: MetricGaugeProps) {
35
+ const { colors } = usePluginTheme();
36
+
37
+ const clamped = Math.max(0, Math.min(100, Number.isFinite(value) ? value : 0));
38
+
39
+ let gaugeColor = color || colors.accent;
40
+ if (!color && autoStatusColor) {
41
+ const status = resolveMetricStatus(clamped, thresholds);
42
+ if (status === "danger") {
43
+ gaugeColor = colors.statusDanger;
44
+ } else if (status === "warning") {
45
+ gaugeColor = colors.statusWarning;
46
+ } else {
47
+ gaugeColor = colors.statusSuccess;
48
+ }
49
+ }
50
+
51
+ const radius = size / 2;
52
+ const innerSize = Math.max(0, size - strokeWidth * 2);
53
+ const innerRadius = innerSize / 2;
54
+ const trackColor = colors.surface2;
55
+
56
+ // Web & React Native Web: Conic gradient provides a pixel-perfect proportional ring arc
57
+ if (Platform.OS === "web") {
58
+ const webBackground = `conic-gradient(${gaugeColor} 0% ${clamped}%, ${trackColor} ${clamped}% 100%)`;
59
+
60
+ return (
61
+ <View style={[styles.wrapper, style]}>
62
+ <View
63
+ style={[
64
+ styles.gaugeBox,
65
+ {
66
+ width: size,
67
+ height: size,
68
+ borderRadius: radius,
69
+ },
70
+ ({ background: webBackground } as any),
71
+ ]}
72
+ >
73
+ {/* Inner cutout mask */}
74
+ <View
75
+ style={[
76
+ styles.centerHole,
77
+ {
78
+ width: innerSize,
79
+ height: innerSize,
80
+ borderRadius: innerRadius,
81
+ backgroundColor: colors.surface0,
82
+ },
83
+ ]}
84
+ >
85
+ {centerSlot ? (
86
+ centerSlot
87
+ ) : showPercent ? (
88
+ <Text style={[styles.percentText, { color: colors.foreground }]}>
89
+ {Math.round(clamped)}%
90
+ </Text>
91
+ ) : null}
92
+ </View>
93
+ </View>
94
+
95
+ {label ? (
96
+ <Text style={[styles.labelText, { color: colors.foregroundMuted }]}>
97
+ {label}
98
+ </Text>
99
+ ) : null}
100
+ </View>
101
+ );
102
+ }
103
+
104
+ // Native (iOS/Android): Two-semicircle clipping approach
105
+ const firstHalfRotation = Math.min(180, clamped * 3.6);
106
+ const secondHalfRotation = clamped > 50 ? (clamped - 50) * 3.6 : 0;
107
+
108
+ return (
109
+ <View style={[styles.wrapper, style]}>
110
+ <View style={[styles.gaugeBox, { width: size, height: size }]}>
111
+ {/* Background track circle */}
112
+ <View
113
+ style={[
114
+ StyleSheet.absoluteFillObject,
115
+ {
116
+ borderRadius: radius,
117
+ borderWidth: strokeWidth,
118
+ borderColor: trackColor,
119
+ },
120
+ ]}
121
+ />
122
+
123
+ {/* First 180 degrees */}
124
+ <View
125
+ style={[
126
+ styles.halfCircleContainer,
127
+ {
128
+ width: size,
129
+ height: size,
130
+ transform: [{ rotate: `${firstHalfRotation}deg` }],
131
+ },
132
+ ]}
133
+ >
134
+ <View
135
+ style={[
136
+ styles.halfCircle,
137
+ {
138
+ width: size,
139
+ height: size,
140
+ borderRadius: radius,
141
+ borderWidth: strokeWidth,
142
+ borderColor: gaugeColor,
143
+ borderBottomColor: "transparent",
144
+ borderLeftColor: "transparent",
145
+ },
146
+ ]}
147
+ />
148
+ </View>
149
+
150
+ {/* Second 180 degrees */}
151
+ {clamped > 50 ? (
152
+ <View
153
+ style={[
154
+ styles.halfCircleContainer,
155
+ {
156
+ width: size,
157
+ height: size,
158
+ transform: [{ rotate: `${secondHalfRotation + 180}deg` }],
159
+ },
160
+ ]}
161
+ >
162
+ <View
163
+ style={[
164
+ styles.halfCircle,
165
+ {
166
+ width: size,
167
+ height: size,
168
+ borderRadius: radius,
169
+ borderWidth: strokeWidth,
170
+ borderColor: gaugeColor,
171
+ borderBottomColor: "transparent",
172
+ borderLeftColor: "transparent",
173
+ },
174
+ ]}
175
+ />
176
+ </View>
177
+ ) : null}
178
+
179
+ {/* Center Content Slot */}
180
+ <View
181
+ style={[
182
+ styles.centerHole,
183
+ {
184
+ width: innerSize,
185
+ height: innerSize,
186
+ borderRadius: innerRadius,
187
+ backgroundColor: colors.surface0,
188
+ },
189
+ ]}
190
+ >
191
+ {centerSlot ? (
192
+ centerSlot
193
+ ) : showPercent ? (
194
+ <Text style={[styles.percentText, { color: colors.foreground }]}>
195
+ {Math.round(clamped)}%
196
+ </Text>
197
+ ) : null}
198
+ </View>
199
+ </View>
200
+
201
+ {label ? (
202
+ <Text style={[styles.labelText, { color: colors.foregroundMuted }]}>
203
+ {label}
204
+ </Text>
205
+ ) : null}
206
+ </View>
207
+ );
208
+ }
209
+
210
+ const styles = StyleSheet.create({
211
+ wrapper: {
212
+ alignItems: "center",
213
+ justifyContent: "center",
214
+ gap: 6,
215
+ },
216
+ gaugeBox: {
217
+ position: "relative",
218
+ alignItems: "center",
219
+ justifyContent: "center",
220
+ overflow: "hidden",
221
+ },
222
+ centerHole: {
223
+ position: "absolute",
224
+ alignItems: "center",
225
+ justifyContent: "center",
226
+ zIndex: 10,
227
+ },
228
+ halfCircleContainer: {
229
+ position: "absolute",
230
+ top: 0,
231
+ left: 0,
232
+ overflow: "hidden",
233
+ },
234
+ halfCircle: {
235
+ position: "absolute",
236
+ top: 0,
237
+ left: 0,
238
+ },
239
+ percentText: {
240
+ fontSize: 13,
241
+ fontWeight: "700",
242
+ },
243
+ labelText: {
244
+ fontSize: 11,
245
+ fontWeight: "500",
246
+ },
247
+ });
@@ -0,0 +1,117 @@
1
+ import React from "react";
2
+ import { StyleSheet, Text, View, type StyleProp, type ViewStyle } from "react-native";
3
+ import { usePluginTheme } from "../theme/provider";
4
+ import { resolveMetricStatus, type MetricThresholds } from "../../../../shared/vendor/paseo-plugin-helper/formatters";
5
+
6
+ export interface ProgressBarProps {
7
+ value: number; // 0 to 100
8
+ color?: string;
9
+ autoStatusColor?: boolean;
10
+ thresholds?: MetricThresholds;
11
+ label?: string;
12
+ showValueText?: boolean;
13
+ height?: number;
14
+ style?: StyleProp<ViewStyle>;
15
+ }
16
+
17
+ export function ProgressBar({
18
+ value,
19
+ color,
20
+ autoStatusColor = true,
21
+ thresholds,
22
+ label,
23
+ showValueText = false,
24
+ height = 8,
25
+ style,
26
+ }: ProgressBarProps) {
27
+ const { colors, resolveRadius, typography } = usePluginTheme();
28
+
29
+ const clamped = Math.max(0, Math.min(100, Number.isFinite(value) ? value : 0));
30
+ const radius = resolveRadius("pill");
31
+
32
+ let barColor = color || colors.accent;
33
+ if (!color && autoStatusColor) {
34
+ const status = resolveMetricStatus(clamped, thresholds);
35
+ if (status === "danger") {
36
+ barColor = colors.statusDanger;
37
+ } else if (status === "warning") {
38
+ barColor = colors.statusWarning;
39
+ } else {
40
+ barColor = colors.statusSuccess;
41
+ }
42
+ }
43
+
44
+ return (
45
+ <View style={[styles.container, style]}>
46
+ {(label || showValueText) && (
47
+ <View style={styles.labelRow}>
48
+ {label ? (
49
+ <Text
50
+ style={[
51
+ styles.labelText,
52
+ { color: colors.foregroundMuted, ...typography.caption },
53
+ ]}
54
+ >
55
+ {label}
56
+ </Text>
57
+ ) : null}
58
+ {showValueText ? (
59
+ <Text
60
+ style={[
61
+ styles.valueText,
62
+ { color: colors.foreground, ...typography.caption, fontWeight: "600" },
63
+ ]}
64
+ >
65
+ {Math.round(clamped)}%
66
+ </Text>
67
+ ) : null}
68
+ </View>
69
+ )}
70
+ <View
71
+ style={[
72
+ styles.track,
73
+ {
74
+ backgroundColor: colors.surface2,
75
+ height,
76
+ borderRadius: radius,
77
+ },
78
+ ]}
79
+ >
80
+ <View
81
+ style={[
82
+ styles.fill,
83
+ {
84
+ width: `${clamped}%`,
85
+ backgroundColor: barColor,
86
+ borderRadius: radius,
87
+ },
88
+ ]}
89
+ />
90
+ </View>
91
+ </View>
92
+ );
93
+ }
94
+
95
+ const styles = StyleSheet.create({
96
+ container: {
97
+ gap: 4,
98
+ },
99
+ labelRow: {
100
+ flexDirection: "row",
101
+ justifyContent: "space-between",
102
+ alignItems: "center",
103
+ },
104
+ labelText: {
105
+ fontWeight: "500",
106
+ },
107
+ valueText: {
108
+ fontWeight: "600",
109
+ },
110
+ track: {
111
+ width: "100%",
112
+ overflow: "hidden",
113
+ },
114
+ fill: {
115
+ height: "100%",
116
+ },
117
+ });
@@ -0,0 +1,53 @@
1
+ import React, { type ReactNode } from "react";
2
+ import { useResponsive, type UseResponsiveResult } from "../theme/useResponsive";
3
+
4
+ export interface ResponsiveProps {
5
+ /**
6
+ * Content to render on desktop / wide viewports.
7
+ */
8
+ desktop?: ReactNode;
9
+ /**
10
+ * Content to render on mobile platforms ('ios' | 'android').
11
+ */
12
+ mobile?: ReactNode;
13
+ /**
14
+ * Content to render when in compact mode (narrow trackbar, mobile bottom sheet, or split view).
15
+ */
16
+ compact?: ReactNode;
17
+ /**
18
+ * Content to render when in wide mode (not compact).
19
+ */
20
+ wide?: ReactNode;
21
+ /**
22
+ * Render function taking `UseResponsiveResult` or children.
23
+ */
24
+ children?: ReactNode | ((responsive: UseResponsiveResult) => ReactNode);
25
+ }
26
+
27
+ /**
28
+ * Declarative component for rendering different UI elements across desktop, mobile, and compact layouts.
29
+ *
30
+ * @example
31
+ * ```tsx
32
+ * <Responsive
33
+ * desktop={<DataTable columns={["ID", "Name", "Status", "Latency"]} data={items} />}
34
+ * mobile={<DataTable columns={["Name", "Status"]} data={items} />}
35
+ * />
36
+ * ```
37
+ */
38
+ export function Responsive({ desktop, mobile, compact, wide, children }: ResponsiveProps) {
39
+ const responsive = useResponsive();
40
+
41
+ if (typeof children === "function") {
42
+ return <>{children(responsive)}</>;
43
+ }
44
+
45
+ const selected = responsive.select<ReactNode>({
46
+ desktop,
47
+ mobile,
48
+ compact,
49
+ wide,
50
+ });
51
+
52
+ return <>{selected ?? children ?? null}</>;
53
+ }
@@ -0,0 +1,118 @@
1
+ import React, { type ComponentType, type Ref } from "react";
2
+ import {
3
+ StyleSheet,
4
+ TextInput as RNTextInput,
5
+ View,
6
+ Pressable,
7
+ type StyleProp,
8
+ type ViewStyle,
9
+ type TextStyle,
10
+ type TextInputProps as RNTextInputProps,
11
+ type TextInput as RNTextInputInstance,
12
+ } from "react-native";
13
+ import { getClientHost, getOptionalClientHost } from "../host";
14
+ import { usePluginTheme } from "../theme/provider";
15
+
16
+ export interface SearchInputProps {
17
+ value: string;
18
+ onChangeText: (text: string) => void;
19
+ placeholder?: string;
20
+ onClear?: () => void;
21
+ style?: StyleProp<ViewStyle>;
22
+ inputStyle?: StyleProp<TextStyle>;
23
+ testID?: string;
24
+ }
25
+
26
+ /**
27
+ * Standardized search input with search icon, clear button, and theme support.
28
+ */
29
+ export function SearchInput({
30
+ value,
31
+ onChangeText,
32
+ placeholder = "Search...",
33
+ onClear,
34
+ style,
35
+ inputStyle,
36
+ testID,
37
+ }: SearchInputProps) {
38
+ const { Icon } = getClientHost();
39
+ const ResolvedInput = (getOptionalClientHost()?.TextInput ??
40
+ RNTextInput) as ComponentType<RNTextInputProps & { ref?: Ref<RNTextInputInstance> }>;
41
+ const { colors, resolveRadius, isCompact } = usePluginTheme();
42
+ const radius = resolveRadius("sm");
43
+
44
+ const handleClear = () => {
45
+ onChangeText("");
46
+ if (onClear) onClear();
47
+ };
48
+
49
+ return (
50
+ <View
51
+ style={[
52
+ styles.container,
53
+ {
54
+ backgroundColor: colors.surface1,
55
+ borderColor: colors.border,
56
+ borderRadius: radius,
57
+ height: isCompact ? 36 : 40,
58
+ },
59
+ style,
60
+ ]}
61
+ >
62
+ <View style={styles.iconWrapper}>
63
+ <Icon name="Search" size={16} color={colors.foregroundMuted} />
64
+ </View>
65
+ <ResolvedInput
66
+ testID={testID}
67
+ value={value}
68
+ onChangeText={onChangeText}
69
+ placeholder={placeholder}
70
+ placeholderTextColor={colors.foregroundMuted}
71
+ style={[
72
+ styles.input,
73
+ {
74
+ color: colors.foreground,
75
+ fontSize: isCompact ? 13 : 14,
76
+ },
77
+ inputStyle,
78
+ ]}
79
+ returnKeyType="search"
80
+ autoCapitalize="none"
81
+ autoCorrect={false}
82
+ />
83
+ {Boolean(value) && (
84
+ <Pressable
85
+ onPress={handleClear}
86
+ style={styles.clearButton}
87
+ hitSlop={8}
88
+ accessibilityLabel="Clear search"
89
+ >
90
+ <Icon name="X" size={14} color={colors.foregroundMuted} />
91
+ </Pressable>
92
+ )}
93
+ </View>
94
+ );
95
+ }
96
+
97
+ const styles = StyleSheet.create({
98
+ container: {
99
+ flexDirection: "row",
100
+ alignItems: "center",
101
+ borderWidth: 1,
102
+ paddingHorizontal: 10,
103
+ },
104
+ iconWrapper: {
105
+ marginRight: 8,
106
+ alignItems: "center",
107
+ justifyContent: "center",
108
+ },
109
+ input: {
110
+ flex: 1,
111
+ paddingVertical: 0,
112
+ outlineWidth: 0,
113
+ },
114
+ clearButton: {
115
+ padding: 4,
116
+ marginLeft: 4,
117
+ },
118
+ });
@@ -0,0 +1,80 @@
1
+ import React from "react";
2
+ import {
3
+ StyleSheet,
4
+ Text,
5
+ View,
6
+ type StyleProp,
7
+ type TextStyle,
8
+ type ViewStyle,
9
+ } from "react-native";
10
+ import { usePluginTheme } from "../theme/provider";
11
+ import type { StatusVariant } from "../../../../shared/vendor/paseo-plugin-helper/types";
12
+ import { Badge } from "./Badge";
13
+
14
+ export interface SectionHeaderProps {
15
+ title: string;
16
+ /** Optional count — shown as a Badge (warning variant when > 0, neutral otherwise) */
17
+ count?: number;
18
+ /** Badge variant override */
19
+ badgeVariant?: StatusVariant;
20
+ style?: StyleProp<ViewStyle>;
21
+ textStyle?: StyleProp<TextStyle>;
22
+ }
23
+
24
+ export function SectionHeader({
25
+ title,
26
+ count,
27
+ badgeVariant,
28
+ style,
29
+ textStyle,
30
+ }: SectionHeaderProps): React.ReactElement | null {
31
+ const theme = usePluginTheme();
32
+ const titleType = theme.typography?.bodyStrong ?? {
33
+ fontSize: 13,
34
+ lineHeight: 19,
35
+ fontWeight: "600" as const,
36
+ };
37
+ const headingTransform = (theme.flair?.headingTransform ?? "none") === "uppercase"
38
+ ? "uppercase"
39
+ : "none";
40
+
41
+ return (
42
+ <View style={[styles.container, style]}>
43
+ <Text
44
+ style={[
45
+ styles.title,
46
+ {
47
+ color: theme.colors.foregroundMuted,
48
+ textTransform: headingTransform,
49
+ fontSize: titleType.fontSize,
50
+ lineHeight: titleType.lineHeight,
51
+ fontWeight: titleType.fontWeight,
52
+ },
53
+ textStyle,
54
+ ]}
55
+ >
56
+ {title}
57
+ </Text>
58
+ {count !== undefined ? (
59
+ <Badge
60
+ label={String(count)}
61
+ variant={badgeVariant ?? (count > 0 ? "warning" : "neutral")}
62
+ styleVariant={count > 0 ? "solid" : "tinted"}
63
+ />
64
+ ) : null}
65
+ </View>
66
+ );
67
+ }
68
+
69
+ const styles = StyleSheet.create({
70
+ container: {
71
+ flexDirection: "row",
72
+ alignItems: "center",
73
+ gap: 8,
74
+ marginTop: 8,
75
+ marginBottom: 2,
76
+ },
77
+ title: {
78
+ letterSpacing: 0.8,
79
+ },
80
+ });