@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.
Files changed (43) hide show
  1. package/README.md +11 -12
  2. package/client/multi-host-view.tsx +177 -0
  3. package/client/multi-host.ts +98 -0
  4. package/client/pill-labels.ts +30 -6
  5. package/client/pill.tsx +42 -61
  6. package/client/settings-events.ts +33 -0
  7. package/client/surface.tsx +10 -9
  8. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +12 -0
  9. package/client/vendor/paseo-plugin-helper/components/Button.tsx +29 -3
  10. package/client/vendor/paseo-plugin-helper/components/Card.tsx +32 -0
  11. package/client/vendor/paseo-plugin-helper/components/DataTable.tsx +17 -1
  12. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +17 -2
  13. package/client/vendor/paseo-plugin-helper/components/InteractiveRow.tsx +142 -0
  14. package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +16 -2
  15. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +7 -0
  16. package/client/vendor/paseo-plugin-helper/components/Select.tsx +131 -63
  17. package/client/vendor/paseo-plugin-helper/components/index.ts +1 -0
  18. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +4 -3
  19. package/client/vendor/paseo-plugin-helper/host.ts +12 -58
  20. package/client/vendor/paseo-plugin-helper/index.ts +2 -0
  21. package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +19 -29
  22. package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +24 -22
  23. package/client/vendor/paseo-plugin-helper/panel.tsx +11 -3
  24. package/client/vendor/paseo-plugin-helper/pill.tsx +135 -339
  25. package/client/vendor/paseo-plugin-helper/styles/index.ts +1 -0
  26. package/client/vendor/paseo-plugin-helper/styles/recipes.ts +550 -0
  27. package/client/vendor/paseo-plugin-helper/surface.tsx +62 -2
  28. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +3 -39
  29. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +40 -1
  30. package/client/vendor/paseo-plugin-helper/tickets.tsx +766 -0
  31. package/index.server.ts +7 -0
  32. package/package.json +12 -3
  33. package/paseo-plugin.json +1 -1
  34. package/server/vendor/paseo-plugin-helper/index.ts +2 -0
  35. package/server/vendor/paseo-plugin-helper/mcp-injection.ts +20 -2
  36. package/server/vendor/paseo-plugin-helper/plugins.ts +184 -19
  37. package/server/vendor/paseo-plugin-helper/tickets.ts +1281 -0
  38. package/shared/helper-version.ts +20 -0
  39. package/shared/multi-host.ts +348 -0
  40. package/shared/resources.ts +1 -1
  41. package/shared/vendor/paseo-plugin-helper/index.ts +1 -0
  42. package/shared/vendor/paseo-plugin-helper/tickets.ts +1010 -0
  43. 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
+ }
@@ -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 "./pill";
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="button"
131
+ accessibilityRole={accessibilityRole}
116
132
  accessibilityLabel={accessibilityLabel || label}
117
- hitSlop={Math.max(0, (touchTargetMin - 32) / 2)}
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 style={[styles.headerText, { color: colors.foregroundMuted }]}>
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="button"
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 style={[styles.text, { color: colors.accent }, textStyle]}>{label}</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: isCompact ? 36 : 40,
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
  });