@xpufx/paseo-top 0.4.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.
- package/LICENSE +21 -0
- package/README.md +114 -0
- package/client/pill-labels.ts +480 -0
- package/client/pill.tsx +2461 -0
- package/client/resources-query.ts +68 -0
- package/client/settings-ui.tsx +50 -0
- package/client/surface.tsx +364 -0
- package/client/telemetry-copy.ts +105 -0
- package/client/telemetry.tsx +486 -0
- package/client/turn-counter.ts +151 -0
- package/client/turn-panel.tsx +182 -0
- package/client/vendor/paseo-plugin-helper/command-center.ts +43 -0
- package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +493 -0
- package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +250 -0
- package/client/vendor/paseo-plugin-helper/components/Badge.tsx +156 -0
- package/client/vendor/paseo-plugin-helper/components/Button.tsx +178 -0
- package/client/vendor/paseo-plugin-helper/components/Card.tsx +225 -0
- package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +196 -0
- package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +277 -0
- package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +172 -0
- package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +180 -0
- package/client/vendor/paseo-plugin-helper/components/DataTable.tsx +200 -0
- package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +97 -0
- package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +70 -0
- package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +73 -0
- package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +446 -0
- package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +247 -0
- package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +117 -0
- package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +53 -0
- package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +118 -0
- package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/components/Select.tsx +215 -0
- package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +319 -0
- package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +150 -0
- package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +163 -0
- package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +157 -0
- package/client/vendor/paseo-plugin-helper/components/index.ts +25 -0
- package/client/vendor/paseo-plugin-helper/custom-pills.tsx +224 -0
- package/client/vendor/paseo-plugin-helper/forge-icon.tsx +79 -0
- package/client/vendor/paseo-plugin-helper/host.ts +277 -0
- package/client/vendor/paseo-plugin-helper/icon.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/index.ts +28 -0
- package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +49 -0
- package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +103 -0
- package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +65 -0
- package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +378 -0
- package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +49 -0
- package/client/vendor/paseo-plugin-helper/layout/Row.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/layout/index.ts +7 -0
- package/client/vendor/paseo-plugin-helper/panel.tsx +81 -0
- package/client/vendor/paseo-plugin-helper/pill.tsx +884 -0
- package/client/vendor/paseo-plugin-helper/query-refresh.ts +79 -0
- package/client/vendor/paseo-plugin-helper/query.ts +66 -0
- package/client/vendor/paseo-plugin-helper/settings-screen.tsx +372 -0
- package/client/vendor/paseo-plugin-helper/settings.ts +181 -0
- package/client/vendor/paseo-plugin-helper/shared-settings.ts +46 -0
- package/client/vendor/paseo-plugin-helper/snapshot.ts +68 -0
- package/client/vendor/paseo-plugin-helper/surface.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/theme/color-utils.ts +118 -0
- package/client/vendor/paseo-plugin-helper/theme/flair.ts +76 -0
- package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +121 -0
- package/client/vendor/paseo-plugin-helper/theme/index.ts +7 -0
- package/client/vendor/paseo-plugin-helper/theme/provider.tsx +214 -0
- package/client/vendor/paseo-plugin-helper/theme/responsive.ts +213 -0
- package/client/vendor/paseo-plugin-helper/theme/tokens.ts +161 -0
- package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +57 -0
- package/client/vendor/paseo-plugin-helper/utils/clipboard.ts +149 -0
- package/client/vendor/paseo-plugin-helper/utils/haptics.ts +34 -0
- package/examples/pills/battery.jsonc +23 -0
- package/examples/pills/disk-usage.jsonc +18 -0
- package/examples/pills/docker-containers.jsonc +22 -0
- package/examples/pills/git-dirty.jsonc +22 -0
- package/examples/pills/gpu-nvidia.jsonc +22 -0
- package/package.json +37 -0
- package/paseo-plugin.json +4 -0
- package/server/resources.ts +784 -0
- package/server/vendor/paseo-plugin-helper/agent.ts +85 -0
- package/server/vendor/paseo-plugin-helper/custom-pills.ts +344 -0
- package/server/vendor/paseo-plugin-helper/index.ts +18 -0
- package/server/vendor/paseo-plugin-helper/jsonc.ts +78 -0
- package/server/vendor/paseo-plugin-helper/logger.ts +210 -0
- package/server/vendor/paseo-plugin-helper/mcp-config.ts +367 -0
- package/server/vendor/paseo-plugin-helper/mcp-injection.ts +85 -0
- package/server/vendor/paseo-plugin-helper/network.ts +91 -0
- package/server/vendor/paseo-plugin-helper/plugins.ts +160 -0
- package/server/vendor/paseo-plugin-helper/process.ts +186 -0
- package/server/vendor/paseo-plugin-helper/redact.ts +86 -0
- package/server/vendor/paseo-plugin-helper/rpc-guard.ts +77 -0
- package/server/vendor/paseo-plugin-helper/settings.ts +97 -0
- package/server/vendor/paseo-plugin-helper/shared-settings.ts +243 -0
- package/server/vendor/paseo-plugin-helper/storage.ts +244 -0
- package/server/vendor/paseo-plugin-helper/system.ts +128 -0
- package/server/vendor/paseo-plugin-helper/task.ts +116 -0
- package/server/vendor/paseo-plugin-helper/version.ts +153 -0
- package/server/vendor/paseo-plugin-helper/workspace-beacon.ts +418 -0
- package/shared/resources.ts +686 -0
- package/shared/vendor/paseo-plugin-helper/README.md +11 -0
- package/shared/vendor/paseo-plugin-helper/async.ts +35 -0
- package/shared/vendor/paseo-plugin-helper/custom-pills.ts +169 -0
- package/shared/vendor/paseo-plugin-helper/forge.ts +110 -0
- package/shared/vendor/paseo-plugin-helper/formatters.ts +271 -0
- package/shared/vendor/paseo-plugin-helper/highlight.ts +184 -0
- package/shared/vendor/paseo-plugin-helper/index.ts +10 -0
- package/shared/vendor/paseo-plugin-helper/rpc.ts +72 -0
- package/shared/vendor/paseo-plugin-helper/settings.ts +138 -0
- package/shared/vendor/paseo-plugin-helper/suite-settings.ts +17 -0
- package/shared/vendor/paseo-plugin-helper/suppressed.ts +31 -0
- package/shared/vendor/paseo-plugin-helper/types.ts +36 -0
- 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
|
+
});
|