@xpufx/paseo-top 0.4.4 → 0.4.5
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/OVERVIEW.md +17 -0
- package/client/host-ui.tsx +184 -0
- package/client/multi-host-view.tsx +57 -43
- package/client/multi-host.ts +36 -13
- package/client/pill-labels.ts +94 -1
- package/client/pill.tsx +690 -614
- package/client/resources-query.ts +1 -1
- package/client/settings-events.ts +1 -1
- package/client/settings-ui.tsx +86 -9
- package/client/surface.tsx +308 -276
- package/client/telemetry-copy.ts +8 -2
- package/client/telemetry.tsx +372 -321
- package/client/turn-panel.tsx +126 -104
- package/client/vendor/paseo-plugin-helper/{host.ts → core/host.ts} +9 -3
- package/client/vendor/paseo-plugin-helper/core/index.ts +76 -0
- package/client/vendor/paseo-plugin-helper/core/plugin-load-count.ts +89 -0
- package/client/vendor/paseo-plugin-helper/{query-refresh.ts → core/query-refresh.ts} +2 -2
- package/client/vendor/paseo-plugin-helper/{query.ts → core/query.ts} +2 -2
- package/client/vendor/paseo-plugin-helper/{settings.ts → core/settings.ts} +1 -1
- package/client/vendor/paseo-plugin-helper/{shared-settings.ts → core/shared-settings.ts} +2 -2
- package/client/vendor/paseo-plugin-helper/{utils → lifecycle}/clipboard.ts +2 -2
- package/client/vendor/paseo-plugin-helper/lifecycle/flair.ts +26 -0
- package/client/vendor/paseo-plugin-helper/{theme/color-utils.ts → lifecycle/host-color.ts} +95 -38
- package/client/vendor/paseo-plugin-helper/lifecycle/host-theme.tsx +109 -0
- package/client/vendor/paseo-plugin-helper/lifecycle/index.ts +23 -0
- package/client/vendor/paseo-plugin-helper/lifecycle/multi-host.ts +66 -0
- package/client/vendor/paseo-plugin-helper/{panel.tsx → lifecycle/panel.tsx} +7 -7
- package/client/vendor/paseo-plugin-helper/{pill.tsx → lifecycle/pill.tsx} +51 -18
- package/client/vendor/paseo-plugin-helper/lifecycle/providers.tsx +46 -0
- package/client/vendor/paseo-plugin-helper/lifecycle/scroll-owner.ts +26 -0
- package/client/vendor/paseo-plugin-helper/{surface.tsx → lifecycle/surface.tsx} +6 -6
- package/client/vendor/paseo-plugin-helper/ui/color.ts +1 -0
- package/client/vendor/paseo-plugin-helper/ui/content.tsx +1326 -0
- package/client/vendor/paseo-plugin-helper/ui/controls.tsx +1018 -0
- package/client/vendor/paseo-plugin-helper/{components/DataTable.tsx → ui/data.tsx} +50 -58
- package/client/vendor/paseo-plugin-helper/ui/index.ts +44 -0
- package/client/vendor/paseo-plugin-helper/ui/layout.tsx +263 -0
- package/client/vendor/paseo-plugin-helper/ui/modal.tsx +111 -0
- package/client/vendor/paseo-plugin-helper/ui/plugin-reload-banner.tsx +119 -0
- package/client/vendor/paseo-plugin-helper/{settings-screen.tsx → ui/settings-screen.tsx} +3 -3
- package/client/vendor/paseo-plugin-helper/ui/settings.ts +36 -0
- package/client/vendor/paseo-plugin-helper/ui/text.tsx +821 -0
- package/client/vendor/paseo-plugin-helper/ui/theme.tsx +1 -0
- package/client/vendor/permission-audit/client.tsx +706 -0
- package/client/vendor/permission-audit/index.tsx +1 -0
- package/index.client.tsx +58 -37
- package/index.server.ts +256 -234
- package/package.json +7 -4
- package/paseo-plugin.json +2 -0
- package/screenshots/1-paseo-top-modal-system.jpg +0 -0
- package/screenshots/2-paseo-top-modal-workspace.jpg +0 -0
- package/screenshots/3-paseo-top-modal-settings.jpg +0 -0
- package/screenshots/4-paseo-top-modal-about.jpg +0 -0
- package/screenshots/paseo-top-dashboard.jpg +0 -0
- package/screenshots/paseo-top-general-view.jpg +0 -0
- package/server/resources.ts +98 -0
- package/server/vendor/paseo-plugin-helper/custom-pills.ts +70 -0
- package/server/vendor/paseo-plugin-helper/index.ts +2 -0
- package/server/vendor/paseo-plugin-helper/logger.ts +5 -16
- package/server/vendor/paseo-plugin-helper/rpc-guard.ts +2 -2
- package/server/vendor/paseo-plugin-helper/storage-guard.ts +294 -0
- package/server/vendor/paseo-plugin-helper/storage.ts +36 -2
- package/server/vendor/paseo-plugin-helper/telemetry.ts +486 -0
- package/server/vendor/paseo-plugin-helper/tickets.ts +18 -67
- package/server/vendor/permission-audit/index.ts +1 -0
- package/server/vendor/permission-audit/server.ts +839 -0
- package/shared/helper-version.ts +1 -1
- package/shared/multi-host.ts +153 -159
- package/shared/resources.ts +145 -0
- package/shared/vendor/paseo-plugin-helper/async.ts +14 -7
- package/shared/vendor/paseo-plugin-helper/features.ts +244 -0
- package/shared/vendor/paseo-plugin-helper/forge.ts +27 -0
- package/shared/vendor/paseo-plugin-helper/index.ts +1 -0
- package/shared/vendor/paseo-plugin-helper/tickets.ts +122 -28
- package/shared/vendor/permission-audit/filter.ts +61 -0
- package/shared/vendor/permission-audit/index.ts +2 -0
- package/shared/vendor/permission-audit/shared.ts +119 -0
- package/shared/version.ts +1 -1
- package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +0 -493
- package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +0 -250
- package/client/vendor/paseo-plugin-helper/components/Badge.tsx +0 -168
- package/client/vendor/paseo-plugin-helper/components/Button.tsx +0 -204
- package/client/vendor/paseo-plugin-helper/components/Card.tsx +0 -257
- package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +0 -196
- package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +0 -277
- package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +0 -172
- package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +0 -180
- package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +0 -97
- package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +0 -70
- package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +0 -88
- package/client/vendor/paseo-plugin-helper/components/InteractiveRow.tsx +0 -142
- package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +0 -446
- package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +0 -247
- package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +0 -117
- package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +0 -53
- package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +0 -132
- package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +0 -87
- package/client/vendor/paseo-plugin-helper/components/Select.tsx +0 -283
- package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +0 -80
- package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +0 -319
- package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +0 -150
- package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +0 -163
- package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +0 -157
- package/client/vendor/paseo-plugin-helper/components/index.ts +0 -26
- package/client/vendor/paseo-plugin-helper/custom-pills.tsx +0 -225
- package/client/vendor/paseo-plugin-helper/forge-icon.tsx +0 -79
- package/client/vendor/paseo-plugin-helper/icon.tsx +0 -39
- package/client/vendor/paseo-plugin-helper/index.ts +0 -30
- package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +0 -49
- package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +0 -103
- package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +0 -65
- package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +0 -368
- package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +0 -51
- package/client/vendor/paseo-plugin-helper/layout/Row.tsx +0 -39
- package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +0 -39
- package/client/vendor/paseo-plugin-helper/layout/index.ts +0 -7
- package/client/vendor/paseo-plugin-helper/styles/index.ts +0 -1
- package/client/vendor/paseo-plugin-helper/styles/recipes.ts +0 -550
- package/client/vendor/paseo-plugin-helper/theme/flair.ts +0 -76
- package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +0 -121
- package/client/vendor/paseo-plugin-helper/theme/index.ts +0 -7
- package/client/vendor/paseo-plugin-helper/theme/provider.tsx +0 -178
- package/client/vendor/paseo-plugin-helper/theme/responsive.ts +0 -213
- package/client/vendor/paseo-plugin-helper/theme/tokens.ts +0 -200
- package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +0 -57
- package/client/vendor/paseo-plugin-helper/tickets.tsx +0 -766
- /package/client/vendor/paseo-plugin-helper/{command-center.ts → core/command-center.ts} +0 -0
- /package/client/vendor/paseo-plugin-helper/{snapshot.ts → core/snapshot.ts} +0 -0
- /package/client/vendor/paseo-plugin-helper/{utils → lifecycle}/haptics.ts +0 -0
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import React, { type ReactNode } from "react";
|
|
2
|
-
import { StyleSheet, Text, View, type StyleProp, type ViewStyle } from "react-native";
|
|
3
|
-
import { usePluginTheme } from "../theme/provider";
|
|
4
|
-
|
|
5
|
-
export interface FormRowProps {
|
|
6
|
-
label: string;
|
|
7
|
-
description?: string;
|
|
8
|
-
children: ReactNode;
|
|
9
|
-
/**
|
|
10
|
-
* "stacked" (default) keeps the historical label/description above the
|
|
11
|
-
* control. "inline" puts the label + description in a left column and the
|
|
12
|
-
* control on the right of the SAME line, which is what a short control
|
|
13
|
-
* (Toggle, StatusDot, Badge, small Button) wants. Stacking a one-line
|
|
14
|
-
* control under its label doubles a settings row's height for no gain
|
|
15
|
-
* (xpufx-org/paseo#213).
|
|
16
|
-
*/
|
|
17
|
-
layout?: "stacked" | "inline";
|
|
18
|
-
style?: StyleProp<ViewStyle>;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function FormRow({
|
|
22
|
-
label,
|
|
23
|
-
description,
|
|
24
|
-
children,
|
|
25
|
-
layout = "stacked",
|
|
26
|
-
style,
|
|
27
|
-
}: FormRowProps) {
|
|
28
|
-
const { colors, flair, typography } = usePluginTheme();
|
|
29
|
-
|
|
30
|
-
const labelBlock = (
|
|
31
|
-
<View style={styles.labelBlock}>
|
|
32
|
-
<Text
|
|
33
|
-
style={[
|
|
34
|
-
styles.label,
|
|
35
|
-
{
|
|
36
|
-
color: colors.foreground,
|
|
37
|
-
...typography.label,
|
|
38
|
-
textTransform: flair.headingTransform === "uppercase" ? "uppercase" : "none",
|
|
39
|
-
},
|
|
40
|
-
]}
|
|
41
|
-
>
|
|
42
|
-
{label}
|
|
43
|
-
</Text>
|
|
44
|
-
{description && (
|
|
45
|
-
<Text
|
|
46
|
-
style={[
|
|
47
|
-
styles.description,
|
|
48
|
-
{ color: colors.foregroundMuted, ...typography.caption },
|
|
49
|
-
]}
|
|
50
|
-
>
|
|
51
|
-
{description}
|
|
52
|
-
</Text>
|
|
53
|
-
)}
|
|
54
|
-
</View>
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
if (layout === "inline") {
|
|
58
|
-
return (
|
|
59
|
-
<View style={[styles.container, styles.inlineContainer, style]}>
|
|
60
|
-
{labelBlock}
|
|
61
|
-
<View style={styles.inlineContent}>{children}</View>
|
|
62
|
-
</View>
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return (
|
|
67
|
-
<View style={[styles.container, style]}>
|
|
68
|
-
{labelBlock}
|
|
69
|
-
<View style={styles.content}>{children}</View>
|
|
70
|
-
</View>
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const styles = StyleSheet.create({
|
|
75
|
-
container: {
|
|
76
|
-
gap: 4,
|
|
77
|
-
width: "100%",
|
|
78
|
-
},
|
|
79
|
-
inlineContainer: {
|
|
80
|
-
flexDirection: "row",
|
|
81
|
-
alignItems: "center",
|
|
82
|
-
justifyContent: "space-between",
|
|
83
|
-
gap: 8,
|
|
84
|
-
},
|
|
85
|
-
labelBlock: {
|
|
86
|
-
gap: 2,
|
|
87
|
-
flexShrink: 1,
|
|
88
|
-
minWidth: 0,
|
|
89
|
-
},
|
|
90
|
-
inlineContent: {
|
|
91
|
-
flexShrink: 0,
|
|
92
|
-
alignItems: "flex-end",
|
|
93
|
-
},
|
|
94
|
-
label: {
|
|
95
|
-
fontWeight: "600",
|
|
96
|
-
},
|
|
97
|
-
description: {
|
|
98
|
-
lineHeight: 16,
|
|
99
|
-
},
|
|
100
|
-
content: {
|
|
101
|
-
marginTop: 2,
|
|
102
|
-
},
|
|
103
|
-
});
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import React, { type ReactNode } from "react";
|
|
2
|
-
import { StyleSheet, View, type StyleProp, type ViewStyle } from "react-native";
|
|
3
|
-
import { usePluginTheme } from "../theme/provider";
|
|
4
|
-
import { resolveSpacing, type SpacingValue } from "../theme/tokens";
|
|
5
|
-
import { resolveGridColumns } from "../theme/responsive";
|
|
6
|
-
|
|
7
|
-
export interface GridProps {
|
|
8
|
-
children?: ReactNode;
|
|
9
|
-
/** Maximum column count. Default: 2. */
|
|
10
|
-
columns?: number;
|
|
11
|
-
/**
|
|
12
|
-
* Minimum width a column should keep. When set and the container width is
|
|
13
|
-
* known, the grid uses as many columns as fit (up to `columns`) and wraps
|
|
14
|
-
* down instead of collapsing to one.
|
|
15
|
-
*/
|
|
16
|
-
minColumnWidth?: number;
|
|
17
|
-
/** Gap between cells: a spacing token or raw px. Defaults to the theme gap. */
|
|
18
|
-
gap?: SpacingValue;
|
|
19
|
-
style?: StyleProp<ViewStyle>;
|
|
20
|
-
testID?: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Width-aware wrapping grid. Cells keep a percentage basis driven by the
|
|
25
|
-
* effective column count, so they reflow across rows rather than stacking
|
|
26
|
-
* one-per-line or collapsing to a single column.
|
|
27
|
-
*/
|
|
28
|
-
export function Grid({ children, columns = 2, minColumnWidth, gap, style, testID }: GridProps) {
|
|
29
|
-
const { padding, isCompact, layout } = usePluginTheme();
|
|
30
|
-
const resolvedGap = resolveSpacing(gap, padding.gap);
|
|
31
|
-
const effectiveColumns = resolveGridColumns({
|
|
32
|
-
columns,
|
|
33
|
-
minColumnWidth,
|
|
34
|
-
gap: resolvedGap,
|
|
35
|
-
width: layout.width,
|
|
36
|
-
isCompact,
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
const cells = React.Children.toArray(children).filter(Boolean);
|
|
40
|
-
|
|
41
|
-
return (
|
|
42
|
-
<View testID={testID} style={[styles.container, { gap: resolvedGap }, style]}>
|
|
43
|
-
{cells.map((child, index) => (
|
|
44
|
-
<View
|
|
45
|
-
key={index}
|
|
46
|
-
style={[styles.cell, { flexBasis: `${Math.floor(100 / effectiveColumns) - 2}%` }]}
|
|
47
|
-
>
|
|
48
|
-
{child}
|
|
49
|
-
</View>
|
|
50
|
-
))}
|
|
51
|
-
</View>
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const styles = StyleSheet.create({
|
|
56
|
-
container: {
|
|
57
|
-
flexDirection: "row",
|
|
58
|
-
flexWrap: "wrap",
|
|
59
|
-
width: "100%",
|
|
60
|
-
},
|
|
61
|
-
cell: {
|
|
62
|
-
flexGrow: 1,
|
|
63
|
-
flexShrink: 1,
|
|
64
|
-
},
|
|
65
|
-
});
|
|
@@ -1,368 +0,0 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
createContext,
|
|
3
|
-
useContext,
|
|
4
|
-
useRef,
|
|
5
|
-
type ComponentType,
|
|
6
|
-
type ReactNode,
|
|
7
|
-
type Ref,
|
|
8
|
-
} from "react";
|
|
9
|
-
import {
|
|
10
|
-
RefreshControl,
|
|
11
|
-
ScrollView as FallbackScrollView,
|
|
12
|
-
StyleSheet,
|
|
13
|
-
View,
|
|
14
|
-
type ScrollView as ScrollViewInstance,
|
|
15
|
-
type ScrollViewProps,
|
|
16
|
-
type StyleProp,
|
|
17
|
-
type ViewStyle,
|
|
18
|
-
} from "react-native";
|
|
19
|
-
import { getOptionalClientHost, selectHostScrollView, type HostScrollView } from "../host";
|
|
20
|
-
import { usePluginTheme } from "../theme/provider";
|
|
21
|
-
|
|
22
|
-
export type ModalBodySize = "default" | "large";
|
|
23
|
-
|
|
24
|
-
export interface ModalBodyProps {
|
|
25
|
-
children: ReactNode;
|
|
26
|
-
style?: StyleProp<ViewStyle>;
|
|
27
|
-
contentContainerStyle?: StyleProp<ViewStyle>;
|
|
28
|
-
header?: ReactNode;
|
|
29
|
-
headerStyle?: StyleProp<ViewStyle>;
|
|
30
|
-
/**
|
|
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.
|
|
35
|
-
*/
|
|
36
|
-
size?: ModalBodySize;
|
|
37
|
-
/**
|
|
38
|
-
* Optional upper bound (px) on the content column width. On large viewports
|
|
39
|
-
* the host still allocates a wide dialog, but the body's content column stays
|
|
40
|
-
* readable instead of stretching edge-to-edge: `width: "100%"` keeps it fluid
|
|
41
|
-
* below the cap and `alignSelf: "center"` centers the capped column. Undefined
|
|
42
|
-
* (default) preserves the fully fluid body. This does not dictate the dialog
|
|
43
|
-
* frame; widen the frame with `size` when dense content genuinely needs room.
|
|
44
|
-
*/
|
|
45
|
-
maxContentWidth?: number;
|
|
46
|
-
/**
|
|
47
|
-
* "scroll" (default): header renders INSIDE the helper-owned compact/mobile
|
|
48
|
-
* ScrollView and moves with content. "pinned": header renders above that
|
|
49
|
-
* compact/mobile scroller; on desktop the host remains the scroll owner.
|
|
50
|
-
*/
|
|
51
|
-
headerMode?: "pinned" | "scroll";
|
|
52
|
-
/**
|
|
53
|
-
* Scroll ownership override. "auto" (default) renders the helper-owned
|
|
54
|
-
* scroller only on compact/mobile surfaces and defers to the host elsewhere.
|
|
55
|
-
* "always" makes the helper the scroll owner on every surface; use it when
|
|
56
|
-
* the host supplies no scroller because the content view is bounded
|
|
57
|
-
* (`ModalContent` passes it for `<Modal.Content scrollable={false}>`).
|
|
58
|
-
*/
|
|
59
|
-
scrollMode?: "auto" | "always";
|
|
60
|
-
/**
|
|
61
|
-
* When set, ModalBody logs measured layout values (viewport height,
|
|
62
|
-
* content height) via onLayout/onContentSizeChange under this tag, e.g.
|
|
63
|
-
* `[ModalBody:mcp] viewport=… content=…`. Use on-device to see which
|
|
64
|
-
* container actually scrolls instead of guessing from theory (#110).
|
|
65
|
-
*/
|
|
66
|
-
debugTag?: string;
|
|
67
|
-
extraBottomInset?: number;
|
|
68
|
-
refreshing?: boolean;
|
|
69
|
-
onRefresh?: () => void | Promise<void>;
|
|
70
|
-
stickToEnd?: boolean;
|
|
71
|
-
scrollRef?: Ref<ScrollViewInstance>;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Scroll-ownership signal for `ModalBody`.
|
|
76
|
-
*
|
|
77
|
-
* - `"helper"` — the default; `ModalBody` decides from the surface
|
|
78
|
-
* (compact/mobile) and `scrollMode`.
|
|
79
|
-
* - `"host"` — an ancestor host view already provides the one scroller
|
|
80
|
-
* (0.8 composer popovers). `ModalBody` renders plain content.
|
|
81
|
-
* - `"required"` — the ancestor has NO host scroller and the content is
|
|
82
|
-
* host-sized, so `ModalBody` MUST own the scroll on every surface. Set by
|
|
83
|
-
* `registerSidebarSurface` (a plugin surface is a full host page whose body
|
|
84
|
-
* is not wrapped in a host scroller) and by `ModalContent`.
|
|
85
|
-
*
|
|
86
|
-
* Adding the `"required"` member is additive: the existing two values keep
|
|
87
|
-
* their meaning and the default stays `"helper"`.
|
|
88
|
-
*/
|
|
89
|
-
export type ModalBodyScrollOwner = "helper" | "host" | "required" | "popover";
|
|
90
|
-
|
|
91
|
-
export const ModalBodyScrollOwnerContext = createContext<ModalBodyScrollOwner>("helper");
|
|
92
|
-
|
|
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.
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Mobile-safe scrollable body for Paseo <Modal.Content>.
|
|
100
|
-
*
|
|
101
|
-
* Size contract: a modal takes the host-allocated dialog size and is fluid
|
|
102
|
-
* within it. `ModalBody` fills that allocation (`flex: 1`, `minHeight: 0`,
|
|
103
|
-
* `width: "100%"`) and never lets its children drive the dialog frame, so the
|
|
104
|
-
* modal stays stable while content loads, refreshes, or grows. Do not wrap it
|
|
105
|
-
* in a container that hardcodes `minWidth`/`minHeight`/`width`/`height` or that
|
|
106
|
-
* sizes itself to its children - that reintroduces content-driven resize/redraw.
|
|
107
|
-
* Shrinkable text uses `minWidth: 0` + `flexShrink: 1`, never a fixed dimension.
|
|
108
|
-
*
|
|
109
|
-
* Host behavior differs by platform: on desktop the host owns a bounded dialog
|
|
110
|
-
* and its outer scroll, so `ModalBody` renders plain content and adds no second
|
|
111
|
-
* scroll region. On mobile the host presents a bottom sheet
|
|
112
|
-
* (`AdaptiveModalSheet`) that already owns the viewport and sheet gesture, so
|
|
113
|
-
* `ModalBody` defers to a host-provided scroller (or a plain view) and only adds
|
|
114
|
-
* the safe bottom inset. Plugin code must not guess either size.
|
|
115
|
-
*
|
|
116
|
-
* Scroll ownership: ordinary compact/mobile modal content uses this helper
|
|
117
|
-
* scroller. A 0.8 composer popover is different: Paseo's MenuSurface already
|
|
118
|
-
* supplies the sole outer scroller, and registerComposerPill marks that
|
|
119
|
-
* subtree through ModalBodyScrollOwnerContext so this component renders plain
|
|
120
|
-
* content instead. Pass `scrollMode="always"` when the host content view is
|
|
121
|
-
* bounded and supplies no scroller (`ModalContent` does this), so the helper
|
|
122
|
-
* scrolls on desktop too instead of clipping the bounded dialog.
|
|
123
|
-
* "pinned" is opt-in. Desktop surfaces retain host-owned scrolling so they do
|
|
124
|
-
* not create a second scrollbar; web hosts pin the header with sticky layout.
|
|
125
|
-
* Automatically calculates responsive bottom padding so controls are not cut off
|
|
126
|
-
* by mobile home bars or virtual keyboards.
|
|
127
|
-
* Supports pull-to-refresh on mobile via `refreshing` and `onRefresh`.
|
|
128
|
-
* Uses the host ScrollView from initClientHelpers when supplied (sheet-gesture
|
|
129
|
-
* integrated on Paseo v0.8), otherwise plain React Native ScrollView.
|
|
130
|
-
* Pass `stickToEnd` for conversation-style views that track new content, or
|
|
131
|
-
* `scrollRef` for imperative scrolling.
|
|
132
|
-
* Pass `header` for a pinned navbar (e.g. <Tabs>): it renders above the
|
|
133
|
-
* scroller in a flex column, so the header stays fixed while the body scrolls.
|
|
134
|
-
* Requires the host <Modal.Content scrollable={false}> so no outer sheet
|
|
135
|
-
* scroller drags the header along.
|
|
136
|
-
*/
|
|
137
|
-
export function ModalBody({
|
|
138
|
-
children,
|
|
139
|
-
style,
|
|
140
|
-
contentContainerStyle,
|
|
141
|
-
header,
|
|
142
|
-
headerStyle,
|
|
143
|
-
headerMode = "scroll",
|
|
144
|
-
maxContentWidth,
|
|
145
|
-
scrollMode = "auto",
|
|
146
|
-
debugTag,
|
|
147
|
-
extraBottomInset = 0,
|
|
148
|
-
refreshing = false,
|
|
149
|
-
onRefresh,
|
|
150
|
-
stickToEnd = false,
|
|
151
|
-
scrollRef,
|
|
152
|
-
}: ModalBodyProps) {
|
|
153
|
-
const { isCompact, isMobile, layout, padding, colors } = usePluginTheme();
|
|
154
|
-
const scrollOwner = useContext(ModalBodyScrollOwnerContext);
|
|
155
|
-
const hostOwnsScroll = scrollOwner === "host" || scrollOwner === "popover";
|
|
156
|
-
// "required": an ancestor proved the host supplies no scroller (plugin
|
|
157
|
-
// surface) — own the scroll on every surface, including non-compact desktop.
|
|
158
|
-
const helperOwnsScroll =
|
|
159
|
-
!hostOwnsScroll &&
|
|
160
|
-
(scrollOwner === "required" || scrollMode === "always" || isCompact || isMobile);
|
|
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.
|
|
166
|
-
const ResolvedScrollView = selectHostScrollView(
|
|
167
|
-
getOptionalClientHost(),
|
|
168
|
-
FallbackScrollView as unknown as HostScrollView,
|
|
169
|
-
);
|
|
170
|
-
// Optional readability cap on the content column.
|
|
171
|
-
//
|
|
172
|
-
// `width: "100%"` keeps the column fluid up to the cap. Centering must use
|
|
173
|
-
// auto side margins, NOT `alignSelf: "center"`: on a ScrollView content
|
|
174
|
-
// container `alignSelf` stops the column stretching to the viewport, so it
|
|
175
|
-
// sizes to its children instead — a wrapping pill row then grows as wide as
|
|
176
|
-
// its pills and never wraps (reported as "pills row rigid" on x-comms).
|
|
177
|
-
// Auto margins center the column while preserving full-width stretch.
|
|
178
|
-
const columnStyle =
|
|
179
|
-
maxContentWidth !== undefined
|
|
180
|
-
? ({
|
|
181
|
-
width: "100%",
|
|
182
|
-
maxWidth: maxContentWidth,
|
|
183
|
-
marginLeft: "auto",
|
|
184
|
-
marginRight: "auto",
|
|
185
|
-
} as ViewStyle)
|
|
186
|
-
: undefined;
|
|
187
|
-
const innerRef = useRef<ScrollViewInstance>(null);
|
|
188
|
-
|
|
189
|
-
const setRefs = (node: ScrollViewInstance | null) => {
|
|
190
|
-
(innerRef as { current: ScrollViewInstance | null }).current = node;
|
|
191
|
-
if (typeof scrollRef === "function") {
|
|
192
|
-
scrollRef(node);
|
|
193
|
-
} else if (scrollRef) {
|
|
194
|
-
(scrollRef as { current: ScrollViewInstance | null }).current = node;
|
|
195
|
-
}
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
// Only real mobile platforms reserve the large bottom inset that clears
|
|
199
|
-
// navigation bars; a compact desktop popover does not need it.
|
|
200
|
-
const bottomPadding = (isMobile ? 48 : 20) + extraBottomInset;
|
|
201
|
-
|
|
202
|
-
const refreshControl = onRefresh ? (
|
|
203
|
-
<RefreshControl
|
|
204
|
-
refreshing={refreshing}
|
|
205
|
-
onRefresh={onRefresh}
|
|
206
|
-
tintColor={colors.accent}
|
|
207
|
-
colors={[colors.accent]}
|
|
208
|
-
/>
|
|
209
|
-
) : undefined;
|
|
210
|
-
|
|
211
|
-
// Compact without an injected host scroller still scrolls via the plain
|
|
212
|
-
// fallback: the helper pill sets <Modal.Content scrollable={false}>, so no
|
|
213
|
-
// outer sheet scroller fights the inner one.
|
|
214
|
-
const log = debugTag
|
|
215
|
-
? (kind: string, v: Record<string, unknown>) =>
|
|
216
|
-
// eslint-disable-next-line no-console
|
|
217
|
-
console.log(`[ModalBody:${debugTag}] ${kind}`, JSON.stringify(v))
|
|
218
|
-
: undefined;
|
|
219
|
-
const scrollBody = (
|
|
220
|
-
<ResolvedScrollView
|
|
221
|
-
ref={setRefs}
|
|
222
|
-
style={[{ backgroundColor: colors.surface0 }, styles.container, style]}
|
|
223
|
-
nestedScrollEnabled={true}
|
|
224
|
-
keyboardShouldPersistTaps="handled"
|
|
225
|
-
showsVerticalScrollIndicator={true}
|
|
226
|
-
refreshControl={refreshControl}
|
|
227
|
-
onLayout={
|
|
228
|
-
log
|
|
229
|
-
? (e: { nativeEvent: { layout: { height: number; width: number } } }) =>
|
|
230
|
-
log("viewport", e.nativeEvent.layout)
|
|
231
|
-
: undefined
|
|
232
|
-
}
|
|
233
|
-
onContentSizeChange={(w: number, h: number) => {
|
|
234
|
-
log?.("content", { width: w, height: h });
|
|
235
|
-
if (stickToEnd) innerRef.current?.scrollToEnd({ animated: true });
|
|
236
|
-
}}
|
|
237
|
-
contentContainerStyle={[
|
|
238
|
-
styles.content,
|
|
239
|
-
{
|
|
240
|
-
paddingHorizontal: padding.horizontal,
|
|
241
|
-
paddingTop: padding.vertical,
|
|
242
|
-
paddingBottom: bottomPadding,
|
|
243
|
-
gap: padding.gap,
|
|
244
|
-
},
|
|
245
|
-
columnStyle,
|
|
246
|
-
contentContainerStyle,
|
|
247
|
-
]}
|
|
248
|
-
>
|
|
249
|
-
{children}
|
|
250
|
-
</ResolvedScrollView>
|
|
251
|
-
);
|
|
252
|
-
|
|
253
|
-
const stickyHeaderStyle =
|
|
254
|
-
headerMode === "pinned" && layout.platform === "web"
|
|
255
|
-
? ({
|
|
256
|
-
position: "sticky",
|
|
257
|
-
top: 0,
|
|
258
|
-
zIndex: 10,
|
|
259
|
-
backgroundColor: colors.surface0,
|
|
260
|
-
} as unknown as ViewStyle)
|
|
261
|
-
: undefined;
|
|
262
|
-
const plainBody = (
|
|
263
|
-
<View style={[styles.plainBody, style]}>
|
|
264
|
-
{header ? (
|
|
265
|
-
<View style={[styles.header, stickyHeaderStyle, headerStyle]}>{header}</View>
|
|
266
|
-
) : null}
|
|
267
|
-
<View
|
|
268
|
-
style={[
|
|
269
|
-
styles.plainContent,
|
|
270
|
-
{
|
|
271
|
-
paddingHorizontal: padding.horizontal,
|
|
272
|
-
paddingTop: padding.vertical,
|
|
273
|
-
paddingBottom: bottomPadding,
|
|
274
|
-
gap: padding.gap,
|
|
275
|
-
},
|
|
276
|
-
columnStyle,
|
|
277
|
-
contentContainerStyle,
|
|
278
|
-
]}
|
|
279
|
-
>
|
|
280
|
-
{children}
|
|
281
|
-
</View>
|
|
282
|
-
</View>
|
|
283
|
-
);
|
|
284
|
-
|
|
285
|
-
if (!helperOwnsScroll) return plainBody;
|
|
286
|
-
if (!header) return scrollBody;
|
|
287
|
-
|
|
288
|
-
// "scroll" keeps the header in the content flow for compact/mobile hosts.
|
|
289
|
-
// "pinned" is handled below with a helper-owned scroller on every platform.
|
|
290
|
-
if (headerMode === "scroll") {
|
|
291
|
-
return (
|
|
292
|
-
<ResolvedScrollView
|
|
293
|
-
ref={setRefs}
|
|
294
|
-
style={[{ backgroundColor: colors.surface0 }, styles.container, style]}
|
|
295
|
-
nestedScrollEnabled={true}
|
|
296
|
-
keyboardShouldPersistTaps="handled"
|
|
297
|
-
showsVerticalScrollIndicator={true}
|
|
298
|
-
refreshControl={refreshControl}
|
|
299
|
-
onLayout={
|
|
300
|
-
log
|
|
301
|
-
? (e: { nativeEvent: { layout: { height: number; width: number } } }) =>
|
|
302
|
-
log("viewport", e.nativeEvent.layout)
|
|
303
|
-
: undefined
|
|
304
|
-
}
|
|
305
|
-
onContentSizeChange={(w: number, h: number) => {
|
|
306
|
-
log?.("content", { width: w, height: h });
|
|
307
|
-
if (stickToEnd) innerRef.current?.scrollToEnd({ animated: true });
|
|
308
|
-
}}
|
|
309
|
-
contentContainerStyle={[
|
|
310
|
-
styles.content,
|
|
311
|
-
{
|
|
312
|
-
paddingHorizontal: padding.horizontal,
|
|
313
|
-
paddingTop: padding.vertical,
|
|
314
|
-
paddingBottom: bottomPadding,
|
|
315
|
-
gap: padding.gap,
|
|
316
|
-
},
|
|
317
|
-
columnStyle,
|
|
318
|
-
contentContainerStyle,
|
|
319
|
-
]}
|
|
320
|
-
>
|
|
321
|
-
<View style={[styles.header, headerStyle]}>{header}</View>
|
|
322
|
-
{children}
|
|
323
|
-
</ResolvedScrollView>
|
|
324
|
-
);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
return (
|
|
328
|
-
<View style={[styles.screen]}>
|
|
329
|
-
<View style={[styles.header, headerStyle]}>{header}</View>
|
|
330
|
-
{scrollBody}
|
|
331
|
-
</View>
|
|
332
|
-
);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
const styles = StyleSheet.create({
|
|
336
|
-
screen: {
|
|
337
|
-
flex: 1,
|
|
338
|
-
minHeight: 0,
|
|
339
|
-
width: "100%",
|
|
340
|
-
},
|
|
341
|
-
header: {
|
|
342
|
-
width: "100%",
|
|
343
|
-
flexShrink: 0,
|
|
344
|
-
},
|
|
345
|
-
plainBody: {
|
|
346
|
-
flex: 1,
|
|
347
|
-
minHeight: 0,
|
|
348
|
-
width: "100%",
|
|
349
|
-
maxWidth: "100%",
|
|
350
|
-
},
|
|
351
|
-
plainContent: {
|
|
352
|
-
flexGrow: 1,
|
|
353
|
-
minHeight: 0,
|
|
354
|
-
width: "100%",
|
|
355
|
-
maxWidth: "100%",
|
|
356
|
-
},
|
|
357
|
-
container: {
|
|
358
|
-
flex: 1,
|
|
359
|
-
minHeight: 0,
|
|
360
|
-
width: "100%",
|
|
361
|
-
maxWidth: "100%",
|
|
362
|
-
},
|
|
363
|
-
content: {
|
|
364
|
-
flexGrow: 1,
|
|
365
|
-
width: "100%",
|
|
366
|
-
maxWidth: "100%",
|
|
367
|
-
},
|
|
368
|
-
});
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import React, { type ReactNode } from "react";
|
|
2
|
-
import { getClientHost } from "../host";
|
|
3
|
-
import { ModalBody, ModalBodyScrollOwnerContext, type ModalBodyProps } from "./ModalBody";
|
|
4
|
-
|
|
5
|
-
export interface ModalContentProps extends Omit<ModalBodyProps, "scrollMode"> {
|
|
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;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Helper-owned modal body for plugins that open their own host `<Modal>`.
|
|
19
|
-
*
|
|
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.
|
|
23
|
-
*
|
|
24
|
-
* Sizing stays fluid within the host-allocated modal frame, respecting
|
|
25
|
-
* `size?: "default" | "large"`.
|
|
26
|
-
*
|
|
27
|
-
* ```tsx
|
|
28
|
-
* <Modal title="…" open={open} onOpenChange={setOpen}>
|
|
29
|
-
* <ModalContent size="default">
|
|
30
|
-
* …cards, controls, rows…
|
|
31
|
-
* </ModalContent>
|
|
32
|
-
* </Modal>
|
|
33
|
-
* ```
|
|
34
|
-
*
|
|
35
|
-
* Accepts every `ModalBody` prop (header, headerMode, refreshing, onRefresh,
|
|
36
|
-
* stickToEnd, scrollRef, debugTag, style, contentContainerStyle, …).
|
|
37
|
-
*/
|
|
38
|
-
export function ModalContent({
|
|
39
|
-
children,
|
|
40
|
-
scrollable = true,
|
|
41
|
-
...bodyProps
|
|
42
|
-
}: ModalContentProps) {
|
|
43
|
-
const { Modal } = getClientHost();
|
|
44
|
-
return (
|
|
45
|
-
<Modal.Content scrollable={scrollable}>
|
|
46
|
-
<ModalBodyScrollOwnerContext.Provider value={scrollable ? "host" : "required"}>
|
|
47
|
-
<ModalBody {...bodyProps}>{children}</ModalBody>
|
|
48
|
-
</ModalBodyScrollOwnerContext.Provider>
|
|
49
|
-
</Modal.Content>
|
|
50
|
-
);
|
|
51
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import React, { type ReactNode } from "react";
|
|
2
|
-
import { View, type StyleProp, type ViewStyle } from "react-native";
|
|
3
|
-
import { usePluginTheme } from "../theme/provider";
|
|
4
|
-
import { resolveSpacing, type SpacingValue } from "../theme/tokens";
|
|
5
|
-
|
|
6
|
-
export interface RowProps {
|
|
7
|
-
children?: ReactNode;
|
|
8
|
-
/** Gap between children: a spacing token or raw px. Defaults to the theme gap. */
|
|
9
|
-
gap?: SpacingValue;
|
|
10
|
-
/** Allow children to wrap onto the next line. Default: false. */
|
|
11
|
-
wrap?: boolean;
|
|
12
|
-
align?: ViewStyle["alignItems"];
|
|
13
|
-
justify?: ViewStyle["justifyContent"];
|
|
14
|
-
style?: StyleProp<ViewStyle>;
|
|
15
|
-
testID?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Horizontal flexbox row with theme-derived gap. A thin vocabulary wrapper so
|
|
20
|
-
* plugins stop hand-rolling `<View style={{ flexDirection: "row", gap }}>`.
|
|
21
|
-
*/
|
|
22
|
-
export function Row({ children, gap, wrap = false, align, justify, style, testID }: RowProps) {
|
|
23
|
-
const { padding } = usePluginTheme();
|
|
24
|
-
|
|
25
|
-
return (
|
|
26
|
-
<View
|
|
27
|
-
testID={testID}
|
|
28
|
-
style={[
|
|
29
|
-
{ flexDirection: "row", gap: resolveSpacing(gap, padding.gap) },
|
|
30
|
-
wrap && { flexWrap: "wrap" },
|
|
31
|
-
align !== undefined && { alignItems: align },
|
|
32
|
-
justify !== undefined && { justifyContent: justify },
|
|
33
|
-
style,
|
|
34
|
-
]}
|
|
35
|
-
>
|
|
36
|
-
{children}
|
|
37
|
-
</View>
|
|
38
|
-
);
|
|
39
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import React, { type ReactNode } from "react";
|
|
2
|
-
import { View, type StyleProp, type ViewStyle } from "react-native";
|
|
3
|
-
import { usePluginTheme } from "../theme/provider";
|
|
4
|
-
import { resolveSpacing, type SpacingValue } from "../theme/tokens";
|
|
5
|
-
|
|
6
|
-
export interface StackProps {
|
|
7
|
-
children?: ReactNode;
|
|
8
|
-
/** Gap between children: a spacing token or raw px. Defaults to the theme gap. */
|
|
9
|
-
gap?: SpacingValue;
|
|
10
|
-
align?: ViewStyle["alignItems"];
|
|
11
|
-
justify?: ViewStyle["justifyContent"];
|
|
12
|
-
style?: StyleProp<ViewStyle>;
|
|
13
|
-
testID?: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Vertical flexbox stack with theme-derived gap. The deliberate column default,
|
|
18
|
-
* named so it composes visually alongside {@link Row}.
|
|
19
|
-
*/
|
|
20
|
-
export function Stack({ children, gap, align, justify, style, testID }: StackProps) {
|
|
21
|
-
const { padding } = usePluginTheme();
|
|
22
|
-
|
|
23
|
-
return (
|
|
24
|
-
<View
|
|
25
|
-
testID={testID}
|
|
26
|
-
style={[
|
|
27
|
-
{ flexDirection: "column", gap: resolveSpacing(gap, padding.gap) },
|
|
28
|
-
align !== undefined && { alignItems: align },
|
|
29
|
-
justify !== undefined && { justifyContent: justify },
|
|
30
|
-
style,
|
|
31
|
-
]}
|
|
32
|
-
>
|
|
33
|
-
{children}
|
|
34
|
-
</View>
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Explicit vertical-stack alias; identical to {@link Stack}. */
|
|
39
|
-
export const VStack = Stack;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./recipes";
|