@xpufx/paseo-top 0.4.4 → 0.4.7

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 (130) hide show
  1. package/OVERVIEW.md +18 -0
  2. package/client/host-ui.tsx +184 -0
  3. package/client/multi-host-view.tsx +57 -43
  4. package/client/multi-host.ts +36 -13
  5. package/client/pill-labels.ts +94 -1
  6. package/client/pill.tsx +692 -616
  7. package/client/resources-query.ts +1 -1
  8. package/client/settings-events.ts +1 -1
  9. package/client/settings-ui.tsx +86 -9
  10. package/client/surface.tsx +308 -276
  11. package/client/telemetry-copy.ts +8 -2
  12. package/client/telemetry.tsx +372 -321
  13. package/client/turn-panel.tsx +126 -104
  14. package/client/vendor/paseo-plugin-helper/{host.ts → core/host.ts} +9 -3
  15. package/client/vendor/paseo-plugin-helper/core/index.ts +76 -0
  16. package/client/vendor/paseo-plugin-helper/core/plugin-load-count.ts +89 -0
  17. package/client/vendor/paseo-plugin-helper/{query-refresh.ts → core/query-refresh.ts} +2 -2
  18. package/client/vendor/paseo-plugin-helper/{query.ts → core/query.ts} +2 -2
  19. package/client/vendor/paseo-plugin-helper/{settings.ts → core/settings.ts} +1 -1
  20. package/client/vendor/paseo-plugin-helper/{shared-settings.ts → core/shared-settings.ts} +2 -2
  21. package/client/vendor/paseo-plugin-helper/{utils → lifecycle}/clipboard.ts +2 -2
  22. package/client/vendor/paseo-plugin-helper/lifecycle/flair.ts +26 -0
  23. package/client/vendor/paseo-plugin-helper/{theme/color-utils.ts → lifecycle/host-color.ts} +95 -38
  24. package/client/vendor/paseo-plugin-helper/lifecycle/host-theme.tsx +109 -0
  25. package/client/vendor/paseo-plugin-helper/lifecycle/index.ts +23 -0
  26. package/client/vendor/paseo-plugin-helper/lifecycle/multi-host.ts +66 -0
  27. package/client/vendor/paseo-plugin-helper/{panel.tsx → lifecycle/panel.tsx} +7 -7
  28. package/client/vendor/paseo-plugin-helper/{pill.tsx → lifecycle/pill.tsx} +51 -18
  29. package/client/vendor/paseo-plugin-helper/lifecycle/providers.tsx +46 -0
  30. package/client/vendor/paseo-plugin-helper/lifecycle/scroll-owner.ts +26 -0
  31. package/client/vendor/paseo-plugin-helper/{surface.tsx → lifecycle/surface.tsx} +6 -6
  32. package/client/vendor/paseo-plugin-helper/ui/color.ts +1 -0
  33. package/client/vendor/paseo-plugin-helper/ui/content.tsx +1326 -0
  34. package/client/vendor/paseo-plugin-helper/ui/controls.tsx +1018 -0
  35. package/client/vendor/paseo-plugin-helper/{components/DataTable.tsx → ui/data.tsx} +50 -58
  36. package/client/vendor/paseo-plugin-helper/ui/index.ts +44 -0
  37. package/client/vendor/paseo-plugin-helper/ui/layout.tsx +263 -0
  38. package/client/vendor/paseo-plugin-helper/ui/modal.tsx +111 -0
  39. package/client/vendor/paseo-plugin-helper/ui/plugin-reload-banner.tsx +119 -0
  40. package/client/vendor/paseo-plugin-helper/{settings-screen.tsx → ui/settings-screen.tsx} +3 -3
  41. package/client/vendor/paseo-plugin-helper/ui/settings.ts +36 -0
  42. package/client/vendor/paseo-plugin-helper/ui/text.tsx +821 -0
  43. package/client/vendor/paseo-plugin-helper/ui/theme.tsx +1 -0
  44. package/client/vendor/permission-audit/client.tsx +706 -0
  45. package/client/vendor/permission-audit/index.tsx +1 -0
  46. package/index.client.tsx +58 -37
  47. package/index.server.ts +256 -234
  48. package/npm-shrinkwrap.json +44 -0
  49. package/package.json +8 -4
  50. package/paseo-plugin.json +2 -0
  51. package/screenshots/1-paseo-top-modal-system.jpg +0 -0
  52. package/screenshots/2-paseo-top-modal-workspace.jpg +0 -0
  53. package/screenshots/3-paseo-top-modal-settings.jpg +0 -0
  54. package/screenshots/4-paseo-top-modal-about.jpg +0 -0
  55. package/screenshots/paseo-top-dashboard.jpg +0 -0
  56. package/screenshots/paseo-top-general-view.jpg +0 -0
  57. package/server/resources.ts +98 -0
  58. package/server/vendor/paseo-plugin-helper/custom-pills.ts +70 -0
  59. package/server/vendor/paseo-plugin-helper/index.ts +2 -0
  60. package/server/vendor/paseo-plugin-helper/logger.ts +5 -16
  61. package/server/vendor/paseo-plugin-helper/rpc-guard.ts +2 -2
  62. package/server/vendor/paseo-plugin-helper/storage-guard.ts +294 -0
  63. package/server/vendor/paseo-plugin-helper/storage.ts +36 -2
  64. package/server/vendor/paseo-plugin-helper/telemetry.ts +486 -0
  65. package/server/vendor/paseo-plugin-helper/tickets.ts +18 -67
  66. package/server/vendor/permission-audit/index.ts +1 -0
  67. package/server/vendor/permission-audit/server.ts +839 -0
  68. package/shared/helper-version.ts +1 -1
  69. package/shared/multi-host.ts +153 -159
  70. package/shared/resources.ts +145 -0
  71. package/shared/vendor/paseo-plugin-helper/async.ts +14 -7
  72. package/shared/vendor/paseo-plugin-helper/features.ts +244 -0
  73. package/shared/vendor/paseo-plugin-helper/forge.ts +27 -0
  74. package/shared/vendor/paseo-plugin-helper/index.ts +1 -0
  75. package/shared/vendor/paseo-plugin-helper/tickets.ts +122 -28
  76. package/shared/vendor/permission-audit/filter.ts +61 -0
  77. package/shared/vendor/permission-audit/index.ts +2 -0
  78. package/shared/vendor/permission-audit/shared.ts +119 -0
  79. package/shared/version.ts +1 -1
  80. package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +0 -493
  81. package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +0 -250
  82. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +0 -168
  83. package/client/vendor/paseo-plugin-helper/components/Button.tsx +0 -204
  84. package/client/vendor/paseo-plugin-helper/components/Card.tsx +0 -257
  85. package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +0 -196
  86. package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +0 -277
  87. package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +0 -172
  88. package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +0 -180
  89. package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +0 -97
  90. package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +0 -70
  91. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +0 -88
  92. package/client/vendor/paseo-plugin-helper/components/InteractiveRow.tsx +0 -142
  93. package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +0 -446
  94. package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +0 -247
  95. package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +0 -117
  96. package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +0 -53
  97. package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +0 -132
  98. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +0 -87
  99. package/client/vendor/paseo-plugin-helper/components/Select.tsx +0 -283
  100. package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +0 -80
  101. package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +0 -319
  102. package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +0 -150
  103. package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +0 -163
  104. package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +0 -157
  105. package/client/vendor/paseo-plugin-helper/components/index.ts +0 -26
  106. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +0 -225
  107. package/client/vendor/paseo-plugin-helper/forge-icon.tsx +0 -79
  108. package/client/vendor/paseo-plugin-helper/icon.tsx +0 -39
  109. package/client/vendor/paseo-plugin-helper/index.ts +0 -30
  110. package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +0 -49
  111. package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +0 -103
  112. package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +0 -65
  113. package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +0 -368
  114. package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +0 -51
  115. package/client/vendor/paseo-plugin-helper/layout/Row.tsx +0 -39
  116. package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +0 -39
  117. package/client/vendor/paseo-plugin-helper/layout/index.ts +0 -7
  118. package/client/vendor/paseo-plugin-helper/styles/index.ts +0 -1
  119. package/client/vendor/paseo-plugin-helper/styles/recipes.ts +0 -550
  120. package/client/vendor/paseo-plugin-helper/theme/flair.ts +0 -76
  121. package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +0 -121
  122. package/client/vendor/paseo-plugin-helper/theme/index.ts +0 -7
  123. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +0 -178
  124. package/client/vendor/paseo-plugin-helper/theme/responsive.ts +0 -213
  125. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +0 -200
  126. package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +0 -57
  127. package/client/vendor/paseo-plugin-helper/tickets.tsx +0 -766
  128. /package/client/vendor/paseo-plugin-helper/{command-center.ts → core/command-center.ts} +0 -0
  129. /package/client/vendor/paseo-plugin-helper/{snapshot.ts → core/snapshot.ts} +0 -0
  130. /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,7 +0,0 @@
1
- export * from "./ModalBody";
2
- export * from "./ModalContent";
3
- export * from "./ActionBar";
4
- export * from "./FormRow";
5
- export * from "./Row";
6
- export * from "./Stack";
7
- export * from "./Grid";
@@ -1 +0,0 @@
1
- export * from "./recipes";