@xpufx/paseo-forges 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.
Files changed (128) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +109 -0
  3. package/client/active-forge.ts +47 -0
  4. package/client/board-alert.tsx +236 -0
  5. package/client/foreign-link.tsx +33 -0
  6. package/client/hook-queue-panel.tsx +412 -0
  7. package/client/issues-pill.tsx +1976 -0
  8. package/client/label-chip.tsx +116 -0
  9. package/client/linkifier.tsx +280 -0
  10. package/client/pill-label.ts +183 -0
  11. package/client/vendor/paseo-plugin-helper/command-center.ts +43 -0
  12. package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +493 -0
  13. package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +250 -0
  14. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +156 -0
  15. package/client/vendor/paseo-plugin-helper/components/Button.tsx +178 -0
  16. package/client/vendor/paseo-plugin-helper/components/Card.tsx +225 -0
  17. package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +196 -0
  18. package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +277 -0
  19. package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +172 -0
  20. package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +180 -0
  21. package/client/vendor/paseo-plugin-helper/components/DataTable.tsx +200 -0
  22. package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +97 -0
  23. package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +70 -0
  24. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +73 -0
  25. package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +446 -0
  26. package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +247 -0
  27. package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +117 -0
  28. package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +53 -0
  29. package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +118 -0
  30. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +80 -0
  31. package/client/vendor/paseo-plugin-helper/components/Select.tsx +215 -0
  32. package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +80 -0
  33. package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +319 -0
  34. package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +150 -0
  35. package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +163 -0
  36. package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +157 -0
  37. package/client/vendor/paseo-plugin-helper/components/index.ts +25 -0
  38. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +224 -0
  39. package/client/vendor/paseo-plugin-helper/forge-icon.tsx +79 -0
  40. package/client/vendor/paseo-plugin-helper/host.ts +277 -0
  41. package/client/vendor/paseo-plugin-helper/icon.tsx +39 -0
  42. package/client/vendor/paseo-plugin-helper/index.ts +28 -0
  43. package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +49 -0
  44. package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +103 -0
  45. package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +65 -0
  46. package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +378 -0
  47. package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +49 -0
  48. package/client/vendor/paseo-plugin-helper/layout/Row.tsx +39 -0
  49. package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +39 -0
  50. package/client/vendor/paseo-plugin-helper/layout/index.ts +7 -0
  51. package/client/vendor/paseo-plugin-helper/panel.tsx +81 -0
  52. package/client/vendor/paseo-plugin-helper/pill.tsx +884 -0
  53. package/client/vendor/paseo-plugin-helper/query-refresh.ts +79 -0
  54. package/client/vendor/paseo-plugin-helper/query.ts +66 -0
  55. package/client/vendor/paseo-plugin-helper/settings-screen.tsx +372 -0
  56. package/client/vendor/paseo-plugin-helper/settings.ts +181 -0
  57. package/client/vendor/paseo-plugin-helper/shared-settings.ts +46 -0
  58. package/client/vendor/paseo-plugin-helper/snapshot.ts +68 -0
  59. package/client/vendor/paseo-plugin-helper/surface.tsx +80 -0
  60. package/client/vendor/paseo-plugin-helper/theme/color-utils.ts +118 -0
  61. package/client/vendor/paseo-plugin-helper/theme/flair.ts +76 -0
  62. package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +121 -0
  63. package/client/vendor/paseo-plugin-helper/theme/index.ts +7 -0
  64. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +214 -0
  65. package/client/vendor/paseo-plugin-helper/theme/responsive.ts +213 -0
  66. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +161 -0
  67. package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +57 -0
  68. package/client/vendor/paseo-plugin-helper/utils/clipboard.ts +149 -0
  69. package/client/vendor/paseo-plugin-helper/utils/haptics.ts +34 -0
  70. package/client/webhook-card.tsx +198 -0
  71. package/docs/specs/forge-workflow-gui.md +820 -0
  72. package/docs/workflow.md +339 -0
  73. package/examples/README.md +108 -0
  74. package/examples/hook-service/README.md +66 -0
  75. package/examples/hook-service/forge-hook.service +28 -0
  76. package/examples/hook-service/hook-server.mjs +265 -0
  77. package/examples/hook-service/hook.env.example +21 -0
  78. package/examples/labels/README.md +64 -0
  79. package/examples/labels/label-base.yaml +117 -0
  80. package/examples/skills/coding-agent/SKILL.md +262 -0
  81. package/examples/skills/coding-agent-fgjx/SKILL.md +271 -0
  82. package/examples/skills/orchestrator/SKILL.md +133 -0
  83. package/examples/skills/orchestrator-fgjx/SKILL.md +139 -0
  84. package/examples/tools/README.md +68 -0
  85. package/examples/tools/fgjx +464 -0
  86. package/package.json +38 -0
  87. package/paseo-plugin.json +4 -0
  88. package/server/forge-client.ts +484 -0
  89. package/server/forge-guard.ts +70 -0
  90. package/server/git-origin.ts +70 -0
  91. package/server/hook-queue.ts +127 -0
  92. package/server/issues.ts +542 -0
  93. package/server/settings.ts +52 -0
  94. package/server/vendor/paseo-plugin-helper/agent.ts +85 -0
  95. package/server/vendor/paseo-plugin-helper/custom-pills.ts +344 -0
  96. package/server/vendor/paseo-plugin-helper/index.ts +18 -0
  97. package/server/vendor/paseo-plugin-helper/jsonc.ts +78 -0
  98. package/server/vendor/paseo-plugin-helper/logger.ts +210 -0
  99. package/server/vendor/paseo-plugin-helper/mcp-config.ts +367 -0
  100. package/server/vendor/paseo-plugin-helper/mcp-injection.ts +85 -0
  101. package/server/vendor/paseo-plugin-helper/network.ts +91 -0
  102. package/server/vendor/paseo-plugin-helper/plugins.ts +160 -0
  103. package/server/vendor/paseo-plugin-helper/process.ts +186 -0
  104. package/server/vendor/paseo-plugin-helper/redact.ts +86 -0
  105. package/server/vendor/paseo-plugin-helper/rpc-guard.ts +77 -0
  106. package/server/vendor/paseo-plugin-helper/settings.ts +97 -0
  107. package/server/vendor/paseo-plugin-helper/shared-settings.ts +243 -0
  108. package/server/vendor/paseo-plugin-helper/storage.ts +244 -0
  109. package/server/vendor/paseo-plugin-helper/system.ts +128 -0
  110. package/server/vendor/paseo-plugin-helper/task.ts +116 -0
  111. package/server/vendor/paseo-plugin-helper/version.ts +153 -0
  112. package/server/vendor/paseo-plugin-helper/workspace-beacon.ts +418 -0
  113. package/shared/hook-queue.ts +140 -0
  114. package/shared/issues.ts +1750 -0
  115. package/shared/vendor/paseo-plugin-helper/README.md +11 -0
  116. package/shared/vendor/paseo-plugin-helper/async.ts +35 -0
  117. package/shared/vendor/paseo-plugin-helper/custom-pills.ts +169 -0
  118. package/shared/vendor/paseo-plugin-helper/forge.ts +110 -0
  119. package/shared/vendor/paseo-plugin-helper/formatters.ts +271 -0
  120. package/shared/vendor/paseo-plugin-helper/highlight.ts +184 -0
  121. package/shared/vendor/paseo-plugin-helper/index.ts +10 -0
  122. package/shared/vendor/paseo-plugin-helper/rpc.ts +72 -0
  123. package/shared/vendor/paseo-plugin-helper/settings.ts +138 -0
  124. package/shared/vendor/paseo-plugin-helper/suite-settings.ts +17 -0
  125. package/shared/vendor/paseo-plugin-helper/suppressed.ts +31 -0
  126. package/shared/vendor/paseo-plugin-helper/types.ts +36 -0
  127. package/shared/version.ts +2 -0
  128. package/shared/webhook.ts +223 -0
