@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,225 @@
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 { HighlightedText } from "./HighlightedText";
6
+ import type { SurfaceStyle } from "../theme/flair";
7
+
8
+ export interface CardProps {
9
+ children: ReactNode;
10
+ variant?: SurfaceStyle;
11
+ style?: StyleProp<ViewStyle>;
12
+ noPadding?: boolean;
13
+ }
14
+
15
+ export interface CardHeaderProps {
16
+ title: string;
17
+ subtitle?: string;
18
+ value?: string | number | ReactNode;
19
+ badge?: ReactNode;
20
+ action?: ReactNode;
21
+ icon?: string;
22
+ style?: StyleProp<ViewStyle>;
23
+ titleStyle?: StyleProp<TextStyle>;
24
+ subtitleStyle?: StyleProp<TextStyle>;
25
+ /**
26
+ * When set, every case-insensitive (literal, non-regex) occurrence of the
27
+ * query inside `title` is painted with the accent highlight. With no literal
28
+ * or token hit the whole title is marked, so a fuzzy match still reads.
29
+ */
30
+ highlightQuery?: string;
31
+ }
32
+
33
+ export function CardHeader({
34
+ title,
35
+ subtitle,
36
+ value,
37
+ badge,
38
+ action,
39
+ icon,
40
+ style,
41
+ titleStyle,
42
+ subtitleStyle,
43
+ highlightQuery,
44
+ }: CardHeaderProps) {
45
+ const { Icon } = getClientHost();
46
+ const { colors, flair, typography, padding, isCompact } = usePluginTheme();
47
+
48
+ return (
49
+ <View
50
+ style={[
51
+ styles.headerContainer,
52
+ // Density-aware gap below the header. A hardcoded 8 wasted vertical
53
+ // space under the compact preset (xpufx-org/paseo#213).
54
+ { marginBottom: padding.gap },
55
+ // On a compact/narrow surface, stack the title block above the action
56
+ // row instead of sharing one line. Sharing the line lets a wide action
57
+ // group (badges + toggle + buttons) hold its intrinsic width while the
58
+ // title column collapses to its longest word, wrapping one character
59
+ // per line and spilling the actions past the card (xpufx-org/paseo#202).
60
+ isCompact && styles.headerContainerCompact,
61
+ style,
62
+ ]}
63
+ >
64
+ <View style={[styles.headerLeft, isCompact && styles.headerLeftCompact]}>
65
+ {icon ? <Icon name={icon} size={15} color={colors.foregroundMuted} /> : null}
66
+ <View style={styles.titleColumn}>
67
+ <Text
68
+ style={[
69
+ styles.headerTitle,
70
+ {
71
+ color: colors.foreground,
72
+ ...typography.heading,
73
+ textTransform:
74
+ flair.headingTransform === "uppercase" ? "uppercase" : "none",
75
+ },
76
+ titleStyle,
77
+ ]}
78
+ >
79
+ {highlightQuery ? (
80
+ <HighlightedText text={title} query={highlightQuery} fuzzyFallback />
81
+ ) : (
82
+ title
83
+ )}
84
+ </Text>
85
+ {subtitle ? (
86
+ <Text
87
+ style={[
88
+ styles.headerSubtitle,
89
+ { color: colors.foregroundMuted, ...typography.caption },
90
+ subtitleStyle,
91
+ ]}
92
+ >
93
+ {subtitle}
94
+ </Text>
95
+ ) : null}
96
+ </View>
97
+ </View>
98
+
99
+ <View style={[styles.headerRight, isCompact && styles.headerRightCompact]}>
100
+ {badge ? <View style={{ marginRight: 6 }}>{badge}</View> : null}
101
+ {typeof value === "string" || typeof value === "number" ? (
102
+ <Text
103
+ style={[
104
+ styles.headerValue,
105
+ { color: colors.foreground, ...typography.bodyStrong },
106
+ ]}
107
+ >
108
+ {value}
109
+ </Text>
110
+ ) : (
111
+ value
112
+ )}
113
+ {action}
114
+ </View>
115
+ </View>
116
+ );
117
+ }
118
+
119
+ export function Card({ children, variant, style, noPadding = false }: CardProps) {
120
+ const { colors, flair, resolveRadius, padding, alpha } = usePluginTheme();
121
+
122
+ const effectiveVariant = variant || flair.surfaceStyle;
123
+ const radius = resolveRadius("md");
124
+
125
+ let bg = colors.surface0;
126
+ let border = colors.border;
127
+
128
+ if (effectiveVariant === "tinted") {
129
+ bg = alpha(colors.accent, 0.04);
130
+ border = alpha(colors.accent, 0.2);
131
+ } else if (effectiveVariant === "elevated") {
132
+ bg = colors.surface1;
133
+ border = colors.border;
134
+ }
135
+
136
+ return (
137
+ <View
138
+ style={[
139
+ styles.card,
140
+ {
141
+ backgroundColor: bg,
142
+ borderColor: border,
143
+ borderRadius: radius,
144
+ borderWidth: flair.borderWidth,
145
+ paddingHorizontal: noPadding ? 0 : padding.horizontal,
146
+ paddingVertical: noPadding ? 0 : padding.vertical,
147
+ },
148
+ style,
149
+ ]}
150
+ >
151
+ {children}
152
+ </View>
153
+ );
154
+ }
155
+
156
+ Card.Header = CardHeader;
157
+
158
+ const styles = StyleSheet.create({
159
+ card: {
160
+ overflow: "hidden",
161
+ width: "100%",
162
+ },
163
+ headerContainer: {
164
+ flexDirection: "row",
165
+ alignItems: "center",
166
+ justifyContent: "space-between",
167
+ flexWrap: "wrap",
168
+ gap: 8,
169
+ width: "100%",
170
+ },
171
+ headerContainerCompact: {
172
+ flexDirection: "column",
173
+ alignItems: "stretch",
174
+ justifyContent: "flex-start",
175
+ flexWrap: "nowrap",
176
+ },
177
+ headerLeft: {
178
+ flexDirection: "row",
179
+ alignItems: "center",
180
+ gap: 8,
181
+ // `flex: 1` written out: the compact override has to reset the basis too,
182
+ // and `flex` + explicit flex* keys in one flattened style is ambiguous.
183
+ flexGrow: 1,
184
+ flexShrink: 1,
185
+ flexBasis: 0,
186
+ minWidth: 0,
187
+ },
188
+ // Release `flex: 1` (basis 0) in the stacked column so the title block takes
189
+ // the full width and wraps at word boundaries instead of collapsing to its
190
+ // longest word.
191
+ headerLeftCompact: {
192
+ flexGrow: 0,
193
+ flexShrink: 0,
194
+ flexBasis: "auto",
195
+ width: "100%",
196
+ },
197
+ titleColumn: {
198
+ gap: 1,
199
+ flexShrink: 1,
200
+ },
201
+ headerTitle: {
202
+ fontWeight: "600",
203
+ },
204
+ headerSubtitle: {
205
+ fontWeight: "400",
206
+ },
207
+ headerRight: {
208
+ flexDirection: "row",
209
+ alignItems: "center",
210
+ gap: 6,
211
+ flexShrink: 0,
212
+ },
213
+ // The action group may wrap onto multiple lines once the title owns its own
214
+ // row; letting it shrink + wrap keeps a long badge/toggle/button set inside
215
+ // the card instead of overflowing it.
216
+ headerRightCompact: {
217
+ flexShrink: 1,
218
+ flexWrap: "wrap",
219
+ justifyContent: "flex-start",
220
+ minWidth: 0,
221
+ },
222
+ headerValue: {
223
+ fontWeight: "600",
224
+ },
225
+ });
@@ -0,0 +1,196 @@
1
+ import React, { useState } from "react";
2
+ import {
3
+ Platform,
4
+ Pressable,
5
+ ScrollView as FallbackScrollView,
6
+ StyleSheet,
7
+ Text,
8
+ View,
9
+ type StyleProp,
10
+ type TextStyle,
11
+ type ViewStyle,
12
+ } from "react-native";
13
+ import { getClientHost } from "../host";
14
+ import { usePluginTheme } from "../theme/provider";
15
+ import { copyToClipboard } from "../utils/clipboard";
16
+
17
+ export interface CodeBlockProps {
18
+ code: string;
19
+ language?: string;
20
+ title?: string;
21
+ maxHeight?: number;
22
+ copyable?: boolean;
23
+ style?: StyleProp<ViewStyle>;
24
+ textStyle?: StyleProp<TextStyle>;
25
+ }
26
+
27
+ export function CodeBlock({
28
+ code,
29
+ language,
30
+ title,
31
+ maxHeight = 320,
32
+ copyable = true,
33
+ style,
34
+ textStyle,
35
+ }: CodeBlockProps) {
36
+ const { Icon, useToast } = getClientHost();
37
+ // Inner snippet scrollers are NEVER the host sheet scroller: the host
38
+ // ScrollView is a sheet-gesture pan controller, so using it horizontally or
39
+ // nested inside the host sheet collapses to height 0 / crashes the gesture
40
+ // handler (#219). Plain React Native ScrollViews own this bounded box.
41
+ const { colors, resolveRadius, isCompact, touchTargetMin, alpha } = usePluginTheme();
42
+ const toast = useToast();
43
+ const [copied, setCopied] = useState(false);
44
+
45
+ const radius = resolveRadius("md");
46
+
47
+ const handleCopy = async () => {
48
+ const ok = await copyToClipboard(code, {
49
+ toast,
50
+ toastMessage: title || "Code",
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
+ return (
65
+ <View
66
+ style={[
67
+ styles.container,
68
+ {
69
+ backgroundColor: colors.surface0,
70
+ borderColor: colors.border,
71
+ borderRadius: radius,
72
+ },
73
+ style,
74
+ ]}
75
+ >
76
+ {(title || language || copyable) && (
77
+ <View
78
+ style={[
79
+ styles.header,
80
+ {
81
+ borderBottomColor: alpha(colors.border, 0.7),
82
+ },
83
+ ]}
84
+ >
85
+ <View style={styles.headerLeft}>
86
+ {title ? (
87
+ <Text style={[styles.title, { color: colors.foreground }]}>{title}</Text>
88
+ ) : language ? (
89
+ <Text style={[styles.language, { color: colors.foregroundMuted }]}>
90
+ {language.toUpperCase()}
91
+ </Text>
92
+ ) : null}
93
+ </View>
94
+ {copyable && (
95
+ <Pressable
96
+ onPress={handleCopy}
97
+ hitSlop={Math.max(0, (touchTargetMin - 28) / 2)}
98
+ style={({ pressed }) => [
99
+ styles.copyButton,
100
+ {
101
+ backgroundColor: pressed ? colors.surface2 : colors.surface1,
102
+ borderColor: colors.border,
103
+ borderRadius: radius - 2,
104
+ },
105
+ ]}
106
+ >
107
+ <Icon
108
+ name={copied ? "Check" : "Copy"}
109
+ size={12}
110
+ color={copied ? colors.statusSuccess : colors.foregroundMuted}
111
+ />
112
+ <Text
113
+ style={[
114
+ styles.copyText,
115
+ { color: copied ? colors.statusSuccess : colors.foregroundMuted },
116
+ ]}
117
+ >
118
+ {copied ? "Copied!" : "Copy"}
119
+ </Text>
120
+ </Pressable>
121
+ )}
122
+ </View>
123
+ )}
124
+
125
+ <FallbackScrollView
126
+ nestedScrollEnabled
127
+ style={{ maxHeight }}
128
+ contentContainerStyle={styles.scrollContent}
129
+ >
130
+ <FallbackScrollView horizontal showsHorizontalScrollIndicator>
131
+ <Text
132
+ selectable
133
+ style={[
134
+ styles.codeText,
135
+ {
136
+ color: colors.foreground,
137
+ fontFamily,
138
+ fontSize: isCompact ? 11 : 12,
139
+ },
140
+ textStyle,
141
+ ]}
142
+ >
143
+ {code}
144
+ </Text>
145
+ </FallbackScrollView>
146
+ </FallbackScrollView>
147
+ </View>
148
+ );
149
+ }
150
+
151
+ const styles = StyleSheet.create({
152
+ container: {
153
+ borderWidth: 1,
154
+ overflow: "hidden",
155
+ },
156
+ header: {
157
+ flexDirection: "row",
158
+ alignItems: "center",
159
+ justifyContent: "space-between",
160
+ paddingHorizontal: 10,
161
+ paddingVertical: 6,
162
+ borderBottomWidth: 1,
163
+ },
164
+ headerLeft: {
165
+ flexDirection: "row",
166
+ alignItems: "center",
167
+ gap: 8,
168
+ },
169
+ title: {
170
+ fontSize: 12,
171
+ fontWeight: "600",
172
+ },
173
+ language: {
174
+ fontSize: 10,
175
+ fontWeight: "700",
176
+ letterSpacing: 0.5,
177
+ },
178
+ copyButton: {
179
+ flexDirection: "row",
180
+ alignItems: "center",
181
+ gap: 4,
182
+ paddingHorizontal: 8,
183
+ paddingVertical: 3,
184
+ borderWidth: 1,
185
+ },
186
+ copyText: {
187
+ fontSize: 11,
188
+ fontWeight: "500",
189
+ },
190
+ scrollContent: {
191
+ padding: 10,
192
+ },
193
+ codeText: {
194
+ lineHeight: 18,
195
+ },
196
+ });
@@ -0,0 +1,277 @@
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
+ });