@xpufx/paseo-twofado 0.1.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 +62 -0
- package/client/approvals.tsx +1525 -0
- package/client/error-boundary.tsx +28 -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/package.json +36 -0
- package/paseo-plugin.json +6 -0
- package/server/twofado.ts +536 -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/approval.ts +254 -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
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
|
+
import { Text, type StyleProp, type TextStyle } from "react-native";
|
|
3
|
+
import { splitHighlightParts } from "../../../../shared/vendor/paseo-plugin-helper/highlight";
|
|
4
|
+
import { usePluginTheme } from "../theme/provider";
|
|
5
|
+
|
|
6
|
+
export interface HighlightedTextProps {
|
|
7
|
+
/** Source text rendered as-is when no query is active. */
|
|
8
|
+
text: string;
|
|
9
|
+
/**
|
|
10
|
+
* Active search query; matched case-insensitively and never as a regex. When
|
|
11
|
+
* the literal query is absent the query's tokens are highlighted, so a fuzzy
|
|
12
|
+
* result still reads.
|
|
13
|
+
*/
|
|
14
|
+
query: string;
|
|
15
|
+
style?: StyleProp<TextStyle>;
|
|
16
|
+
/**
|
|
17
|
+
* Overrides the matched-run style. Defaults to the theme accent background
|
|
18
|
+
* with `accentForeground` text so a match reads as selected.
|
|
19
|
+
*/
|
|
20
|
+
highlightStyle?: StyleProp<TextStyle>;
|
|
21
|
+
numberOfLines?: number;
|
|
22
|
+
selectable?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* When true, a query with no literal or token hit marks the whole text, so a
|
|
25
|
+
* fuzzy search result still reads as matched. Use it on the primary label
|
|
26
|
+
* (row/detail title) rather than every small field.
|
|
27
|
+
*/
|
|
28
|
+
fuzzyFallback?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* `<Text>` that paints every case-insensitive occurrence of `query` with the
|
|
33
|
+
* accent background/foreground. The query is matched literally, so user input
|
|
34
|
+
* is never evaluated as a regular expression. When the literal query is absent
|
|
35
|
+
* the splitter highlights the query's tokens; `fuzzyFallback` additionally
|
|
36
|
+
* marks the whole text when even a token misses, so a fuzzy result is not left
|
|
37
|
+
* silently unmarked. Renders the plain text when the query is empty or absent.
|
|
38
|
+
*/
|
|
39
|
+
export function HighlightedText({
|
|
40
|
+
text,
|
|
41
|
+
query,
|
|
42
|
+
style,
|
|
43
|
+
highlightStyle,
|
|
44
|
+
numberOfLines,
|
|
45
|
+
selectable,
|
|
46
|
+
fuzzyFallback = false,
|
|
47
|
+
}: HighlightedTextProps) {
|
|
48
|
+
const { colors } = usePluginTheme();
|
|
49
|
+
const parts = useMemo(
|
|
50
|
+
() => splitHighlightParts(text, query, { fallbackToWholeField: fuzzyFallback }),
|
|
51
|
+
[text, query, fuzzyFallback],
|
|
52
|
+
);
|
|
53
|
+
const matchStyle: StyleProp<TextStyle> = [
|
|
54
|
+
{ backgroundColor: colors.accent, color: colors.accentForeground },
|
|
55
|
+
highlightStyle,
|
|
56
|
+
];
|
|
57
|
+
return (
|
|
58
|
+
<Text style={style} numberOfLines={numberOfLines} selectable={selectable}>
|
|
59
|
+
{parts.map((part, index) =>
|
|
60
|
+
part.matched ? (
|
|
61
|
+
<Text key={`m${index}`} style={matchStyle}>
|
|
62
|
+
{part.text}
|
|
63
|
+
</Text>
|
|
64
|
+
) : (
|
|
65
|
+
part.text
|
|
66
|
+
),
|
|
67
|
+
)}
|
|
68
|
+
</Text>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import React, { type ReactNode } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Pressable,
|
|
4
|
+
StyleSheet,
|
|
5
|
+
Text,
|
|
6
|
+
type StyleProp,
|
|
7
|
+
type TextStyle,
|
|
8
|
+
type ViewStyle,
|
|
9
|
+
} from "react-native";
|
|
10
|
+
import { getClientHost } from "../host";
|
|
11
|
+
import { usePluginTheme } from "../theme/provider";
|
|
12
|
+
|
|
13
|
+
export interface InlineButtonProps {
|
|
14
|
+
label: string;
|
|
15
|
+
onPress?: () => void | Promise<void>;
|
|
16
|
+
icon?: string | ReactNode;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
accessibilityLabel?: string;
|
|
19
|
+
style?: StyleProp<ViewStyle>;
|
|
20
|
+
textStyle?: StyleProp<TextStyle>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Compact text/link action for inline cards and timeline content. */
|
|
24
|
+
export function InlineButton({
|
|
25
|
+
label,
|
|
26
|
+
onPress,
|
|
27
|
+
icon,
|
|
28
|
+
disabled = false,
|
|
29
|
+
accessibilityLabel,
|
|
30
|
+
style,
|
|
31
|
+
textStyle,
|
|
32
|
+
}: InlineButtonProps) {
|
|
33
|
+
const { Icon } = getClientHost();
|
|
34
|
+
const { colors, touchTargetMin, alpha } = usePluginTheme();
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<Pressable
|
|
38
|
+
onPress={onPress}
|
|
39
|
+
disabled={disabled}
|
|
40
|
+
accessibilityRole="button"
|
|
41
|
+
accessibilityLabel={accessibilityLabel || label}
|
|
42
|
+
hitSlop={Math.max(0, (touchTargetMin - 24) / 2)}
|
|
43
|
+
style={({ pressed }) => [
|
|
44
|
+
styles.base,
|
|
45
|
+
{
|
|
46
|
+
opacity: disabled ? 0.45 : 1,
|
|
47
|
+
backgroundColor: pressed && !disabled ? alpha(colors.accent, 0.12) : "transparent",
|
|
48
|
+
},
|
|
49
|
+
style,
|
|
50
|
+
]}
|
|
51
|
+
>
|
|
52
|
+
{typeof icon === "string" ? <Icon name={icon} size={13} color={colors.accent} /> : icon}
|
|
53
|
+
<Text style={[styles.text, { color: colors.accent }, textStyle]}>{label}</Text>
|
|
54
|
+
</Pressable>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const styles = StyleSheet.create({
|
|
59
|
+
base: {
|
|
60
|
+
alignSelf: "flex-start",
|
|
61
|
+
flexDirection: "row",
|
|
62
|
+
alignItems: "center",
|
|
63
|
+
gap: 4,
|
|
64
|
+
minHeight: 24,
|
|
65
|
+
paddingHorizontal: 3,
|
|
66
|
+
paddingVertical: 2,
|
|
67
|
+
borderRadius: 4,
|
|
68
|
+
},
|
|
69
|
+
text: {
|
|
70
|
+
fontSize: 12,
|
|
71
|
+
fontWeight: "600",
|
|
72
|
+
},
|
|
73
|
+
});
|
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import React, { useState, type ReactNode } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Platform,
|
|
4
|
+
Pressable,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
Text,
|
|
7
|
+
View,
|
|
8
|
+
type StyleProp,
|
|
9
|
+
type TextStyle,
|
|
10
|
+
type ViewStyle,
|
|
11
|
+
} from "react-native";
|
|
12
|
+
import { getClientHost } from "../host";
|
|
13
|
+
import { usePluginTheme } from "../theme/provider";
|
|
14
|
+
import { resolveGridColumns } from "../theme/responsive";
|
|
15
|
+
import { spacing } from "../theme/tokens";
|
|
16
|
+
import { copyToClipboard } from "../utils/clipboard";
|
|
17
|
+
import {
|
|
18
|
+
truncate,
|
|
19
|
+
truncateMiddle,
|
|
20
|
+
truncatePath,
|
|
21
|
+
TruncatePathOptions,
|
|
22
|
+
} from "../../../../shared/vendor/paseo-plugin-helper/formatters";
|
|
23
|
+
|
|
24
|
+
export type KeyValueTruncateMode = "end" | "middle" | "path";
|
|
25
|
+
|
|
26
|
+
export interface KeyValueProps {
|
|
27
|
+
label: string;
|
|
28
|
+
value: string | number | null | undefined;
|
|
29
|
+
subValue?: string;
|
|
30
|
+
mono?: boolean;
|
|
31
|
+
copyable?: boolean;
|
|
32
|
+
/** Truncate long value: "middle" (UUIDs/hashes), "path" (filepaths), or "end" (standard) */
|
|
33
|
+
truncate?: boolean | KeyValueTruncateMode;
|
|
34
|
+
/** Maximum length before truncation applies. Default: 32 */
|
|
35
|
+
truncateMaxLength?: number;
|
|
36
|
+
/** Custom options when truncate="path" */
|
|
37
|
+
truncatePathOptions?: TruncatePathOptions;
|
|
38
|
+
/**
|
|
39
|
+
* "stacked" (default) keeps the existing label-above-value layout.
|
|
40
|
+
* "inline" renders label and value on one line, with the value truncating
|
|
41
|
+
* middle so the row stays a single text line.
|
|
42
|
+
*/
|
|
43
|
+
layout?: "stacked" | "inline";
|
|
44
|
+
stackOnCompact?: boolean;
|
|
45
|
+
style?: StyleProp<ViewStyle>;
|
|
46
|
+
labelStyle?: StyleProp<TextStyle>;
|
|
47
|
+
valueStyle?: StyleProp<TextStyle>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function KeyValue({
|
|
51
|
+
label,
|
|
52
|
+
value,
|
|
53
|
+
subValue,
|
|
54
|
+
mono = false,
|
|
55
|
+
copyable = false,
|
|
56
|
+
truncate: truncateProp = false,
|
|
57
|
+
truncateMaxLength = 32,
|
|
58
|
+
truncatePathOptions,
|
|
59
|
+
layout = "stacked",
|
|
60
|
+
stackOnCompact = true,
|
|
61
|
+
style,
|
|
62
|
+
labelStyle,
|
|
63
|
+
valueStyle,
|
|
64
|
+
}: KeyValueProps) {
|
|
65
|
+
const { Icon, useToast } = getClientHost();
|
|
66
|
+
const { colors, flair, isCompact, touchTargetMin, fonts, typography } = usePluginTheme();
|
|
67
|
+
const toast = useToast();
|
|
68
|
+
const [copied, setCopied] = useState(false);
|
|
69
|
+
|
|
70
|
+
const rawString = value === null || value === undefined ? "" : String(value);
|
|
71
|
+
|
|
72
|
+
let displayValue = rawString || "-";
|
|
73
|
+
if (truncateProp && rawString.length > truncateMaxLength) {
|
|
74
|
+
const mode: KeyValueTruncateMode =
|
|
75
|
+
typeof truncateProp === "string" ? truncateProp : "middle";
|
|
76
|
+
switch (mode) {
|
|
77
|
+
case "path":
|
|
78
|
+
displayValue = truncatePath(rawString, truncateMaxLength, truncatePathOptions);
|
|
79
|
+
break;
|
|
80
|
+
case "end":
|
|
81
|
+
displayValue = truncate(rawString, truncateMaxLength);
|
|
82
|
+
break;
|
|
83
|
+
case "middle":
|
|
84
|
+
default:
|
|
85
|
+
displayValue = truncateMiddle(rawString, truncateMaxLength);
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const handleCopy = async () => {
|
|
91
|
+
if (!copyable || !rawString) return;
|
|
92
|
+
const ok = await copyToClipboard(rawString, {
|
|
93
|
+
toast,
|
|
94
|
+
toastMessage: label,
|
|
95
|
+
});
|
|
96
|
+
if (ok) {
|
|
97
|
+
setCopied(true);
|
|
98
|
+
setTimeout(() => setCopied(false), 2000);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const fontFamily = mono
|
|
103
|
+
? fonts.mono ?? Platform.select({ ios: "Menlo", android: "monospace", default: "monospace" })
|
|
104
|
+
: undefined;
|
|
105
|
+
|
|
106
|
+
const shouldStack = stackOnCompact && isCompact;
|
|
107
|
+
|
|
108
|
+
const copyButton = copyable && value ? (
|
|
109
|
+
<Pressable
|
|
110
|
+
onPress={handleCopy}
|
|
111
|
+
hitSlop={Math.max(8, (touchTargetMin - 20) / 2)}
|
|
112
|
+
style={styles.copyBtn}
|
|
113
|
+
accessibilityRole="button"
|
|
114
|
+
accessibilityLabel={`Copy ${label}`}
|
|
115
|
+
>
|
|
116
|
+
<Icon
|
|
117
|
+
name={copied ? "Check" : "Copy"}
|
|
118
|
+
size={isCompact ? 12 : 13}
|
|
119
|
+
color={copied ? colors.statusSuccess : colors.foregroundMuted}
|
|
120
|
+
/>
|
|
121
|
+
</Pressable>
|
|
122
|
+
) : null;
|
|
123
|
+
|
|
124
|
+
if (layout === "inline") {
|
|
125
|
+
return (
|
|
126
|
+
<View
|
|
127
|
+
style={[
|
|
128
|
+
styles.container,
|
|
129
|
+
styles.inlineContainer,
|
|
130
|
+
{ paddingVertical: isCompact ? spacing.xs : spacing.sm },
|
|
131
|
+
style,
|
|
132
|
+
]}
|
|
133
|
+
>
|
|
134
|
+
<Text
|
|
135
|
+
style={[
|
|
136
|
+
styles.inlineLabel,
|
|
137
|
+
{
|
|
138
|
+
color: colors.foregroundMuted,
|
|
139
|
+
...typography.label,
|
|
140
|
+
textTransform: flair.headingTransform === "uppercase" ? "uppercase" : "none",
|
|
141
|
+
},
|
|
142
|
+
labelStyle,
|
|
143
|
+
]}
|
|
144
|
+
>
|
|
145
|
+
{label}
|
|
146
|
+
</Text>
|
|
147
|
+
|
|
148
|
+
<Text
|
|
149
|
+
selectable
|
|
150
|
+
numberOfLines={1}
|
|
151
|
+
ellipsizeMode="middle"
|
|
152
|
+
style={[
|
|
153
|
+
styles.inlineValueText,
|
|
154
|
+
{
|
|
155
|
+
color: colors.foreground,
|
|
156
|
+
...typography.bodySmall,
|
|
157
|
+
fontFamily,
|
|
158
|
+
},
|
|
159
|
+
valueStyle,
|
|
160
|
+
]}
|
|
161
|
+
>
|
|
162
|
+
{displayValue}
|
|
163
|
+
</Text>
|
|
164
|
+
|
|
165
|
+
{subValue ? (
|
|
166
|
+
<Text
|
|
167
|
+
selectable
|
|
168
|
+
numberOfLines={1}
|
|
169
|
+
style={[
|
|
170
|
+
styles.inlineSubValue,
|
|
171
|
+
{
|
|
172
|
+
color: colors.foregroundMuted,
|
|
173
|
+
...typography.caption,
|
|
174
|
+
},
|
|
175
|
+
]}
|
|
176
|
+
>
|
|
177
|
+
{subValue}
|
|
178
|
+
</Text>
|
|
179
|
+
) : null}
|
|
180
|
+
|
|
181
|
+
{copyButton}
|
|
182
|
+
</View>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (shouldStack) {
|
|
187
|
+
return (
|
|
188
|
+
<View
|
|
189
|
+
style={[
|
|
190
|
+
styles.container,
|
|
191
|
+
styles.stackedContainer,
|
|
192
|
+
{ paddingVertical: isCompact ? spacing.xs : spacing.sm },
|
|
193
|
+
style,
|
|
194
|
+
]}
|
|
195
|
+
>
|
|
196
|
+
<View style={styles.stackedHeaderRow}>
|
|
197
|
+
<Text
|
|
198
|
+
style={[
|
|
199
|
+
styles.label,
|
|
200
|
+
{
|
|
201
|
+
color: colors.foregroundMuted,
|
|
202
|
+
...typography.label,
|
|
203
|
+
textTransform: flair.headingTransform === "uppercase" ? "uppercase" : "none",
|
|
204
|
+
},
|
|
205
|
+
labelStyle,
|
|
206
|
+
]}
|
|
207
|
+
>
|
|
208
|
+
{label}
|
|
209
|
+
</Text>
|
|
210
|
+
{copyButton}
|
|
211
|
+
</View>
|
|
212
|
+
|
|
213
|
+
<Text
|
|
214
|
+
selectable
|
|
215
|
+
style={[
|
|
216
|
+
styles.stackedValueText,
|
|
217
|
+
{
|
|
218
|
+
color: colors.foreground,
|
|
219
|
+
...typography.bodySmall,
|
|
220
|
+
fontFamily,
|
|
221
|
+
},
|
|
222
|
+
valueStyle,
|
|
223
|
+
]}
|
|
224
|
+
>
|
|
225
|
+
{displayValue}
|
|
226
|
+
</Text>
|
|
227
|
+
|
|
228
|
+
{subValue ? (
|
|
229
|
+
<Text
|
|
230
|
+
style={[
|
|
231
|
+
styles.subValue,
|
|
232
|
+
{
|
|
233
|
+
color: colors.foregroundMuted,
|
|
234
|
+
...typography.caption,
|
|
235
|
+
},
|
|
236
|
+
]}
|
|
237
|
+
>
|
|
238
|
+
{subValue}
|
|
239
|
+
</Text>
|
|
240
|
+
) : null}
|
|
241
|
+
</View>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Horizontal layout for Desktop / Wide screens
|
|
246
|
+
return (
|
|
247
|
+
<View
|
|
248
|
+
style={[
|
|
249
|
+
styles.container,
|
|
250
|
+
styles.rowContainer,
|
|
251
|
+
{ paddingVertical: isCompact ? spacing.xs : spacing.sm },
|
|
252
|
+
style,
|
|
253
|
+
]}
|
|
254
|
+
>
|
|
255
|
+
<Text
|
|
256
|
+
style={[
|
|
257
|
+
styles.label,
|
|
258
|
+
{
|
|
259
|
+
color: colors.foregroundMuted,
|
|
260
|
+
...typography.label,
|
|
261
|
+
textTransform: flair.headingTransform === "uppercase" ? "uppercase" : "none",
|
|
262
|
+
},
|
|
263
|
+
labelStyle,
|
|
264
|
+
]}
|
|
265
|
+
>
|
|
266
|
+
{label}
|
|
267
|
+
</Text>
|
|
268
|
+
|
|
269
|
+
<View style={styles.rowValueWrapper}>
|
|
270
|
+
<View style={styles.rowValueLine}>
|
|
271
|
+
<Text
|
|
272
|
+
selectable
|
|
273
|
+
style={[
|
|
274
|
+
styles.rowValueText,
|
|
275
|
+
{
|
|
276
|
+
color: colors.foreground,
|
|
277
|
+
...typography.bodySmall,
|
|
278
|
+
fontFamily,
|
|
279
|
+
},
|
|
280
|
+
valueStyle,
|
|
281
|
+
]}
|
|
282
|
+
>
|
|
283
|
+
{displayValue}
|
|
284
|
+
</Text>
|
|
285
|
+
|
|
286
|
+
{copyButton}
|
|
287
|
+
</View>
|
|
288
|
+
|
|
289
|
+
{subValue ? (
|
|
290
|
+
<Text
|
|
291
|
+
style={[
|
|
292
|
+
styles.subValue,
|
|
293
|
+
{
|
|
294
|
+
color: colors.foregroundMuted,
|
|
295
|
+
...typography.caption,
|
|
296
|
+
},
|
|
297
|
+
]}
|
|
298
|
+
>
|
|
299
|
+
{subValue}
|
|
300
|
+
</Text>
|
|
301
|
+
) : null}
|
|
302
|
+
</View>
|
|
303
|
+
</View>
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export interface KeyValueGroupProps {
|
|
308
|
+
children: ReactNode;
|
|
309
|
+
columns?: 1 | 2 | 3 | 4;
|
|
310
|
+
gap?: number;
|
|
311
|
+
/**
|
|
312
|
+
* How a compact surface treats the column count.
|
|
313
|
+
* - "compact" (default): collapse to a single column on a compact surface —
|
|
314
|
+
* the historical behavior.
|
|
315
|
+
* - "never": keep the requested column count on a compact surface.
|
|
316
|
+
*/
|
|
317
|
+
collapse?: "compact" | "never";
|
|
318
|
+
/**
|
|
319
|
+
* Minimum width a column should keep. When set and the container width is
|
|
320
|
+
* known, the effective column count is capped so each column stays at least
|
|
321
|
+
* this wide, wrapping to fewer columns rather than collapsing to one.
|
|
322
|
+
* `columns` remains the upper bound.
|
|
323
|
+
*/
|
|
324
|
+
minColumnWidth?: number;
|
|
325
|
+
style?: StyleProp<ViewStyle>;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function KeyValueGroup({
|
|
329
|
+
children,
|
|
330
|
+
columns = 2,
|
|
331
|
+
gap = spacing.md,
|
|
332
|
+
collapse = "compact",
|
|
333
|
+
minColumnWidth,
|
|
334
|
+
style,
|
|
335
|
+
}: KeyValueGroupProps) {
|
|
336
|
+
const { isCompact, layout } = usePluginTheme();
|
|
337
|
+
const effectiveColumns = resolveGridColumns({
|
|
338
|
+
columns,
|
|
339
|
+
gap,
|
|
340
|
+
width: layout.width,
|
|
341
|
+
minColumnWidth,
|
|
342
|
+
collapse,
|
|
343
|
+
isCompact,
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
const childArray = React.Children.toArray(children).filter(Boolean);
|
|
347
|
+
|
|
348
|
+
return (
|
|
349
|
+
<View style={[styles.groupContainer, { gap }, style]}>
|
|
350
|
+
{childArray.map((child, index) => (
|
|
351
|
+
<View
|
|
352
|
+
key={index}
|
|
353
|
+
style={{
|
|
354
|
+
flexGrow: 1,
|
|
355
|
+
flexShrink: 1,
|
|
356
|
+
flexBasis: `${Math.floor(100 / effectiveColumns) - 2}%`,
|
|
357
|
+
}}
|
|
358
|
+
>
|
|
359
|
+
{child}
|
|
360
|
+
</View>
|
|
361
|
+
))}
|
|
362
|
+
</View>
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const styles = StyleSheet.create({
|
|
367
|
+
container: {
|
|
368
|
+
width: "100%",
|
|
369
|
+
},
|
|
370
|
+
rowContainer: {
|
|
371
|
+
flexDirection: "row",
|
|
372
|
+
alignItems: "flex-start",
|
|
373
|
+
justifyContent: "space-between",
|
|
374
|
+
gap: 12,
|
|
375
|
+
},
|
|
376
|
+
stackedContainer: {
|
|
377
|
+
flexDirection: "column",
|
|
378
|
+
gap: 3,
|
|
379
|
+
width: "100%",
|
|
380
|
+
},
|
|
381
|
+
stackedHeaderRow: {
|
|
382
|
+
flexDirection: "row",
|
|
383
|
+
alignItems: "center",
|
|
384
|
+
justifyContent: "space-between",
|
|
385
|
+
width: "100%",
|
|
386
|
+
},
|
|
387
|
+
stackedValueText: {
|
|
388
|
+
width: "100%",
|
|
389
|
+
},
|
|
390
|
+
inlineContainer: {
|
|
391
|
+
flexDirection: "row",
|
|
392
|
+
alignItems: "center",
|
|
393
|
+
gap: spacing.xs,
|
|
394
|
+
minWidth: 0,
|
|
395
|
+
},
|
|
396
|
+
inlineLabel: {
|
|
397
|
+
flexShrink: 0,
|
|
398
|
+
},
|
|
399
|
+
inlineValueText: {
|
|
400
|
+
flexShrink: 1,
|
|
401
|
+
minWidth: 0,
|
|
402
|
+
},
|
|
403
|
+
inlineSubValue: {
|
|
404
|
+
flexShrink: 0,
|
|
405
|
+
},
|
|
406
|
+
rowValueWrapper: {
|
|
407
|
+
flexDirection: "column",
|
|
408
|
+
alignItems: "flex-end",
|
|
409
|
+
flexGrow: 1,
|
|
410
|
+
flexShrink: 1,
|
|
411
|
+
minWidth: 0,
|
|
412
|
+
gap: 2,
|
|
413
|
+
},
|
|
414
|
+
rowValueLine: {
|
|
415
|
+
flexDirection: "row",
|
|
416
|
+
alignItems: "center",
|
|
417
|
+
justifyContent: "flex-end",
|
|
418
|
+
gap: 6,
|
|
419
|
+
flexShrink: 1,
|
|
420
|
+
minWidth: 0,
|
|
421
|
+
maxWidth: "100%",
|
|
422
|
+
},
|
|
423
|
+
rowValueText: {
|
|
424
|
+
flexShrink: 1,
|
|
425
|
+
minWidth: 0,
|
|
426
|
+
textAlign: "right",
|
|
427
|
+
},
|
|
428
|
+
groupContainer: {
|
|
429
|
+
flexDirection: "row",
|
|
430
|
+
flexWrap: "wrap",
|
|
431
|
+
width: "100%",
|
|
432
|
+
},
|
|
433
|
+
label: {
|
|
434
|
+
flexShrink: 1,
|
|
435
|
+
minWidth: 0,
|
|
436
|
+
},
|
|
437
|
+
subValue: {
|
|
438
|
+
flexShrink: 1,
|
|
439
|
+
minWidth: 0,
|
|
440
|
+
},
|
|
441
|
+
copyBtn: {
|
|
442
|
+
padding: 3,
|
|
443
|
+
alignItems: "center",
|
|
444
|
+
justifyContent: "center",
|
|
445
|
+
},
|
|
446
|
+
});
|