@xpufx/paseo-top 0.4.2 → 0.4.4
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/README.md +11 -12
- package/client/multi-host-view.tsx +177 -0
- package/client/multi-host.ts +98 -0
- package/client/pill-labels.ts +30 -6
- package/client/pill.tsx +42 -61
- package/client/settings-events.ts +33 -0
- package/client/surface.tsx +10 -9
- package/client/vendor/paseo-plugin-helper/components/Badge.tsx +12 -0
- package/client/vendor/paseo-plugin-helper/components/Button.tsx +29 -3
- package/client/vendor/paseo-plugin-helper/components/Card.tsx +32 -0
- package/client/vendor/paseo-plugin-helper/components/DataTable.tsx +17 -1
- package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +17 -2
- package/client/vendor/paseo-plugin-helper/components/InteractiveRow.tsx +142 -0
- package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +16 -2
- package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +7 -0
- package/client/vendor/paseo-plugin-helper/components/Select.tsx +131 -63
- package/client/vendor/paseo-plugin-helper/components/index.ts +1 -0
- package/client/vendor/paseo-plugin-helper/custom-pills.tsx +4 -3
- package/client/vendor/paseo-plugin-helper/host.ts +12 -58
- package/client/vendor/paseo-plugin-helper/index.ts +2 -0
- package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +19 -29
- package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +24 -22
- package/client/vendor/paseo-plugin-helper/panel.tsx +11 -3
- package/client/vendor/paseo-plugin-helper/pill.tsx +135 -339
- package/client/vendor/paseo-plugin-helper/styles/index.ts +1 -0
- package/client/vendor/paseo-plugin-helper/styles/recipes.ts +550 -0
- package/client/vendor/paseo-plugin-helper/surface.tsx +62 -2
- package/client/vendor/paseo-plugin-helper/theme/provider.tsx +3 -39
- package/client/vendor/paseo-plugin-helper/theme/tokens.ts +40 -1
- package/client/vendor/paseo-plugin-helper/tickets.tsx +766 -0
- package/index.server.ts +7 -0
- package/package.json +12 -3
- package/paseo-plugin.json +1 -1
- package/server/vendor/paseo-plugin-helper/index.ts +2 -0
- package/server/vendor/paseo-plugin-helper/mcp-injection.ts +20 -2
- package/server/vendor/paseo-plugin-helper/plugins.ts +184 -19
- package/server/vendor/paseo-plugin-helper/tickets.ts +1281 -0
- package/shared/helper-version.ts +20 -0
- package/shared/multi-host.ts +348 -0
- package/shared/resources.ts +1 -1
- package/shared/vendor/paseo-plugin-helper/index.ts +1 -0
- package/shared/vendor/paseo-plugin-helper/tickets.ts +1010 -0
- package/shared/version.ts +1 -1
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { shouldEmitSnapshotUpdate } from "./vendor/paseo-plugin-helper/index";
|
|
2
|
+
import type { TopSettings } from "../shared/resources";
|
|
3
|
+
|
|
4
|
+
export type SettingsListener = (settings: TopSettings) => void;
|
|
5
|
+
const settingsListeners = new Set<SettingsListener>();
|
|
6
|
+
let lastNotifiedSettings: TopSettings | null = null;
|
|
7
|
+
|
|
8
|
+
export function addSettingsListener(listener: SettingsListener): void {
|
|
9
|
+
settingsListeners.add(listener);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function removeSettingsListener(listener: SettingsListener): void {
|
|
13
|
+
settingsListeners.delete(listener);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function notifySettingsChanged(settings: TopSettings): void {
|
|
17
|
+
if (
|
|
18
|
+
!shouldEmitSnapshotUpdate(
|
|
19
|
+
lastNotifiedSettings as unknown as Record<string, unknown> | null,
|
|
20
|
+
settings as unknown as Record<string, unknown>,
|
|
21
|
+
)
|
|
22
|
+
) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
lastNotifiedSettings = settings;
|
|
26
|
+
for (const listener of settingsListeners) {
|
|
27
|
+
try {
|
|
28
|
+
listener(settings);
|
|
29
|
+
} catch {
|
|
30
|
+
// Ignore listener errors
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
package/client/surface.tsx
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
usePluginTheme,
|
|
19
19
|
} from "./vendor/paseo-plugin-helper/index";
|
|
20
20
|
import { formatBytes, formatUptime } from "../shared/vendor/paseo-plugin-helper/index";
|
|
21
|
+
import { PermissionAuditView } from "permission-audit/client";
|
|
21
22
|
import {
|
|
22
23
|
topSettingsContract,
|
|
23
24
|
checkboxesFromTarget,
|
|
@@ -27,14 +28,17 @@ import {
|
|
|
27
28
|
type TopSettings,
|
|
28
29
|
} from "../shared/resources";
|
|
29
30
|
import { PLUGIN_VERSION } from "../shared/version";
|
|
30
|
-
import { notifySettingsChanged } from "./
|
|
31
|
+
import { notifySettingsChanged } from "./settings-events";
|
|
31
32
|
import { ChoiceChips } from "./settings-ui";
|
|
32
33
|
import { useTopResourceQuery } from "./resources-query";
|
|
34
|
+
import { FleetView } from "./multi-host-view";
|
|
33
35
|
|
|
34
|
-
type SurfaceTab = "system" | "settings" | "about";
|
|
36
|
+
type SurfaceTab = "system" | "fleet" | "permissions" | "settings" | "about";
|
|
35
37
|
|
|
36
38
|
const TABS = [
|
|
37
39
|
{ id: "system", label: "Activity", shortLabel: "Activity", icon: "Activity" },
|
|
40
|
+
{ id: "fleet", label: "Fleet", shortLabel: "Fleet", icon: "Server" },
|
|
41
|
+
{ id: "permissions", label: "Permissions", shortLabel: "Permissions", icon: "ShieldCheck" },
|
|
38
42
|
{ id: "settings", label: "Settings", shortLabel: "Settings", icon: "Sliders" },
|
|
39
43
|
{ id: "about", label: "About", shortLabel: "About", icon: "Info" },
|
|
40
44
|
];
|
|
@@ -47,12 +51,6 @@ const TIMELINE_CADENCE_OPTIONS = Array.from({ length: 11 }, (_, n) => ({
|
|
|
47
51
|
const CPU_THRESHOLDS = { warning: 60, danger: 85 };
|
|
48
52
|
const MEM_THRESHOLDS = { warning: 70, danger: 85 };
|
|
49
53
|
|
|
50
|
-
// Keep the dashboard a readable centered column instead of stretching
|
|
51
|
-
// edge-to-edge on large viewports. The cap lives in the helper
|
|
52
|
-
// (`ModalBody maxContentWidth`); this is the only place the surface picks
|
|
53
|
-
// the value.
|
|
54
|
-
const TOP_CONTENT_MAX_WIDTH = 600;
|
|
55
|
-
|
|
56
54
|
export function TopDashboardSurface(_props: PluginSurfaceProps) {
|
|
57
55
|
const { colors } = usePluginTheme();
|
|
58
56
|
const [activeTab, setActiveTab] = useState<SurfaceTab>("system");
|
|
@@ -83,7 +81,6 @@ export function TopDashboardSurface(_props: PluginSurfaceProps) {
|
|
|
83
81
|
<View style={[styles.root, { backgroundColor: colors.surface0 }]}>
|
|
84
82
|
<ModalBody
|
|
85
83
|
headerMode="pinned"
|
|
86
|
-
maxContentWidth={TOP_CONTENT_MAX_WIDTH}
|
|
87
84
|
header={
|
|
88
85
|
<Tabs
|
|
89
86
|
tabs={TABS}
|
|
@@ -177,6 +174,10 @@ export function TopDashboardSurface(_props: PluginSurfaceProps) {
|
|
|
177
174
|
</Stack>
|
|
178
175
|
)}
|
|
179
176
|
|
|
177
|
+
{activeTab === "fleet" && <FleetView />}
|
|
178
|
+
|
|
179
|
+
{activeTab === "permissions" && <PermissionAuditView variant="compact" />}
|
|
180
|
+
|
|
180
181
|
{activeTab === "settings" && (
|
|
181
182
|
<Stack gap={12}>
|
|
182
183
|
<Card variant="elevated">
|
|
@@ -112,6 +112,9 @@ export function Badge({
|
|
|
112
112
|
>
|
|
113
113
|
{renderIcon()}
|
|
114
114
|
<Text
|
|
115
|
+
accessibilityLabel={label}
|
|
116
|
+
numberOfLines={1}
|
|
117
|
+
ellipsizeMode="tail"
|
|
115
118
|
style={[
|
|
116
119
|
styles.text,
|
|
117
120
|
{
|
|
@@ -128,6 +131,7 @@ export function Badge({
|
|
|
128
131
|
text={label}
|
|
129
132
|
query={highlightQuery}
|
|
130
133
|
fuzzyFallback={highlightFuzzyFallback}
|
|
134
|
+
numberOfLines={1}
|
|
131
135
|
/>
|
|
132
136
|
) : (
|
|
133
137
|
label
|
|
@@ -144,9 +148,17 @@ const styles = StyleSheet.create({
|
|
|
144
148
|
alignSelf: "flex-start",
|
|
145
149
|
borderWidth: 1,
|
|
146
150
|
gap: 4,
|
|
151
|
+
// A badge carries operator-supplied strings (ids, paths, refs, model names)
|
|
152
|
+
// of unbounded length. Yoga defaults `flexShrink` to 0, so without this the
|
|
153
|
+
// chip insists on its full text width and pushes its row past the viewport
|
|
154
|
+
// instead of truncating.
|
|
155
|
+
flexShrink: 1,
|
|
156
|
+
maxWidth: "100%",
|
|
147
157
|
},
|
|
148
158
|
text: {
|
|
149
159
|
fontWeight: "600",
|
|
160
|
+
// Truncation only engages if the label itself is allowed to compress.
|
|
161
|
+
flexShrink: 1,
|
|
150
162
|
},
|
|
151
163
|
dot: {
|
|
152
164
|
width: 6,
|
|
@@ -33,6 +33,7 @@ export function resolveButtonAttentionTone(variant: ButtonVariant): AttentionBea
|
|
|
33
33
|
|
|
34
34
|
export interface ButtonProps {
|
|
35
35
|
label?: string;
|
|
36
|
+
children?: ReactNode;
|
|
36
37
|
variant?: ButtonVariant;
|
|
37
38
|
size?: ButtonSize;
|
|
38
39
|
icon?: string | ReactNode;
|
|
@@ -43,11 +44,13 @@ export interface ButtonProps {
|
|
|
43
44
|
style?: StyleProp<ViewStyle>;
|
|
44
45
|
textStyle?: StyleProp<TextStyle>;
|
|
45
46
|
accessibilityLabel?: string;
|
|
47
|
+
accessibilityRole?: "button" | "link";
|
|
46
48
|
attention?: ButtonAttention;
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
export function Button({
|
|
50
52
|
label,
|
|
53
|
+
children,
|
|
51
54
|
variant = "secondary",
|
|
52
55
|
size = "md",
|
|
53
56
|
icon,
|
|
@@ -58,6 +61,7 @@ export function Button({
|
|
|
58
61
|
style,
|
|
59
62
|
textStyle,
|
|
60
63
|
accessibilityLabel,
|
|
64
|
+
accessibilityRole = "button",
|
|
61
65
|
attention,
|
|
62
66
|
}: ButtonProps) {
|
|
63
67
|
const { Icon } = getClientHost();
|
|
@@ -108,13 +112,25 @@ export function Button({
|
|
|
108
112
|
|
|
109
113
|
const attentionMode = resolveButtonAttentionMode(attention);
|
|
110
114
|
|
|
115
|
+
// Accessibility touch targets are met with `hitSlop`, which expands the
|
|
116
|
+
// tappable area without moving a single neighbouring pixel. They are NOT met
|
|
117
|
+
// by inflating the painted box: a `minHeight` floor forced `size="sm"` to
|
|
118
|
+
// render 40px tall on mobile against a ~26px recipe, because the floor was
|
|
119
|
+
// computed from the touch target rather than from the size (#647).
|
|
120
|
+
//
|
|
121
|
+
// The slop has to be derived from this button's own height, not a fixed 32 —
|
|
122
|
+
// a 32px baseline silently under-serves every smaller recipe, so `sm` got
|
|
123
|
+
// both a too-tall box and too little slop.
|
|
124
|
+
const contentHeight = Math.round(fontSize * 1.2) + py * 2;
|
|
125
|
+
const hitSlopSide = Math.max(0, (touchTargetMin - contentHeight) / 2);
|
|
126
|
+
|
|
111
127
|
const pressable = (
|
|
112
128
|
<Pressable
|
|
113
129
|
onPress={onPress}
|
|
114
130
|
disabled={disabled || loading}
|
|
115
|
-
accessibilityRole=
|
|
131
|
+
accessibilityRole={accessibilityRole}
|
|
116
132
|
accessibilityLabel={accessibilityLabel || label}
|
|
117
|
-
hitSlop={
|
|
133
|
+
hitSlop={hitSlopSide}
|
|
118
134
|
style={({ pressed }) => [
|
|
119
135
|
styles.base,
|
|
120
136
|
{
|
|
@@ -124,7 +140,6 @@ export function Button({
|
|
|
124
140
|
borderRadius: radius,
|
|
125
141
|
paddingVertical: py,
|
|
126
142
|
paddingHorizontal: px,
|
|
127
|
-
minHeight: Math.max(30, touchTargetMin - 4),
|
|
128
143
|
opacity: disabled ? 0.45 : 1,
|
|
129
144
|
},
|
|
130
145
|
style,
|
|
@@ -132,11 +147,15 @@ export function Button({
|
|
|
132
147
|
>
|
|
133
148
|
{loading ? (
|
|
134
149
|
<ActivityIndicator size="small" color={textColor} />
|
|
150
|
+
) : children ? (
|
|
151
|
+
children
|
|
135
152
|
) : (
|
|
136
153
|
<>
|
|
137
154
|
{iconPosition === "left" && renderIcon()}
|
|
138
155
|
{label ? (
|
|
139
156
|
<Text
|
|
157
|
+
numberOfLines={1}
|
|
158
|
+
ellipsizeMode="tail"
|
|
140
159
|
style={[
|
|
141
160
|
styles.text,
|
|
142
161
|
{
|
|
@@ -170,9 +189,16 @@ const styles = StyleSheet.create({
|
|
|
170
189
|
alignItems: "center",
|
|
171
190
|
justifyContent: "center",
|
|
172
191
|
gap: 6,
|
|
192
|
+
// Labels are caller-supplied and unbounded (issue titles, branch names,
|
|
193
|
+
// sort labels). Yoga defaults `flexShrink` to 0, so an unconstrained
|
|
194
|
+
// button claims its full label width and overflows narrow viewports.
|
|
195
|
+
flexShrink: 1,
|
|
196
|
+
maxWidth: "100%",
|
|
173
197
|
},
|
|
174
198
|
text: {
|
|
175
199
|
fontWeight: "600",
|
|
176
200
|
textAlign: "center",
|
|
201
|
+
// The label must be allowed to compress for `numberOfLines` to engage.
|
|
202
|
+
flexShrink: 1,
|
|
177
203
|
},
|
|
178
204
|
});
|
|
@@ -30,6 +30,11 @@ export interface CardHeaderProps {
|
|
|
30
30
|
highlightQuery?: string;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// TEMPORARY DIAGNOSTIC (#555) — one-shot probe logging the host icon the
|
|
34
|
+
// first CardHeader render resolves, right before it is used; strip once the
|
|
35
|
+
// missing-binding cause is confirmed.
|
|
36
|
+
let cardHeaderIconProbeFired = false;
|
|
37
|
+
|
|
33
38
|
export function CardHeader({
|
|
34
39
|
title,
|
|
35
40
|
subtitle,
|
|
@@ -43,6 +48,33 @@ export function CardHeader({
|
|
|
43
48
|
highlightQuery,
|
|
44
49
|
}: CardHeaderProps) {
|
|
45
50
|
const { Icon } = getClientHost();
|
|
51
|
+
// TEMPORARY DIAGNOSTIC (#555) — strip once the missing-binding cause is
|
|
52
|
+
// confirmed. A probe that throws must never crash the header itself.
|
|
53
|
+
try {
|
|
54
|
+
if (!cardHeaderIconProbeFired) {
|
|
55
|
+
cardHeaderIconProbeFired = true;
|
|
56
|
+
const describe = (value: unknown): string => {
|
|
57
|
+
if (value === undefined) return "undefined";
|
|
58
|
+
if (value === null) return "null";
|
|
59
|
+
if (typeof value === "function") {
|
|
60
|
+
const named = (value as { displayName?: string; name?: string }).displayName
|
|
61
|
+
?? (value as { name?: string }).name;
|
|
62
|
+
return `function(${named || "(anonymous)"})`;
|
|
63
|
+
}
|
|
64
|
+
if (typeof value === "object") {
|
|
65
|
+
const symbolTag = (value as { $$typeof?: unknown }).$$typeof;
|
|
66
|
+
return `object($$typeof=${String(symbolTag)}, render=${typeof (value as { render?: unknown }).render})`;
|
|
67
|
+
}
|
|
68
|
+
return typeof value;
|
|
69
|
+
};
|
|
70
|
+
console.warn(
|
|
71
|
+
"[2fado] CardHeader probe: icon=", icon ?? "(no icon prop)",
|
|
72
|
+
"resolved host Icon=", `typeofIcon=${typeof Icon}`, describe(Icon),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.warn("[2fado] CardHeader probe failed:", err);
|
|
77
|
+
}
|
|
46
78
|
const { colors, flair, typography, padding, isCompact } = usePluginTheme();
|
|
47
79
|
|
|
48
80
|
return (
|
|
@@ -104,7 +104,11 @@ export function DataTable<T>({
|
|
|
104
104
|
: styles.alignLeft,
|
|
105
105
|
]}
|
|
106
106
|
>
|
|
107
|
-
<Text
|
|
107
|
+
<Text
|
|
108
|
+
numberOfLines={1}
|
|
109
|
+
ellipsizeMode="tail"
|
|
110
|
+
style={[styles.headerText, { color: colors.foregroundMuted }]}
|
|
111
|
+
>
|
|
108
112
|
{col.header}
|
|
109
113
|
</Text>
|
|
110
114
|
</View>
|
|
@@ -162,6 +166,11 @@ const styles = StyleSheet.create({
|
|
|
162
166
|
},
|
|
163
167
|
cell: {
|
|
164
168
|
justifyContent: "center",
|
|
169
|
+
// Yoga defaults `flexShrink` to 0, so without this a cell whose content
|
|
170
|
+
// carries a long string (a title, a path) refuses to give up width and
|
|
171
|
+
// squeezes its sibling columns instead of truncating.
|
|
172
|
+
flexShrink: 1,
|
|
173
|
+
maxWidth: "100%",
|
|
165
174
|
},
|
|
166
175
|
alignLeft: {
|
|
167
176
|
alignItems: "flex-start",
|
|
@@ -177,6 +186,7 @@ const styles = StyleSheet.create({
|
|
|
177
186
|
fontWeight: "600",
|
|
178
187
|
textTransform: "uppercase",
|
|
179
188
|
letterSpacing: 0.5,
|
|
189
|
+
flexShrink: 1,
|
|
180
190
|
},
|
|
181
191
|
compactContainer: {
|
|
182
192
|
gap: 8,
|
|
@@ -190,11 +200,17 @@ const styles = StyleSheet.create({
|
|
|
190
200
|
flexDirection: "row",
|
|
191
201
|
justifyContent: "space-between",
|
|
192
202
|
alignItems: "center",
|
|
203
|
+
gap: 8,
|
|
193
204
|
},
|
|
194
205
|
compactHeader: {
|
|
195
206
|
fontSize: 12,
|
|
207
|
+
flexShrink: 1,
|
|
196
208
|
},
|
|
197
209
|
compactValue: {
|
|
198
210
|
alignItems: "flex-end",
|
|
211
|
+
// The value side carries the actual data, so it takes the compressible
|
|
212
|
+
// slot and the fixed label beside it keeps its intrinsic width.
|
|
213
|
+
flexShrink: 1,
|
|
214
|
+
maxWidth: "100%",
|
|
199
215
|
},
|
|
200
216
|
});
|
|
@@ -16,6 +16,7 @@ export interface InlineButtonProps {
|
|
|
16
16
|
icon?: string | ReactNode;
|
|
17
17
|
disabled?: boolean;
|
|
18
18
|
accessibilityLabel?: string;
|
|
19
|
+
accessibilityRole?: "button" | "link";
|
|
19
20
|
style?: StyleProp<ViewStyle>;
|
|
20
21
|
textStyle?: StyleProp<TextStyle>;
|
|
21
22
|
}
|
|
@@ -27,6 +28,7 @@ export function InlineButton({
|
|
|
27
28
|
icon,
|
|
28
29
|
disabled = false,
|
|
29
30
|
accessibilityLabel,
|
|
31
|
+
accessibilityRole = "button",
|
|
30
32
|
style,
|
|
31
33
|
textStyle,
|
|
32
34
|
}: InlineButtonProps) {
|
|
@@ -37,7 +39,7 @@ export function InlineButton({
|
|
|
37
39
|
<Pressable
|
|
38
40
|
onPress={onPress}
|
|
39
41
|
disabled={disabled}
|
|
40
|
-
accessibilityRole=
|
|
42
|
+
accessibilityRole={accessibilityRole}
|
|
41
43
|
accessibilityLabel={accessibilityLabel || label}
|
|
42
44
|
hitSlop={Math.max(0, (touchTargetMin - 24) / 2)}
|
|
43
45
|
style={({ pressed }) => [
|
|
@@ -50,7 +52,14 @@ export function InlineButton({
|
|
|
50
52
|
]}
|
|
51
53
|
>
|
|
52
54
|
{typeof icon === "string" ? <Icon name={icon} size={13} color={colors.accent} /> : icon}
|
|
53
|
-
<Text
|
|
55
|
+
<Text
|
|
56
|
+
accessibilityLabel={label}
|
|
57
|
+
numberOfLines={1}
|
|
58
|
+
ellipsizeMode="tail"
|
|
59
|
+
style={[styles.text, { color: colors.accent }, textStyle]}
|
|
60
|
+
>
|
|
61
|
+
{label}
|
|
62
|
+
</Text>
|
|
54
63
|
</Pressable>
|
|
55
64
|
);
|
|
56
65
|
}
|
|
@@ -65,9 +74,15 @@ const styles = StyleSheet.create({
|
|
|
65
74
|
paddingHorizontal: 3,
|
|
66
75
|
paddingVertical: 2,
|
|
67
76
|
borderRadius: 4,
|
|
77
|
+
// Caller-supplied labels are unbounded; without a shrink budget the control
|
|
78
|
+
// claims its full text width and overflows narrow viewports.
|
|
79
|
+
flexShrink: 1,
|
|
80
|
+
maxWidth: "100%",
|
|
68
81
|
},
|
|
69
82
|
text: {
|
|
70
83
|
fontSize: 12,
|
|
71
84
|
fontWeight: "600",
|
|
85
|
+
// The label must be allowed to compress for `numberOfLines` to engage.
|
|
86
|
+
flexShrink: 1,
|
|
72
87
|
},
|
|
73
88
|
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import React, { useCallback, useState, type ReactNode } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Pressable,
|
|
4
|
+
type AccessibilityRole,
|
|
5
|
+
type GestureResponderEvent,
|
|
6
|
+
type StyleProp,
|
|
7
|
+
type ViewStyle,
|
|
8
|
+
} from "react-native";
|
|
9
|
+
import { usePluginTheme } from "../theme/provider";
|
|
10
|
+
|
|
11
|
+
export interface InteractiveRowProps {
|
|
12
|
+
/** Arbitrary row content — status dots, badges, text, metric readouts. */
|
|
13
|
+
children?: ReactNode;
|
|
14
|
+
/**
|
|
15
|
+
* Press handler. Receives the raw event so callers can control propagation
|
|
16
|
+
* (e.g. a nested status light calling `event.stopPropagation()`).
|
|
17
|
+
*/
|
|
18
|
+
onPress?: (event: GestureResponderEvent) => void;
|
|
19
|
+
/** Native / RN-web tooltip text attached to the hit area. */
|
|
20
|
+
title?: string;
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
accessibilityRole?: AccessibilityRole;
|
|
23
|
+
accessibilityLabel?: string;
|
|
24
|
+
accessibilityHint?: string;
|
|
25
|
+
testID?: string;
|
|
26
|
+
/** Base layout style, merged before the interaction layers. */
|
|
27
|
+
style?: StyleProp<ViewStyle>;
|
|
28
|
+
/** Extra style layered only while hovered (and not disabled). */
|
|
29
|
+
hoverStyle?: StyleProp<ViewStyle>;
|
|
30
|
+
/** Apply a subtle theme accent tint while hovered. Default: false. */
|
|
31
|
+
hoverTint?: boolean;
|
|
32
|
+
/** Accent opacity used by `hoverTint`. Default: 0.05. */
|
|
33
|
+
hoverTintOpacity?: number;
|
|
34
|
+
/** Opacity while pressed. Default: 0.7. */
|
|
35
|
+
pressedOpacity?: number;
|
|
36
|
+
/** Opacity while hovered but not pressed. Defaults to `opacity`. */
|
|
37
|
+
hoveredOpacity?: number;
|
|
38
|
+
/** Opacity at rest. Default: 1. */
|
|
39
|
+
opacity?: number;
|
|
40
|
+
/** Opacity when disabled. Default: 0.45. */
|
|
41
|
+
disabledOpacity?: number;
|
|
42
|
+
/** Expanded touch target beyond the visual bounds. */
|
|
43
|
+
hitSlop?: number;
|
|
44
|
+
/** Notified whenever the hover state flips, for callers that tint their own children. */
|
|
45
|
+
onHoverChange?: (hovered: boolean) => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Hover-aware, pressable row container for dense interactive content (#580).
|
|
50
|
+
*
|
|
51
|
+
* Owns the interaction concerns a labeled `Button` cannot: a hover state (with
|
|
52
|
+
* optional accent tint), an RN-web `title` tooltip, pressed opacity, a pointer
|
|
53
|
+
* cursor, and an `onPress` that forwards the event for `stopPropagation`.
|
|
54
|
+
* Content is arbitrary, so a status dot, badges, and metric readouts compose
|
|
55
|
+
* inside without wrapping each in its own control.
|
|
56
|
+
*/
|
|
57
|
+
export function InteractiveRow({
|
|
58
|
+
children,
|
|
59
|
+
onPress,
|
|
60
|
+
title,
|
|
61
|
+
disabled = false,
|
|
62
|
+
accessibilityRole,
|
|
63
|
+
accessibilityLabel,
|
|
64
|
+
accessibilityHint,
|
|
65
|
+
testID,
|
|
66
|
+
style,
|
|
67
|
+
hoverStyle,
|
|
68
|
+
hoverTint = false,
|
|
69
|
+
hoverTintOpacity = 0.05,
|
|
70
|
+
pressedOpacity = 0.7,
|
|
71
|
+
hoveredOpacity,
|
|
72
|
+
opacity = 1,
|
|
73
|
+
disabledOpacity = 0.45,
|
|
74
|
+
hitSlop,
|
|
75
|
+
onHoverChange,
|
|
76
|
+
}: InteractiveRowProps) {
|
|
77
|
+
const { colors, alpha } = usePluginTheme();
|
|
78
|
+
const [hovered, setHovered] = useState(false);
|
|
79
|
+
const [pressed, setPressed] = useState(false);
|
|
80
|
+
|
|
81
|
+
const handleMouseEnter = useCallback(() => {
|
|
82
|
+
if (disabled) return;
|
|
83
|
+
setHovered(true);
|
|
84
|
+
onHoverChange?.(true);
|
|
85
|
+
}, [disabled, onHoverChange]);
|
|
86
|
+
|
|
87
|
+
const handleMouseLeave = useCallback(() => {
|
|
88
|
+
setHovered(false);
|
|
89
|
+
onHoverChange?.(false);
|
|
90
|
+
}, [onHoverChange]);
|
|
91
|
+
|
|
92
|
+
// `title` is an RN-web DOM passthrough absent from the native prop types, and
|
|
93
|
+
// `onMouseEnter`/`onMouseLeave` are the RN-web hover hooks the dense rows
|
|
94
|
+
// already relied on; attach both via an untyped spread to keep the runtime
|
|
95
|
+
// semantics identical to the hand-rolled rows (#580).
|
|
96
|
+
const webProps = {
|
|
97
|
+
onMouseEnter: handleMouseEnter,
|
|
98
|
+
onMouseLeave: handleMouseLeave,
|
|
99
|
+
...(title ? { title } : {}),
|
|
100
|
+
} as any;
|
|
101
|
+
|
|
102
|
+
const interactive = hovered && !disabled;
|
|
103
|
+
const hoverBackground = interactive && hoverTint
|
|
104
|
+
? { backgroundColor: alpha(colors.accent, hoverTintOpacity) }
|
|
105
|
+
: null;
|
|
106
|
+
const restingOpacity = interactive ? (hoveredOpacity ?? opacity) : opacity;
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<Pressable
|
|
110
|
+
{...webProps}
|
|
111
|
+
onPress={onPress}
|
|
112
|
+
onPressIn={() => setPressed(true)}
|
|
113
|
+
onPressOut={() => setPressed(false)}
|
|
114
|
+
disabled={disabled}
|
|
115
|
+
accessibilityRole={accessibilityRole ?? (onPress ? "button" : undefined)}
|
|
116
|
+
accessibilityLabel={accessibilityLabel}
|
|
117
|
+
accessibilityHint={accessibilityHint}
|
|
118
|
+
testID={testID}
|
|
119
|
+
hitSlop={hitSlop}
|
|
120
|
+
style={[
|
|
121
|
+
{ cursor: onPress && !disabled ? "pointer" : "auto" },
|
|
122
|
+
// A pressable wraps arbitrary content (titles, badges, metric readouts)
|
|
123
|
+
// and must never widen its parent: Yoga defaults `flexShrink` to 0, so
|
|
124
|
+
// without a shrink budget a long label forces the whole row past the
|
|
125
|
+
// viewport. Declared before `style` so callers can still override.
|
|
126
|
+
{ flexShrink: 1, maxWidth: "100%" },
|
|
127
|
+
style,
|
|
128
|
+
hoverBackground,
|
|
129
|
+
interactive ? hoverStyle : null,
|
|
130
|
+
{
|
|
131
|
+
opacity: disabled
|
|
132
|
+
? disabledOpacity
|
|
133
|
+
: pressed
|
|
134
|
+
? pressedOpacity
|
|
135
|
+
: restingOpacity,
|
|
136
|
+
},
|
|
137
|
+
]}
|
|
138
|
+
>
|
|
139
|
+
{children}
|
|
140
|
+
</Pressable>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
@@ -18,6 +18,15 @@ export interface SearchInputProps {
|
|
|
18
18
|
onChangeText: (text: string) => void;
|
|
19
19
|
placeholder?: string;
|
|
20
20
|
onClear?: () => void;
|
|
21
|
+
/**
|
|
22
|
+
* Painted box height, opt-in per call site. Unset keeps the original
|
|
23
|
+
* `isCompact ? 36 : 40` box so no untouched surface changes appearance.
|
|
24
|
+
* 26 matches the compact `size="sm"` button recipe (~26px) so a search field
|
|
25
|
+
* can sit beside filter pills in one row without inflating it (#645); a
|
|
26
|
+
* height of 26 or less also switches the input to the 12px `sm` label size,
|
|
27
|
+
* anything taller keeps the prior 13/14.
|
|
28
|
+
*/
|
|
29
|
+
height?: number;
|
|
21
30
|
style?: StyleProp<ViewStyle>;
|
|
22
31
|
inputStyle?: StyleProp<TextStyle>;
|
|
23
32
|
testID?: string;
|
|
@@ -31,6 +40,7 @@ export function SearchInput({
|
|
|
31
40
|
onChangeText,
|
|
32
41
|
placeholder = "Search...",
|
|
33
42
|
onClear,
|
|
43
|
+
height,
|
|
34
44
|
style,
|
|
35
45
|
inputStyle,
|
|
36
46
|
testID,
|
|
@@ -40,6 +50,10 @@ export function SearchInput({
|
|
|
40
50
|
RNTextInput) as ComponentType<RNTextInputProps & { ref?: Ref<RNTextInputInstance> }>;
|
|
41
51
|
const { colors, resolveRadius, isCompact } = usePluginTheme();
|
|
42
52
|
const radius = resolveRadius("sm");
|
|
53
|
+
// Unspecified keeps the original compact-aware default so every call site
|
|
54
|
+
// that does not opt in renders exactly as before (#645's scope was the
|
|
55
|
+
// uppidi-fleet filter rows only).
|
|
56
|
+
const resolvedHeight = height ?? (isCompact ? 36 : 40);
|
|
43
57
|
|
|
44
58
|
const handleClear = () => {
|
|
45
59
|
onChangeText("");
|
|
@@ -54,7 +68,7 @@ export function SearchInput({
|
|
|
54
68
|
backgroundColor: colors.surface1,
|
|
55
69
|
borderColor: colors.border,
|
|
56
70
|
borderRadius: radius,
|
|
57
|
-
height:
|
|
71
|
+
height: resolvedHeight,
|
|
58
72
|
},
|
|
59
73
|
style,
|
|
60
74
|
]}
|
|
@@ -72,7 +86,7 @@ export function SearchInput({
|
|
|
72
86
|
styles.input,
|
|
73
87
|
{
|
|
74
88
|
color: colors.foreground,
|
|
75
|
-
fontSize: isCompact ? 13 : 14,
|
|
89
|
+
fontSize: height !== undefined && height <= 26 ? 12 : isCompact ? 13 : 14,
|
|
76
90
|
},
|
|
77
91
|
inputStyle,
|
|
78
92
|
]}
|
|
@@ -41,6 +41,8 @@ export function SectionHeader({
|
|
|
41
41
|
return (
|
|
42
42
|
<View style={[styles.container, style]}>
|
|
43
43
|
<Text
|
|
44
|
+
numberOfLines={1}
|
|
45
|
+
ellipsizeMode="tail"
|
|
44
46
|
style={[
|
|
45
47
|
styles.title,
|
|
46
48
|
{
|
|
@@ -73,8 +75,13 @@ const styles = StyleSheet.create({
|
|
|
73
75
|
gap: 8,
|
|
74
76
|
marginTop: 8,
|
|
75
77
|
marginBottom: 2,
|
|
78
|
+
flexShrink: 1,
|
|
79
|
+
maxWidth: "100%",
|
|
76
80
|
},
|
|
77
81
|
title: {
|
|
78
82
|
letterSpacing: 0.8,
|
|
83
|
+
// Section titles are caller-supplied and unbounded; the title must be
|
|
84
|
+
// allowed to compress for `numberOfLines` to engage.
|
|
85
|
+
flexShrink: 1,
|
|
79
86
|
},
|
|
80
87
|
});
|