@@ -0,0 +1,150 @@
1
+ import React, { useState, type ComponentType, type Ref } from "react";
2
+ import {
3
+ StyleSheet,
4
+ Text,
5
+ TextInput as RNTextInput,
6
+ View,
7
+ type KeyboardTypeOptions,
8
+ type StyleProp,
9
+ type TextInputProps as RNTextInputProps,
10
+ type TextInput as RNTextInputInstance,
11
+ type TextStyle,
12
+ type ViewStyle,
13
+ } from "react-native";
14
+ import { getOptionalClientHost } from "../host";
15
+ import { usePluginTheme } from "../theme/provider";
16
+
17
+ export interface TextInputProps {
18
+ value: string;
19
+ onChangeText: (text: string) => void;
20
+ label?: string;
21
+ placeholder?: string;
22
+ helperText?: string;
23
+ errorText?: string;
24
+ secureTextEntry?: boolean;
25
+ keyboardType?: KeyboardTypeOptions;
26
+ autoCapitalize?: "none" | "sentences" | "words" | "characters";
27
+ autoCorrect?: boolean;
28
+ disabled?: boolean;
29
+ mono?: boolean;
30
+ multiline?: boolean;
31
+ numberOfLines?: number;
32
+ style?: StyleProp<ViewStyle>;
33
+ inputStyle?: StyleProp<TextStyle>;
34
+ onSubmitEditing?: () => void;
35
+ }
36
+
37
+ export function TextInput({
38
+ value,
39
+ onChangeText,
40
+ label,
41
+ placeholder,
42
+ helperText,
43
+ errorText,
44
+ secureTextEntry = false,
45
+ keyboardType = "default",
46
+ autoCapitalize = "none",
47
+ autoCorrect = false,
48
+ disabled = false,
49
+ mono = false,
50
+ multiline = false,
51
+ numberOfLines = 1,
52
+ style,
53
+ inputStyle,
54
+ onSubmitEditing,
55
+ }: TextInputProps) {
56
+ const { colors, resolveRadius, isCompact, touchTargetMin, alpha } = usePluginTheme();
57
+ const [isFocused, setIsFocused] = useState(false);
58
+ const ResolvedInput = (getOptionalClientHost()?.TextInput ??
59
+ RNTextInput) as ComponentType<RNTextInputProps & { ref?: Ref<RNTextInputInstance> }>;
60
+
61
+ const radius = resolveRadius("md");
62
+ const hasError = Boolean(errorText);
63
+
64
+ const borderColor = hasError
65
+ ? colors.statusDanger
66
+ : isFocused
67
+ ? colors.accent
68
+ : colors.border;
69
+
70
+ const minHeight = multiline ? Math.max(touchTargetMin * 1.5, 64) : touchTargetMin;
71
+
72
+ return (
73
+ <View style={[styles.container, style]}>
74
+ {label ? (
75
+ <Text
76
+ style={[
77
+ styles.label,
78
+ {
79
+ color: hasError ? colors.statusDanger : colors.foreground,
80
+ fontSize: isCompact ? 12 : 13,
81
+ },
82
+ ]}
83
+ >
84
+ {label}
85
+ </Text>
86
+ ) : null}
87
+
88
+ <ResolvedInput
89
+ value={value}
90
+ onChangeText={onChangeText}
91
+ placeholder={placeholder}
92
+ placeholderTextColor={colors.foregroundMuted}
93
+ secureTextEntry={secureTextEntry}
94
+ keyboardType={keyboardType}
95
+ autoCapitalize={autoCapitalize}
96
+ autoCorrect={autoCorrect}
97
+ editable={!disabled}
98
+ multiline={multiline}
99
+ numberOfLines={numberOfLines}
100
+ onFocus={() => setIsFocused(true)}
101
+ onBlur={() => setIsFocused(false)}
102
+ onSubmitEditing={onSubmitEditing}
103
+ style={[
104
+ styles.input,
105
+ {
106
+ color: disabled ? colors.foregroundMuted : colors.foreground,
107
+ backgroundColor: disabled ? alpha(colors.surface1, 0.5) : colors.surface0,
108
+ borderColor,
109
+ borderRadius: radius,
110
+ minHeight,
111
+ paddingVertical: multiline ? 8 : 6,
112
+ paddingHorizontal: 10,
113
+ fontSize: isCompact ? 13 : 14,
114
+ fontFamily: mono ? "monospace" : undefined,
115
+ },
116
+ inputStyle,
117
+ ]}
118
+ />
119
+
120
+ {(errorText || helperText) && (
121
+ <Text
122
+ style={[
123
+ styles.hint,
124
+ {
125
+ color: hasError ? colors.statusDanger : colors.foregroundMuted,
126
+ fontSize: 11,
127
+ },
128
+ ]}
129
+ >
130
+ {errorText || helperText}
131
+ </Text>
132
+ )}
133
+ </View>
134
+ );
135
+ }
136
+
137
+ const styles = StyleSheet.create({
138
+ container: {
139
+ gap: 4,
140
+ },
141
+ label: {
142
+ fontWeight: "600",
143
+ },
144
+ input: {
145
+ borderWidth: 1,
146
+ },
147
+ hint: {
148
+ marginTop: 2,
149
+ },
150
+ });
@@ -0,0 +1,163 @@
1
+ import React from "react";
2
+ import {
3
+ Pressable,
4
+ StyleSheet,
5
+ Text,
6
+ View,
7
+ type StyleProp,
8
+ type TextStyle,
9
+ type ViewStyle,
10
+ } from "react-native";
11
+ import { usePluginTheme } from "../theme/provider";
12
+ import { resolveElevation } from "../theme/tokens";
13
+
14
+ export interface ToggleProps {
15
+ value: boolean;
16
+ onValueChange: (next: boolean) => void;
17
+ label?: string;
18
+ description?: string;
19
+ disabled?: boolean;
20
+ style?: StyleProp<ViewStyle>;
21
+ labelStyle?: StyleProp<TextStyle>;
22
+ }
23
+
24
+ export function Toggle({
25
+ value,
26
+ onValueChange,
27
+ label,
28
+ description,
29
+ disabled = false,
30
+ style,
31
+ labelStyle,
32
+ }: ToggleProps) {
33
+ const { colors, touchTargetMin, isCompact, alpha } = usePluginTheme();
34
+
35
+ const handlePress = () => {
36
+ if (!disabled) {
37
+ onValueChange(!value);
38
+ }
39
+ };
40
+
41
+ const trackWidth = 38;
42
+ const trackHeight = 22;
43
+ const thumbSize = 16;
44
+ const thumbPadding = 3;
45
+
46
+ const trackColor = value
47
+ ? colors.accent
48
+ : alpha(colors.foregroundMuted, 0.35);
49
+
50
+ const thumbPosition = value
51
+ ? trackWidth - thumbSize - thumbPadding
52
+ : thumbPadding;
53
+
54
+ const hasText = Boolean(label || description);
55
+
56
+ return (
57
+ <Pressable
58
+ onPress={handlePress}
59
+ disabled={disabled}
60
+ hitSlop={Math.max(0, (touchTargetMin - trackHeight) / 2)}
61
+ style={({ pressed }) => [
62
+ styles.container,
63
+ !hasText && styles.bareContainer,
64
+ {
65
+ minHeight: touchTargetMin,
66
+ opacity: disabled ? 0.5 : pressed ? 0.8 : 1,
67
+ },
68
+ style,
69
+ ]}
70
+ >
71
+ {(label || description) && (
72
+ <View style={styles.textContainer}>
73
+ {label && (
74
+ <Text
75
+ style={[
76
+ styles.label,
77
+ {
78
+ color: colors.foreground,
79
+ fontSize: isCompact ? 13 : 14,
80
+ },
81
+ labelStyle,
82
+ ]}
83
+ >
84
+ {label}
85
+ </Text>
86
+ )}
87
+ {description && (
88
+ <Text
89
+ style={[
90
+ styles.description,
91
+ {
92
+ color: colors.foregroundMuted,
93
+ fontSize: 11,
94
+ },
95
+ ]}
96
+ >
97
+ {description}
98
+ </Text>
99
+ )}
100
+ </View>
101
+ )}
102
+
103
+ <View
104
+ style={[
105
+ styles.track,
106
+ {
107
+ width: trackWidth,
108
+ height: trackHeight,
109
+ borderRadius: trackHeight / 2,
110
+ backgroundColor: trackColor,
111
+ },
112
+ ]}
113
+ >
114
+ <View
115
+ style={[
116
+ styles.thumb,
117
+ {
118
+ width: thumbSize,
119
+ height: thumbSize,
120
+ borderRadius: thumbSize / 2,
121
+ backgroundColor: colors.surface0,
122
+ transform: [{ translateX: thumbPosition }],
123
+ },
124
+ ]}
125
+ />
126
+ </View>
127
+ </Pressable>
128
+ );
129
+ }
130
+
131
+ const styles = StyleSheet.create({
132
+ container: {
133
+ flexDirection: "row",
134
+ alignItems: "center",
135
+ justifyContent: "space-between",
136
+ gap: 12,
137
+ width: "100%",
138
+ },
139
+ bareContainer: {
140
+ width: "auto",
141
+ alignSelf: "flex-start",
142
+ flexShrink: 0,
143
+ justifyContent: "flex-start",
144
+ },
145
+ textContainer: {
146
+ flex: 1,
147
+ minWidth: 0,
148
+ gap: 2,
149
+ },
150
+ label: {
151
+ fontWeight: "500",
152
+ },
153
+ description: {
154
+ lineHeight: 15,
155
+ },
156
+ track: {
157
+ justifyContent: "center",
158
+ flexShrink: 0,
159
+ },
160
+ thumb: {
161
+ ...resolveElevation("sm"),
162
+ },
163
+ });
@@ -0,0 +1,157 @@
1
+ import React, { useState } from "react";
2
+ import {
3
+ View,
4
+ Text,
5
+ Pressable,
6
+ StyleSheet,
7
+ StyleProp,
8
+ ViewStyle,
9
+ TextStyle,
10
+ Platform,
11
+ } from "react-native";
12
+ import { usePluginTheme } from "../theme/index";
13
+ import { getClientHost } from "../host";
14
+ import { copyToClipboard } from "../utils/clipboard";
15
+ import {
16
+ truncate,
17
+ truncateMiddle,
18
+ truncatePath,
19
+ TruncatePathOptions,
20
+ } from "../../../../shared/vendor/paseo-plugin-helper/formatters";
21
+
22
+ export type TruncateMode = "end" | "middle" | "path";
23
+
24
+ export interface TruncatedTextProps {
25
+ /** The full, raw text string (e.g. UUID, file path, commit SHA, token) */
26
+ text: string;
27
+ /** Maximum length allowed before truncation. Default: 32 */
28
+ maxLength?: number;
29
+ /** Truncation algorithm: "middle" (UUIDs/hashes), "path" (directory paths), or "end" (standard). Default: "middle" */
30
+ mode?: TruncateMode;
31
+ /** Additional path truncation options when mode="path" */
32
+ pathOptions?: TruncatePathOptions;
33
+ /** Whether to render an inline copy button. Default: true */
34
+ copyable?: boolean;
35
+ /** Use monospace font. Default: true */
36
+ mono?: boolean;
37
+ /** Toast message on successful copy. Default: "Copied" */
38
+ toastMessage?: string;
39
+ /** Custom container style */
40
+ style?: StyleProp<ViewStyle>;
41
+ /** Custom text style */
42
+ textStyle?: StyleProp<TextStyle>;
43
+ }
44
+
45
+ /**
46
+ * Renders long strings (paths, UUIDs, hashes) shortened with smart truncation,
47
+ * while preserving the full untruncated string for one-click clipboard copying.
48
+ */
49
+ export function TruncatedText({
50
+ text,
51
+ maxLength = 32,
52
+ mode = "middle",
53
+ pathOptions,
54
+ copyable = true,
55
+ mono = true,
56
+ toastMessage = "Copied",
57
+ style,
58
+ textStyle,
59
+ }: TruncatedTextProps) {
60
+ const { colors, fonts, isCompact, touchTargetMin } = usePluginTheme();
61
+ const { Icon, useToast } = getClientHost();
62
+ const toast = useToast();
63
+ const [copied, setCopied] = useState(false);
64
+
65
+ if (!text) {
66
+ return <Text style={[{ color: colors.foregroundMuted }, textStyle]}>-</Text>;
67
+ }
68
+
69
+ let formattedText = text;
70
+ if (text.length > maxLength) {
71
+ switch (mode) {
72
+ case "path":
73
+ formattedText = truncatePath(text, maxLength, pathOptions);
74
+ break;
75
+ case "end":
76
+ formattedText = truncate(text, maxLength);
77
+ break;
78
+ case "middle":
79
+ default:
80
+ formattedText = truncateMiddle(text, maxLength);
81
+ break;
82
+ }
83
+ }
84
+
85
+ const handleCopy = async () => {
86
+ if (!copyable || !text) return;
87
+ const ok = await copyToClipboard(text, {
88
+ toast,
89
+ toastMessage,
90
+ });
91
+ if (ok) {
92
+ setCopied(true);
93
+ setTimeout(() => setCopied(false), 2000);
94
+ }
95
+ };
96
+
97
+ const fontFamily = mono
98
+ ? fonts.mono ?? Platform.select({ ios: "Menlo", android: "monospace", default: "monospace" })
99
+ : undefined;
100
+
101
+ return (
102
+ <View style={[styles.container, style]}>
103
+ <Text
104
+ selectable
105
+ numberOfLines={1}
106
+ ellipsizeMode="clip"
107
+ accessibilityLabel={text}
108
+ style={[
109
+ styles.text,
110
+ {
111
+ color: colors.foreground,
112
+ fontFamily,
113
+ },
114
+ textStyle,
115
+ ]}
116
+ >
117
+ {formattedText}
118
+ </Text>
119
+
120
+ {copyable && (
121
+ <Pressable
122
+ onPress={handleCopy}
123
+ hitSlop={Math.max(8, (touchTargetMin - 20) / 2)}
124
+ style={styles.copyBtn}
125
+ accessibilityRole="button"
126
+ accessibilityLabel={`Copy ${text}`}
127
+ >
128
+ <Icon
129
+ name={copied ? "Check" : "Copy"}
130
+ size={isCompact ? 12 : 13}
131
+ color={copied ? colors.statusSuccess : colors.foregroundMuted}
132
+ />
133
+ </Pressable>
134
+ )}
135
+ </View>
136
+ );
137
+ }
138
+
139
+ const styles = StyleSheet.create({
140
+ container: {
141
+ flexDirection: "row",
142
+ alignItems: "center",
143
+ maxWidth: "100%",
144
+ gap: 6,
145
+ },
146
+ text: {
147
+ fontSize: 12,
148
+ lineHeight: 18,
149
+ flexShrink: 1,
150
+ },
151
+ copyBtn: {
152
+ padding: 3,
153
+ borderRadius: 4,
154
+ justifyContent: "center",
155
+ alignItems: "center",
156
+ },
157
+ });
@@ -0,0 +1,25 @@
1
+ export * from "./Button";
2
+ export * from "./InlineButton";
3
+ export * from "./AttentionBeacon";
4
+ export * from "./Badge";
5
+ export * from "./StatusDot";
6
+ export * from "./Card";
7
+ export * from "./Tabs";
8
+ export * from "./CodeBlock";
9
+ export * from "./CopyButton";
10
+ export * from "./SearchInput";
11
+ export * from "./TextInput";
12
+ export * from "./Select";
13
+ export * from "./Toggle";
14
+ export * from "./Collapsible";
15
+ export * from "./ProgressBar";
16
+ export * from "./MetricGauge";
17
+ export * from "./DataTable";
18
+ export * from "./KeyValue";
19
+ export * from "./EmptyState";
20
+ export * from "./Responsive";
21
+ export * from "./AboutSection";
22
+ export * from "./TruncatedText";
23
+ export * from "./CommandBox";
24
+ export * from "./SectionHeader";
25
+ export * from "./HighlightedText";
@@ -0,0 +1,224 @@
1
+ import React, { useState, useCallback } from "react";
2
+ import { StyleSheet, Text, View } from "react-native";
3
+ import type { ComposerPillRegistrar, PluginCleanup } from "./host";
4
+ import { getClientHost } from "./host";
5
+ import { usePluginTheme } from "./theme/provider";
6
+ import { useResponsive } from "./theme/useResponsive";
7
+ import type { VisualFlair } from "./theme/flair";
8
+ import type { CustomPillState } from "../../../shared/vendor/paseo-plugin-helper/custom-pills";
9
+ import { Badge } from "./components/Badge";
10
+ import { Button } from "./components/Button";
11
+ import { Card } from "./components/Card";
12
+ import { CodeBlock } from "./components/CodeBlock";
13
+ import { registerComposerPill } from "./pill";
14
+
15
+ export interface CustomPillBodyProps {
16
+ state: CustomPillState;
17
+ }
18
+
19
+ /**
20
+ * Standard pill body renderer for a custom metric pill in the composer trackbar.
21
+ * Automatically adapts to responsive compact/mobile modes and shows threshold status.
22
+ */
23
+ export function CustomPillBody({ state }: CustomPillBodyProps) {
24
+ const { Icon } = getClientHost();
25
+ const { colors } = usePluginTheme();
26
+ const { isCompact } = useResponsive();
27
+
28
+ const title = isCompact && state.compactTitle ? state.compactTitle : state.title;
29
+ const icon = isCompact && state.compactIcon ? state.compactIcon : state.icon;
30
+
31
+ return (
32
+ <View style={styles.pillContainer}>
33
+ {icon && <Icon name={icon} size={13} color={colors.foreground} />}
34
+ {title ? (
35
+ <Text style={[styles.pillTitle, { color: colors.foreground }]}>{title}</Text>
36
+ ) : null}
37
+ <Badge
38
+ label={state.displayValue}
39
+ variant={state.status}
40
+ styleVariant="tinted"
41
+ style={styles.pillBadge}
42
+ />
43
+ </View>
44
+ );
45
+ }
46
+
47
+ export interface CustomPillModalContentProps {
48
+ state: CustomPillState;
49
+ onRefresh?: () => Promise<void> | void;
50
+ isRefreshing?: boolean;
51
+ }
52
+
53
+ /**
54
+ * Full modal inspection content for a custom metric pill.
55
+ * Shows status, preformatted command output, last updated time, and quick actions.
56
+ *
57
+ * Sized by the host: the root fills the host-allocated modal frame (flex/fluid)
58
+ * so changing output never drives the dialog size.
59
+ */
60
+ export function CustomPillModalContent({
61
+ state,
62
+ onRefresh,
63
+ isRefreshing = false,
64
+ }: CustomPillModalContentProps) {
65
+ const { colors, isCompact } = usePluginTheme();
66
+
67
+ const displayText =
68
+ state.modalOutput || state.rawValue || (state.error ? `Error: ${state.error}` : "No output");
69
+
70
+ return (
71
+ <View style={styles.modalContent}>
72
+ <Card>
73
+ <Card.Header
74
+ title={state.modalTitle ?? state.title}
75
+ subtitle={state.modalDescription}
76
+ icon={state.icon}
77
+ badge={<Badge label={state.displayValue} variant={state.status} />}
78
+ action={
79
+ onRefresh ? (
80
+ <Button
81
+ variant="secondary"
82
+ size="sm"
83
+ icon="RefreshCw"
84
+ loading={isRefreshing}
85
+ label={!isCompact ? "Refresh" : undefined}
86
+ onPress={() => onRefresh()}
87
+ />
88
+ ) : undefined
89
+ }
90
+ />
91
+
92
+ {/* Monospace Code & Output Box with Built-in Copy */}
93
+ <CodeBlock
94
+ code={displayText}
95
+ title={state.modalTitle ?? state.title}
96
+ maxHeight={280}
97
+ copyable={true}
98
+ />
99
+
100
+ {/* Footer info */}
101
+ <View style={styles.footerRow}>
102
+ <Text style={[styles.timestampText, { color: colors.foregroundMuted }]}>
103
+ Last updated: {new Date(state.lastUpdated).toLocaleTimeString()}
104
+ </Text>
105
+ </View>
106
+ </Card>
107
+ </View>
108
+ );
109
+ }
110
+
111
+ export interface RegisterCustomPillsOptions {
112
+ /**
113
+ * The list of custom pill states or definitions.
114
+ */
115
+ pills: CustomPillState[];
116
+
117
+ /**
118
+ * Callback invoked when a pill needs a fresh refresh or modal command run.
119
+ */
120
+ onRefreshModal?: (pillId: string) => Promise<{ output?: string; error?: string }>;
121
+
122
+ /**
123
+ * Optional visual flair overrides.
124
+ */
125
+ flair?: Partial<VisualFlair>;
126
+ }
127
+
128
+ /**
129
+ * Registers one or more declarative custom metric pills into Paseo's composer trackbar.
130
+ * Automatically handles pill lifecycle, responsive layouts, and drill-down inspection modals.
131
+ */
132
+ export function registerCustomPills(
133
+ client: ComposerPillRegistrar,
134
+ options: RegisterCustomPillsOptions,
135
+ ): PluginCleanup {
136
+ const cleanups: PluginCleanup[] = [];
137
+
138
+ for (const pill of options.pills) {
139
+ const cleanup = registerComposerPill(client, {
140
+ id: pill.id,
141
+ title: pill.title,
142
+ compactTitle: pill.compactTitle,
143
+ icon: pill.icon,
144
+ compactIcon: pill.compactIcon,
145
+ flair: options.flair,
146
+ resolveLabel: () =>
147
+ pill.displayValue ? `${pill.title} ${pill.displayValue}` : pill.title,
148
+ renderPill: () => <CustomPillBody state={pill} />,
149
+ renderModal: () => {
150
+ const [refreshing, setRefreshing] = useState(false);
151
+ const [currentOutput, setCurrentOutput] = useState<string | undefined>(
152
+ pill.modalOutput,
153
+ );
154
+
155
+ const handleRefresh = useCallback(async () => {
156
+ if (!options.onRefreshModal) return;
157
+ setRefreshing(true);
158
+ try {
159
+ const res = await options.onRefreshModal(pill.id);
160
+ if (res.output) {
161
+ setCurrentOutput(res.output);
162
+ }
163
+ } finally {
164
+ setRefreshing(false);
165
+ }
166
+ }, [pill.id]);
167
+
168
+ const activeState: CustomPillState = {
169
+ ...pill,
170
+ modalOutput: currentOutput ?? pill.modalOutput,
171
+ };
172
+
173
+ return (
174
+ <CustomPillModalContent
175
+ state={activeState}
176
+ onRefresh={options.onRefreshModal ? handleRefresh : undefined}
177
+ isRefreshing={refreshing}
178
+ />
179
+ );
180
+ },
181
+ });
182
+
183
+ cleanups.push(cleanup);
184
+ }
185
+
186
+ return () => {
187
+ for (const dispose of cleanups) {
188
+ dispose();
189
+ }
190
+ };
191
+ }
192
+
193
+ const styles = StyleSheet.create({
194
+ modalContent: {
195
+ flex: 1,
196
+ minHeight: 0,
197
+ width: "100%",
198
+ padding: 12,
199
+ },
200
+ pillContainer: {
201
+ flexDirection: "row",
202
+ alignItems: "center",
203
+ gap: 6,
204
+ paddingHorizontal: 8,
205
+ paddingVertical: 3,
206
+ },
207
+ pillTitle: {
208
+ fontSize: 12,
209
+ fontWeight: "500",
210
+ },
211
+ pillBadge: {
212
+ paddingVertical: 1,
213
+ paddingHorizontal: 5,
214
+ },
215
+ footerRow: {
216
+ flexDirection: "row",
217
+ justifyContent: "flex-end",
218
+ alignItems: "center",
219
+ marginTop: 8,
220
+ },
221
+ timestampText: {
222
+ fontSize: 11,
223
+ },
224
+ });