@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
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import React, { useState } from "react";
|
|
1
|
+
import React, { useRef, useState } from "react";
|
|
2
2
|
import {
|
|
3
|
+
Modal,
|
|
3
4
|
Pressable,
|
|
4
5
|
ScrollView as FallbackScrollView,
|
|
5
6
|
StyleSheet,
|
|
6
7
|
Text,
|
|
8
|
+
TouchableWithoutFeedback,
|
|
7
9
|
View,
|
|
8
10
|
type StyleProp,
|
|
9
11
|
type ViewStyle,
|
|
@@ -32,10 +34,25 @@ export interface SelectProps {
|
|
|
32
34
|
|
|
33
35
|
const OPTION_LIST_MAX_HEIGHT = 216;
|
|
34
36
|
|
|
37
|
+
interface TriggerCoords {
|
|
38
|
+
x: number;
|
|
39
|
+
y: number;
|
|
40
|
+
width: number;
|
|
41
|
+
height: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// When `measureInWindow` is unavailable or never invokes its callback (node
|
|
45
|
+
// test renderers, host shims), the menu still opens anchored to a zeroed box
|
|
46
|
+
// rather than swallowing the press.
|
|
47
|
+
const FALLBACK_COORDS: TriggerCoords = { x: 0, y: 0, width: 0, height: 0 };
|
|
48
|
+
|
|
35
49
|
/**
|
|
36
50
|
* Compact single-choice picker sized to sit inside a {@link FormRow}. The
|
|
37
|
-
* closed trigger stays one line tall; opening
|
|
38
|
-
*
|
|
51
|
+
* closed trigger stays one line tall; opening mounts the menu in a root
|
|
52
|
+
* transparent `<Modal>` overlay portal, so a long list never expands the
|
|
53
|
+
* trigger's parent container, is never clipped by an ancestor
|
|
54
|
+
* `overflow: "hidden"` (Card/Tabs/Modal), and always paints above later
|
|
55
|
+
* siblings regardless of local stacking context.
|
|
39
56
|
*/
|
|
40
57
|
export function Select({
|
|
41
58
|
value,
|
|
@@ -48,12 +65,14 @@ export function Select({
|
|
|
48
65
|
style,
|
|
49
66
|
}: SelectProps) {
|
|
50
67
|
const { Icon } = getClientHost();
|
|
51
|
-
// The
|
|
52
|
-
//
|
|
53
|
-
//
|
|
68
|
+
// The option list is mounted in the root modal overlay, so it scrolls with
|
|
69
|
+
// a plain React Native ScrollView — never the host sheet-gesture scroller
|
|
70
|
+
// (#219) and never a scroller owned by the trigger's parent.
|
|
54
71
|
const { colors, resolveRadius, typography, isCompact, touchTargetMin, alpha } =
|
|
55
72
|
usePluginTheme();
|
|
56
73
|
const [open, setOpen] = useState(false);
|
|
74
|
+
const [coords, setCoords] = useState<TriggerCoords | null>(null);
|
|
75
|
+
const triggerRef = useRef<View>(null);
|
|
57
76
|
|
|
58
77
|
const caption = typography?.caption ?? {
|
|
59
78
|
fontSize: 11,
|
|
@@ -76,14 +95,40 @@ export function Select({
|
|
|
76
95
|
|
|
77
96
|
const triggerMinHeight = size === "sm" ? 28 : Math.max(34, touchTargetMin);
|
|
78
97
|
|
|
98
|
+
const openAt = (next: TriggerCoords) => {
|
|
99
|
+
setCoords(next);
|
|
100
|
+
setOpen(true);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const handleToggle = () => {
|
|
104
|
+
if (isOpen) {
|
|
105
|
+
setOpen(false);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const node = triggerRef.current;
|
|
110
|
+
// A node/shim without `measureInWindow` (node test renderers, stripped
|
|
111
|
+
// host shims) still opens anchored to a zeroed box rather than swallowing
|
|
112
|
+
// the press (#520).
|
|
113
|
+
if (!node || typeof node.measureInWindow !== "function") {
|
|
114
|
+
openAt(FALLBACK_COORDS);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
node.measureInWindow((x, y, width, height) => {
|
|
119
|
+
openAt({ x, y, width, height });
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
|
|
79
123
|
return (
|
|
80
124
|
<View style={[styles.container, style]}>
|
|
81
125
|
<Pressable
|
|
126
|
+
ref={triggerRef}
|
|
82
127
|
accessibilityRole="button"
|
|
83
128
|
accessibilityLabel={label ? `${label}: ${display}` : display}
|
|
84
129
|
accessibilityState={{ expanded: isOpen, disabled }}
|
|
85
130
|
disabled={!canOpen}
|
|
86
|
-
onPress={
|
|
131
|
+
onPress={handleToggle}
|
|
87
132
|
style={({ pressed }) => [
|
|
88
133
|
styles.trigger,
|
|
89
134
|
{
|
|
@@ -122,67 +167,84 @@ export function Select({
|
|
|
122
167
|
/>
|
|
123
168
|
</Pressable>
|
|
124
169
|
|
|
125
|
-
{isOpen ? (
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
borderColor: colors.border,
|
|
132
|
-
backgroundColor: colors.surface0,
|
|
133
|
-
marginTop: spacing.xs,
|
|
134
|
-
},
|
|
135
|
-
]}
|
|
170
|
+
{isOpen && coords ? (
|
|
171
|
+
<Modal
|
|
172
|
+
transparent
|
|
173
|
+
visible={isOpen}
|
|
174
|
+
animationType="none"
|
|
175
|
+
onRequestClose={() => setOpen(false)}
|
|
136
176
|
>
|
|
137
|
-
<
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
177
|
+
<TouchableWithoutFeedback
|
|
178
|
+
accessibilityRole="button"
|
|
179
|
+
accessibilityLabel="Dismiss options"
|
|
180
|
+
onPress={() => setOpen(false)}
|
|
181
|
+
>
|
|
182
|
+
<View style={StyleSheet.absoluteFill} />
|
|
183
|
+
</TouchableWithoutFeedback>
|
|
184
|
+
|
|
185
|
+
<View
|
|
186
|
+
style={[
|
|
187
|
+
styles.overlayList,
|
|
188
|
+
{
|
|
189
|
+
top: coords.y + coords.height + spacing.xs,
|
|
190
|
+
left: coords.x,
|
|
191
|
+
minWidth: coords.width,
|
|
192
|
+
borderRadius: radius,
|
|
193
|
+
borderColor: colors.border,
|
|
194
|
+
backgroundColor: colors.surface0,
|
|
195
|
+
},
|
|
196
|
+
]}
|
|
141
197
|
>
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
: pressed
|
|
162
|
-
? alpha(colors.surface2, 0.5)
|
|
163
|
-
: "transparent",
|
|
164
|
-
},
|
|
165
|
-
]}
|
|
166
|
-
>
|
|
167
|
-
<Text
|
|
168
|
-
numberOfLines={1}
|
|
169
|
-
style={[
|
|
170
|
-
styles.optionText,
|
|
198
|
+
<FallbackScrollView
|
|
199
|
+
nestedScrollEnabled
|
|
200
|
+
keyboardShouldPersistTaps="handled"
|
|
201
|
+
style={{ maxHeight: OPTION_LIST_MAX_HEIGHT }}
|
|
202
|
+
>
|
|
203
|
+
{options.map((option) => {
|
|
204
|
+
const isSelected = option.value === value;
|
|
205
|
+
return (
|
|
206
|
+
<Pressable
|
|
207
|
+
key={option.value}
|
|
208
|
+
accessibilityRole="button"
|
|
209
|
+
accessibilityLabel={option.label}
|
|
210
|
+
accessibilityState={{ selected: isSelected }}
|
|
211
|
+
onPress={() => {
|
|
212
|
+
onValueChange(option.value);
|
|
213
|
+
setOpen(false);
|
|
214
|
+
}}
|
|
215
|
+
style={({ pressed }) => [
|
|
216
|
+
styles.option,
|
|
171
217
|
{
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
218
|
+
paddingVertical,
|
|
219
|
+
paddingHorizontal,
|
|
220
|
+
backgroundColor: isSelected
|
|
221
|
+
? colors.surface2
|
|
222
|
+
: pressed
|
|
223
|
+
? alpha(colors.surface2, 0.5)
|
|
224
|
+
: "transparent",
|
|
176
225
|
},
|
|
177
226
|
]}
|
|
178
227
|
>
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
228
|
+
<Text
|
|
229
|
+
numberOfLines={1}
|
|
230
|
+
style={[
|
|
231
|
+
styles.optionText,
|
|
232
|
+
{
|
|
233
|
+
color: isSelected ? colors.foreground : colors.foregroundMuted,
|
|
234
|
+
fontSize: isCompact ? fontSize : fontSize + 1,
|
|
235
|
+
lineHeight,
|
|
236
|
+
fontWeight: isSelected ? "600" : "400",
|
|
237
|
+
},
|
|
238
|
+
]}
|
|
239
|
+
>
|
|
240
|
+
{option.label}
|
|
241
|
+
</Text>
|
|
242
|
+
</Pressable>
|
|
243
|
+
);
|
|
244
|
+
})}
|
|
245
|
+
</FallbackScrollView>
|
|
246
|
+
</View>
|
|
247
|
+
</Modal>
|
|
186
248
|
) : null}
|
|
187
249
|
</View>
|
|
188
250
|
);
|
|
@@ -203,9 +265,15 @@ const styles = StyleSheet.create({
|
|
|
203
265
|
flexShrink: 1,
|
|
204
266
|
minWidth: 0,
|
|
205
267
|
},
|
|
206
|
-
|
|
268
|
+
overlayList: {
|
|
269
|
+
position: "absolute",
|
|
207
270
|
borderWidth: 1,
|
|
208
271
|
overflow: "hidden",
|
|
272
|
+
elevation: 20,
|
|
273
|
+
shadowColor: "#000",
|
|
274
|
+
shadowOffset: { width: 0, height: 4 },
|
|
275
|
+
shadowOpacity: 0.25,
|
|
276
|
+
shadowRadius: 8,
|
|
209
277
|
},
|
|
210
278
|
option: {},
|
|
211
279
|
optionText: {
|
|
@@ -17,8 +17,10 @@ export interface CustomPillBodyProps {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
* Standard pill body renderer for a custom metric pill
|
|
21
|
-
*
|
|
20
|
+
* Standard pill body renderer for a custom metric pill. The host renders the
|
|
21
|
+
* composer pill body itself, so reach for this from a pill's own surface
|
|
22
|
+
* (`renderModal`) rather than from the trackbar; it adapts to responsive
|
|
23
|
+
* compact/mobile modes and shows threshold status.
|
|
22
24
|
*/
|
|
23
25
|
export function CustomPillBody({ state }: CustomPillBodyProps) {
|
|
24
26
|
const { Icon } = getClientHost();
|
|
@@ -145,7 +147,6 @@ export function registerCustomPills(
|
|
|
145
147
|
flair: options.flair,
|
|
146
148
|
resolveLabel: () =>
|
|
147
149
|
pill.displayValue ? `${pill.title} ${pill.displayValue}` : pill.title,
|
|
148
|
-
renderPill: () => <CustomPillBody state={pill} />,
|
|
149
150
|
renderModal: () => {
|
|
150
151
|
const [refreshing, setRefreshing] = useState(false);
|
|
151
152
|
const [currentOutput, setCurrentOutput] = useState<string | undefined>(
|
|
@@ -14,20 +14,23 @@ import type {
|
|
|
14
14
|
TextInput as RNTextInput,
|
|
15
15
|
TextInputProps,
|
|
16
16
|
} from "react-native";
|
|
17
|
+
import type {
|
|
18
|
+
PluginButtonRegistration,
|
|
19
|
+
PluginComposerPillContribution,
|
|
20
|
+
} from "@getpaseo/plugin/client";
|
|
17
21
|
|
|
18
22
|
/**
|
|
19
23
|
* Structural host types for Paseo client integration.
|
|
20
24
|
*
|
|
21
25
|
* These interfaces describe the shapes `paseo-plugin-helper/client` needs
|
|
22
|
-
* from the Paseo host app
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* bundle satisfies both the Paseo v0.7 SDK entry points and the Paseo
|
|
26
|
-
* v0.8 runtime-owned entry points.
|
|
26
|
+
* from the Paseo host app: the runtime dependencies the plugin injects once
|
|
27
|
+
* in its client entry — the host theme, the layout, the icon/Modal primitives,
|
|
28
|
+
* the agents feed — so they can be supplied from any entry point.
|
|
27
29
|
*
|
|
28
|
-
* The
|
|
29
|
-
*
|
|
30
|
-
*
|
|
30
|
+
* The composer pill is the exception: the host owns that contribution's shape,
|
|
31
|
+
* so it is imported from the Paseo SDK rather than restated here. A restated
|
|
32
|
+
* copy is what let a pre-0.9 `{Component, onPress}` pill through the type
|
|
33
|
+
* system (#666).
|
|
31
34
|
*/
|
|
32
35
|
|
|
33
36
|
export interface HostThemeColors {
|
|
@@ -216,57 +219,8 @@ export interface HostSurfaceProps {
|
|
|
216
219
|
};
|
|
217
220
|
}
|
|
218
221
|
|
|
219
|
-
export interface ComposerPillContribution {
|
|
220
|
-
id: string;
|
|
221
|
-
title: string;
|
|
222
|
-
workspaceId: string;
|
|
223
|
-
agentId: string;
|
|
224
|
-
Component: ComponentType<HostPillProps>;
|
|
225
|
-
onPress(): void | Promise<void>;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
export type ComposerPillButtonIcon = string | ComponentType<any>;
|
|
229
|
-
|
|
230
|
-
export interface ComposerPillButtonDescriptor {
|
|
231
|
-
title: string;
|
|
232
|
-
icon: ComposerPillButtonIcon;
|
|
233
|
-
label?: string;
|
|
234
|
-
visible?: boolean;
|
|
235
|
-
disabled?: boolean;
|
|
236
|
-
behavior:
|
|
237
|
-
| { kind: "action"; onPress(): void | Promise<void> }
|
|
238
|
-
| { kind: "popover"; Content: ComponentType<any> };
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
export interface ComposerPillButtonContribution {
|
|
242
|
-
id: string;
|
|
243
|
-
workspaceId: string;
|
|
244
|
-
agentId: string;
|
|
245
|
-
button: ComposerPillButtonDescriptor;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
export interface ComposerPillSdkContribution {
|
|
249
|
-
id: string;
|
|
250
|
-
workspaceId: string;
|
|
251
|
-
agentId: string;
|
|
252
|
-
button: Record<string, any>;
|
|
253
|
-
[key: string]: any;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
export interface ComposerPillRegistrationHandle {
|
|
257
|
-
update(patch: Record<string, any>): void;
|
|
258
|
-
remove(): void;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
export type ComposerPillRegistration = PluginCleanup | ComposerPillRegistrationHandle;
|
|
262
|
-
|
|
263
222
|
export interface ComposerPillRegistrar {
|
|
264
|
-
addComposerPill(
|
|
265
|
-
contribution:
|
|
266
|
-
| ComposerPillContribution
|
|
267
|
-
| ComposerPillButtonContribution
|
|
268
|
-
| ComposerPillSdkContribution,
|
|
269
|
-
): ComposerPillRegistration;
|
|
223
|
+
addComposerPill(contribution: PluginComposerPillContribution): PluginButtonRegistration;
|
|
270
224
|
paseo: {
|
|
271
225
|
agents: HostAgentsApi;
|
|
272
226
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export * from "./theme/index";
|
|
2
|
+
export * from "./styles/index";
|
|
2
3
|
export * from "./components/index";
|
|
3
4
|
export * from "./layout/index";
|
|
4
5
|
export * from "./pill";
|
|
@@ -26,3 +27,4 @@ export {
|
|
|
26
27
|
type ForgeMarkInput,
|
|
27
28
|
type ResolvedForgeMark,
|
|
28
29
|
} from "../../../shared/vendor/paseo-plugin-helper/forge";
|
|
30
|
+
export * from "./tickets";
|
|
@@ -28,12 +28,10 @@ export interface ModalBodyProps {
|
|
|
28
28
|
header?: ReactNode;
|
|
29
29
|
headerStyle?: StyleProp<ViewStyle>;
|
|
30
30
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* popovers (the host owns that narrow viewport). Use this instead of adding a
|
|
36
|
-
* per-plugin width/minWidth literal; the host still owns the final size.
|
|
31
|
+
* Advisory density hint. It carries no width floor: the host `Modal` has no
|
|
32
|
+
* size prop, so a content-side `minWidth` would override the host dialog
|
|
33
|
+
* allocation and clip inside narrow docks (paseo#641). Use `maxContentWidth`
|
|
34
|
+
* for a readable dense column. Ignored on compact/mobile surfaces.
|
|
37
35
|
*/
|
|
38
36
|
size?: ModalBodySize;
|
|
39
37
|
/**
|
|
@@ -88,13 +86,14 @@ export interface ModalBodyProps {
|
|
|
88
86
|
* Adding the `"required"` member is additive: the existing two values keep
|
|
89
87
|
* their meaning and the default stays `"helper"`.
|
|
90
88
|
*/
|
|
91
|
-
export type ModalBodyScrollOwner = "helper" | "host" | "required";
|
|
89
|
+
export type ModalBodyScrollOwner = "helper" | "host" | "required" | "popover";
|
|
92
90
|
|
|
93
91
|
export const ModalBodyScrollOwnerContext = createContext<ModalBodyScrollOwner>("helper");
|
|
94
92
|
|
|
95
|
-
//
|
|
96
|
-
// `
|
|
97
|
-
|
|
93
|
+
// `size` is advisory and intentionally carries no width floor. A content-side
|
|
94
|
+
// `minWidth` overrides the host dialog allocation instead of deferring to it,
|
|
95
|
+
// so a narrow dock (explorer/sidebar) clips the content it was meant to widen
|
|
96
|
+
// (paseo#641). Use `maxContentWidth` to keep a dense column readable.
|
|
98
97
|
|
|
99
98
|
/**
|
|
100
99
|
* Mobile-safe scrollable body for Paseo <Modal.Content>.
|
|
@@ -142,7 +141,6 @@ export function ModalBody({
|
|
|
142
141
|
header,
|
|
143
142
|
headerStyle,
|
|
144
143
|
headerMode = "scroll",
|
|
145
|
-
size = "default",
|
|
146
144
|
maxContentWidth,
|
|
147
145
|
scrollMode = "auto",
|
|
148
146
|
debugTag,
|
|
@@ -154,20 +152,17 @@ export function ModalBody({
|
|
|
154
152
|
}: ModalBodyProps) {
|
|
155
153
|
const { isCompact, isMobile, layout, padding, colors } = usePluginTheme();
|
|
156
154
|
const scrollOwner = useContext(ModalBodyScrollOwnerContext);
|
|
157
|
-
const hostOwnsScroll = scrollOwner === "host";
|
|
155
|
+
const hostOwnsScroll = scrollOwner === "host" || scrollOwner === "popover";
|
|
158
156
|
// "required": an ancestor proved the host supplies no scroller (plugin
|
|
159
157
|
// surface) — own the scroll on every surface, including non-compact desktop.
|
|
160
158
|
const helperOwnsScroll =
|
|
161
159
|
!hostOwnsScroll &&
|
|
162
160
|
(scrollOwner === "required" || scrollMode === "always" || isCompact || isMobile);
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
size === "large" && !isMobile && !isCompact && !hostOwnsScroll
|
|
169
|
-
? styles.largeDialog
|
|
170
|
-
: undefined;
|
|
161
|
+
// `size` no longer contributes width. The host `Modal` has no size prop, so
|
|
162
|
+
// there is no host allocation to request and nothing to condition on: a
|
|
163
|
+
// content-side floor is the only lever available, and it fights the host by
|
|
164
|
+
// refusing to shrink inside a narrow dialog. Data-dense surfaces get their
|
|
165
|
+
// reading width from `maxContentWidth` on the content column instead.
|
|
171
166
|
const ResolvedScrollView = selectHostScrollView(
|
|
172
167
|
getOptionalClientHost(),
|
|
173
168
|
FallbackScrollView as unknown as HostScrollView,
|
|
@@ -224,7 +219,7 @@ export function ModalBody({
|
|
|
224
219
|
const scrollBody = (
|
|
225
220
|
<ResolvedScrollView
|
|
226
221
|
ref={setRefs}
|
|
227
|
-
style={[{ backgroundColor: colors.surface0 }, styles.container,
|
|
222
|
+
style={[{ backgroundColor: colors.surface0 }, styles.container, style]}
|
|
228
223
|
nestedScrollEnabled={true}
|
|
229
224
|
keyboardShouldPersistTaps="handled"
|
|
230
225
|
showsVerticalScrollIndicator={true}
|
|
@@ -265,7 +260,7 @@ export function ModalBody({
|
|
|
265
260
|
} as unknown as ViewStyle)
|
|
266
261
|
: undefined;
|
|
267
262
|
const plainBody = (
|
|
268
|
-
<View style={[styles.plainBody,
|
|
263
|
+
<View style={[styles.plainBody, style]}>
|
|
269
264
|
{header ? (
|
|
270
265
|
<View style={[styles.header, stickyHeaderStyle, headerStyle]}>{header}</View>
|
|
271
266
|
) : null}
|
|
@@ -296,7 +291,7 @@ export function ModalBody({
|
|
|
296
291
|
return (
|
|
297
292
|
<ResolvedScrollView
|
|
298
293
|
ref={setRefs}
|
|
299
|
-
style={[{ backgroundColor: colors.surface0 }, styles.container,
|
|
294
|
+
style={[{ backgroundColor: colors.surface0 }, styles.container, style]}
|
|
300
295
|
nestedScrollEnabled={true}
|
|
301
296
|
keyboardShouldPersistTaps="handled"
|
|
302
297
|
showsVerticalScrollIndicator={true}
|
|
@@ -330,7 +325,7 @@ export function ModalBody({
|
|
|
330
325
|
}
|
|
331
326
|
|
|
332
327
|
return (
|
|
333
|
-
<View style={[styles.screen
|
|
328
|
+
<View style={[styles.screen]}>
|
|
334
329
|
<View style={[styles.header, headerStyle]}>{header}</View>
|
|
335
330
|
{scrollBody}
|
|
336
331
|
</View>
|
|
@@ -338,11 +333,6 @@ export function ModalBody({
|
|
|
338
333
|
}
|
|
339
334
|
|
|
340
335
|
const styles = StyleSheet.create({
|
|
341
|
-
// Desktop-only wide extent for `size="large"`. The host still owns the final
|
|
342
|
-
// dialog size; this only asks a content-sizing host for more room.
|
|
343
|
-
largeDialog: {
|
|
344
|
-
minWidth: LARGE_DIALOG_MIN_WIDTH,
|
|
345
|
-
},
|
|
346
336
|
screen: {
|
|
347
337
|
flex: 1,
|
|
348
338
|
minHeight: 0,
|
|
@@ -1,35 +1,33 @@
|
|
|
1
1
|
import React, { type ReactNode } from "react";
|
|
2
2
|
import { getClientHost } from "../host";
|
|
3
|
-
import { ModalBody, type ModalBodyProps } from "./ModalBody";
|
|
3
|
+
import { ModalBody, ModalBodyScrollOwnerContext, type ModalBodyProps } from "./ModalBody";
|
|
4
4
|
|
|
5
5
|
export interface ModalContentProps extends Omit<ModalBodyProps, "scrollMode"> {
|
|
6
6
|
children: ReactNode;
|
|
7
|
+
/**
|
|
8
|
+
* Whether the host owns the outer scroll container.
|
|
9
|
+
* Defaults to `true` so host desktop mouse wheel, trackpad, and mobile bottom
|
|
10
|
+
* sheet gestures scroll natively without fighting an inner scroller.
|
|
11
|
+
* Set to `false` only if the modal contains a custom internal scroller
|
|
12
|
+
* (e.g. virtualized list or canvas).
|
|
13
|
+
*/
|
|
14
|
+
scrollable?: boolean;
|
|
7
15
|
}
|
|
8
16
|
|
|
9
17
|
/**
|
|
10
18
|
* Helper-owned modal body for plugins that open their own host `<Modal>`.
|
|
11
19
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
20
|
+
* Delegates scroll ownership to the host `<Modal.Content>` (scrollable by default)
|
|
21
|
+
* so desktop mouse wheel, trackpad, and mobile bottom sheet gestures scroll
|
|
22
|
+
* natively under Paseo host rules without fighting an inner scroller.
|
|
15
23
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* no explicit height, so the dialog grows/shrinks with its children on every
|
|
19
|
-
* data change. This wrapper always passes `scrollable={false}`, which makes the
|
|
20
|
-
* host allocate a bounded dialog (`desktopHeight: "85%"`). Because that host
|
|
21
|
-
* content view then supplies no scroller, the wrapper also forces
|
|
22
|
-
* `ModalBody scrollMode="always"`, so the helper owns the one scroll region on
|
|
23
|
-
* every surface and the bounded dialog scrolls instead of clipping.
|
|
24
|
-
*
|
|
25
|
-
* The size contract is `ModalBody`'s: it takes the host-allocated dialog size
|
|
26
|
-
* and is fluid within it; `size?: "default" | "large"` is the only size escape
|
|
27
|
-
* hatch. Do not add per-plugin width/minWidth/height literals around it.
|
|
24
|
+
* Sizing stays fluid within the host-allocated modal frame, respecting
|
|
25
|
+
* `size?: "default" | "large"`.
|
|
28
26
|
*
|
|
29
27
|
* ```tsx
|
|
30
28
|
* <Modal title="…" open={open} onOpenChange={setOpen}>
|
|
31
29
|
* <ModalContent size="default">
|
|
32
|
-
* …cards, controls, rows
|
|
30
|
+
* …cards, controls, rows…
|
|
33
31
|
* </ModalContent>
|
|
34
32
|
* </Modal>
|
|
35
33
|
* ```
|
|
@@ -37,13 +35,17 @@ export interface ModalContentProps extends Omit<ModalBodyProps, "scrollMode"> {
|
|
|
37
35
|
* Accepts every `ModalBody` prop (header, headerMode, refreshing, onRefresh,
|
|
38
36
|
* stickToEnd, scrollRef, debugTag, style, contentContainerStyle, …).
|
|
39
37
|
*/
|
|
40
|
-
export function ModalContent({
|
|
38
|
+
export function ModalContent({
|
|
39
|
+
children,
|
|
40
|
+
scrollable = true,
|
|
41
|
+
...bodyProps
|
|
42
|
+
}: ModalContentProps) {
|
|
41
43
|
const { Modal } = getClientHost();
|
|
42
44
|
return (
|
|
43
|
-
<Modal.Content scrollable={
|
|
44
|
-
<
|
|
45
|
-
{children}
|
|
46
|
-
</
|
|
45
|
+
<Modal.Content scrollable={scrollable}>
|
|
46
|
+
<ModalBodyScrollOwnerContext.Provider value={scrollable ? "host" : "required"}>
|
|
47
|
+
<ModalBody {...bodyProps}>{children}</ModalBody>
|
|
48
|
+
</ModalBodyScrollOwnerContext.Provider>
|
|
47
49
|
</Modal.Content>
|
|
48
50
|
);
|
|
49
51
|
}
|
|
@@ -20,6 +20,8 @@ export interface RegisterWorkspacePanelOptions {
|
|
|
20
20
|
icon: string;
|
|
21
21
|
Component: ComponentType<PluginWorkspacePanelProps>;
|
|
22
22
|
flair?: VisualFlair;
|
|
23
|
+
/** Host locations in which the panel should be available. */
|
|
24
|
+
locations?: string[];
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export interface RegisterAgentPanelOptions {
|
|
@@ -37,8 +39,8 @@ export interface RegisterAgentPanelOptions {
|
|
|
37
39
|
export function registerWorkspacePanel(
|
|
38
40
|
plugin: WorkspacePanelRegistrar,
|
|
39
41
|
options: RegisterWorkspacePanelOptions,
|
|
40
|
-
): void {
|
|
41
|
-
const { id, title, icon, Component, flair } = options;
|
|
42
|
+
): () => void {
|
|
43
|
+
const { id, title, icon, Component, flair, locations } = options;
|
|
42
44
|
|
|
43
45
|
const WrappedComponent: ComponentType<PluginWorkspacePanelProps> = (props) => (
|
|
44
46
|
<PluginThemeProvider theme={props.theme} layout={props.layout} flair={flair}>
|
|
@@ -46,13 +48,19 @@ export function registerWorkspacePanel(
|
|
|
46
48
|
</PluginThemeProvider>
|
|
47
49
|
);
|
|
48
50
|
|
|
49
|
-
plugin.addWorkspacePanel({
|
|
51
|
+
const registration = plugin.addWorkspacePanel({
|
|
50
52
|
id,
|
|
51
53
|
title,
|
|
52
54
|
icon,
|
|
53
55
|
context: "workspace",
|
|
56
|
+
locations,
|
|
54
57
|
Component: WrappedComponent,
|
|
55
58
|
});
|
|
59
|
+
|
|
60
|
+
return () => {
|
|
61
|
+
if (typeof registration === "function") registration();
|
|
62
|
+
else registration?.remove?.();
|
|
63
|
+
};
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
/**
|