@xpufx/paseo-top 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +11 -12
  2. package/client/multi-host-view.tsx +177 -0
  3. package/client/multi-host.ts +98 -0
  4. package/client/pill-labels.ts +30 -6
  5. package/client/pill.tsx +42 -61
  6. package/client/settings-events.ts +33 -0
  7. package/client/surface.tsx +10 -9
  8. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +12 -0
  9. package/client/vendor/paseo-plugin-helper/components/Button.tsx +29 -3
  10. package/client/vendor/paseo-plugin-helper/components/Card.tsx +32 -0
  11. package/client/vendor/paseo-plugin-helper/components/DataTable.tsx +17 -1
  12. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +17 -2
  13. package/client/vendor/paseo-plugin-helper/components/InteractiveRow.tsx +142 -0
  14. package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +16 -2
  15. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +7 -0
  16. package/client/vendor/paseo-plugin-helper/components/Select.tsx +131 -63
  17. package/client/vendor/paseo-plugin-helper/components/index.ts +1 -0
  18. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +4 -3
  19. package/client/vendor/paseo-plugin-helper/host.ts +12 -58
  20. package/client/vendor/paseo-plugin-helper/index.ts +2 -0
  21. package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +19 -29
  22. package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +24 -22
  23. package/client/vendor/paseo-plugin-helper/panel.tsx +11 -3
  24. package/client/vendor/paseo-plugin-helper/pill.tsx +135 -339
  25. package/client/vendor/paseo-plugin-helper/styles/index.ts +1 -0
  26. package/client/vendor/paseo-plugin-helper/styles/recipes.ts +550 -0
  27. package/client/vendor/paseo-plugin-helper/surface.tsx +62 -2
  28. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +3 -39
  29. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +40 -1
  30. package/client/vendor/paseo-plugin-helper/tickets.tsx +766 -0
  31. package/index.server.ts +7 -0
  32. package/package.json +12 -3
  33. package/paseo-plugin.json +1 -1
  34. package/server/vendor/paseo-plugin-helper/index.ts +2 -0
  35. package/server/vendor/paseo-plugin-helper/mcp-injection.ts +20 -2
  36. package/server/vendor/paseo-plugin-helper/plugins.ts +184 -19
  37. package/server/vendor/paseo-plugin-helper/tickets.ts +1281 -0
  38. package/shared/helper-version.ts +20 -0
  39. package/shared/multi-host.ts +348 -0
  40. package/shared/resources.ts +1 -1
  41. package/shared/vendor/paseo-plugin-helper/index.ts +1 -0
  42. package/shared/vendor/paseo-plugin-helper/tickets.ts +1010 -0
  43. package/shared/version.ts +1 -1
@@ -0,0 +1,550 @@
1
+ import type { TextStyle, ViewStyle } from "react-native";
2
+ import type { PluginTheme, StatusVariant, ThemeColors } from "../../../../shared/vendor/paseo-plugin-helper/types";
3
+ import { alpha, getStatusColor, getVariantPalette, getContrastColor } from "../theme/color-utils";
4
+ import { defaultDarkTheme, FALLBACK_ACCENT_FOREGROUND } from "../theme/tokens";
5
+ import type { PluginThemeContextValue } from "../theme/provider";
6
+ import type { SurfaceStyle } from "../theme/flair";
7
+ import type { ButtonSize, ButtonVariant } from "../components/Button";
8
+ import type { BadgeSize, BadgeStyle } from "../components/Badge";
9
+
10
+ export type ThemeInput =
11
+ | PluginTheme
12
+ | PluginThemeContextValue
13
+ | ThemeColors
14
+ | { colors: ThemeColors }
15
+ | undefined;
16
+
17
+ /**
18
+ * Resolves ThemeColors from a PluginTheme, PluginThemeContextValue, ThemeColors, or undefined.
19
+ */
20
+ export function resolveThemeColors(themeInput?: ThemeInput): ThemeColors {
21
+ if (!themeInput || typeof themeInput !== "object") return defaultDarkTheme.colors;
22
+ if ("colors" in themeInput && themeInput.colors && typeof themeInput.colors === "object") {
23
+ return { ...defaultDarkTheme.colors, ...themeInput.colors };
24
+ }
25
+ if ("surface0" in themeInput && typeof (themeInput as ThemeColors).surface0 === "string") {
26
+ return { ...defaultDarkTheme.colors, ...(themeInput as ThemeColors) };
27
+ }
28
+ return defaultDarkTheme.colors;
29
+ }
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // 1. INPUT RECIPE
33
+ // ---------------------------------------------------------------------------
34
+
35
+ export interface InputRecipeOptions {
36
+ focused?: boolean;
37
+ isFocused?: boolean;
38
+ error?: boolean;
39
+ hasError?: boolean;
40
+ disabled?: boolean;
41
+ multiline?: boolean;
42
+ mono?: boolean;
43
+ compact?: boolean;
44
+ size?: "sm" | "md" | "lg";
45
+ }
46
+
47
+ export interface InputRecipeResult extends TextStyle, ViewStyle {
48
+ input: TextStyle & ViewStyle;
49
+ container: ViewStyle;
50
+ label: TextStyle;
51
+ hint: TextStyle;
52
+ }
53
+
54
+ /**
55
+ * Generates standard React Native styles for text inputs and surrounding container/label/hint elements.
56
+ */
57
+ export function inputRecipe(
58
+ themeInput?: ThemeInput,
59
+ options?: InputRecipeOptions,
60
+ ): InputRecipeResult {
61
+ const colors = resolveThemeColors(themeInput);
62
+ const isFocused = Boolean(options?.focused ?? options?.isFocused);
63
+ const hasError = Boolean(options?.error ?? options?.hasError);
64
+ const disabled = Boolean(options?.disabled);
65
+ const multiline = Boolean(options?.multiline);
66
+ const mono = Boolean(options?.mono);
67
+ const compact = Boolean(options?.compact);
68
+
69
+ const borderColor = hasError
70
+ ? colors.statusDanger
71
+ : isFocused
72
+ ? colors.accent
73
+ : colors.border;
74
+
75
+ const minHeight = multiline
76
+ ? 64
77
+ : options?.size === "sm"
78
+ ? 30
79
+ : options?.size === "lg"
80
+ ? 42
81
+ : compact
82
+ ? 32
83
+ : 36;
84
+ const fontSize =
85
+ options?.size === "sm" ? 12 : options?.size === "lg" ? 15 : compact ? 13 : 14;
86
+ const paddingVertical = multiline
87
+ ? 8
88
+ : options?.size === "sm"
89
+ ? 4
90
+ : options?.size === "lg"
91
+ ? 8
92
+ : 6;
93
+ const paddingHorizontal =
94
+ options?.size === "sm" ? 8 : options?.size === "lg" ? 12 : 10;
95
+
96
+ const baseInput: TextStyle & ViewStyle = {
97
+ backgroundColor: disabled ? alpha(colors.surface1, 0.5) : colors.surface0,
98
+ borderColor,
99
+ borderWidth: 1,
100
+ borderRadius: 8,
101
+ color: disabled ? colors.foregroundMuted : colors.foreground,
102
+ fontSize,
103
+ fontFamily: mono ? "monospace" : undefined,
104
+ minHeight,
105
+ paddingHorizontal,
106
+ paddingVertical,
107
+ };
108
+
109
+ const container: ViewStyle = {
110
+ gap: 4,
111
+ width: "100%",
112
+ };
113
+
114
+ const label: TextStyle = {
115
+ color: hasError ? colors.statusDanger : colors.foreground,
116
+ fontSize: compact ? 12 : 13,
117
+ fontWeight: "600",
118
+ };
119
+
120
+ const hint: TextStyle = {
121
+ color: hasError ? colors.statusDanger : colors.foregroundMuted,
122
+ fontSize: 11,
123
+ marginTop: 2,
124
+ };
125
+
126
+ const input = { ...baseInput };
127
+ return Object.assign(baseInput, {
128
+ input,
129
+ container,
130
+ label,
131
+ hint,
132
+ placeholderColor: colors.foregroundMuted,
133
+ });
134
+ }
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // 2. CARD RECIPE
138
+ // ---------------------------------------------------------------------------
139
+
140
+ export interface CardRecipeOptions {
141
+ variant?: SurfaceStyle;
142
+ noPadding?: boolean;
143
+ radius?: number;
144
+ borderWidth?: number;
145
+ compact?: boolean;
146
+ }
147
+
148
+ export interface CardRecipeResult extends ViewStyle {
149
+ card: ViewStyle;
150
+ header: ViewStyle;
151
+ headerTitle: TextStyle;
152
+ headerSubtitle: TextStyle;
153
+ }
154
+
155
+ /**
156
+ * Generates standard React Native styles for cards and card headers.
157
+ */
158
+ export function cardRecipe(
159
+ themeInput?: ThemeInput,
160
+ optionsOrVariant?: CardRecipeOptions | SurfaceStyle,
161
+ ): CardRecipeResult {
162
+ const colors = resolveThemeColors(themeInput);
163
+ const options: CardRecipeOptions =
164
+ typeof optionsOrVariant === "string" ? { variant: optionsOrVariant } : (optionsOrVariant ?? {});
165
+
166
+ const variant = options.variant ?? "flat";
167
+ const noPadding = Boolean(options.noPadding);
168
+ const compact = Boolean(options.compact);
169
+ const radius = options.radius ?? (compact ? 6 : 8);
170
+ const borderWidth = options.borderWidth ?? 1;
171
+
172
+ let bg = colors.surface0;
173
+ let border = colors.border;
174
+
175
+ if (variant === "tinted") {
176
+ bg = alpha(colors.accent, 0.04);
177
+ border = alpha(colors.accent, 0.2);
178
+ } else if (variant === "elevated") {
179
+ bg = colors.surface1;
180
+ border = colors.border;
181
+ }
182
+
183
+ const baseCard: ViewStyle = {
184
+ backgroundColor: bg,
185
+ borderColor: border,
186
+ borderWidth,
187
+ borderRadius: radius,
188
+ paddingHorizontal: noPadding ? 0 : compact ? 8 : 12,
189
+ paddingVertical: noPadding ? 0 : compact ? 6 : 10,
190
+ width: "100%",
191
+ overflow: "hidden",
192
+ };
193
+
194
+ const header: ViewStyle = {
195
+ flexDirection: "row",
196
+ alignItems: "center",
197
+ justifyContent: "space-between",
198
+ flexWrap: "wrap",
199
+ gap: compact ? 4 : 8,
200
+ width: "100%",
201
+ marginBottom: compact ? 4 : 8,
202
+ };
203
+
204
+ const headerTitle: TextStyle = {
205
+ color: colors.foreground,
206
+ fontSize: compact ? 13 : 14,
207
+ fontWeight: "600",
208
+ lineHeight: compact ? 18 : 20,
209
+ };
210
+
211
+ const headerSubtitle: TextStyle = {
212
+ color: colors.foregroundMuted,
213
+ fontSize: compact ? 10 : 11,
214
+ lineHeight: compact ? 14 : 15,
215
+ fontWeight: "400",
216
+ };
217
+
218
+ const card = { ...baseCard };
219
+ return Object.assign(baseCard, {
220
+ card,
221
+ header,
222
+ headerTitle,
223
+ headerSubtitle,
224
+ });
225
+ }
226
+
227
+ // ---------------------------------------------------------------------------
228
+ // 3. BUTTON RECIPE
229
+ // ---------------------------------------------------------------------------
230
+
231
+ export interface ButtonRecipeOptions {
232
+ variant?: ButtonVariant;
233
+ size?: ButtonSize;
234
+ pressed?: boolean;
235
+ disabled?: boolean;
236
+ loading?: boolean;
237
+ compact?: boolean;
238
+ }
239
+
240
+ export interface ButtonRecipeResult extends ViewStyle {
241
+ container: ViewStyle;
242
+ text: TextStyle;
243
+ }
244
+
245
+ /**
246
+ * Generates standard React Native styles for interactive buttons and text labels.
247
+ */
248
+ export function buttonRecipe(
249
+ themeInput?: ThemeInput,
250
+ optionsOrVariant?: ButtonRecipeOptions | ButtonVariant,
251
+ ): ButtonRecipeResult {
252
+ const colors = resolveThemeColors(themeInput);
253
+ const options: ButtonRecipeOptions =
254
+ typeof optionsOrVariant === "string" ? { variant: optionsOrVariant } : (optionsOrVariant ?? {});
255
+
256
+ const variant = options.variant ?? "secondary";
257
+ const size = options.size ?? "md";
258
+ const pressed = Boolean(options.pressed);
259
+ const disabled = Boolean(options.disabled || options.loading);
260
+ const compact = Boolean(options.compact);
261
+
262
+ const radius = size === "sm" ? 6 : size === "lg" ? 10 : 8;
263
+ const py = size === "sm" ? (compact ? 5 : 6) : size === "lg" ? 12 : compact ? 8 : 10;
264
+ const px = size === "sm" ? (compact ? 8 : 10) : size === "lg" ? 18 : compact ? 12 : 14;
265
+ const fontSize = size === "sm" ? 12 : size === "lg" ? 15 : 13;
266
+
267
+ let bg = "transparent";
268
+ let border = "transparent";
269
+ let textColor = colors.foreground;
270
+
271
+ switch (variant) {
272
+ case "primary":
273
+ bg = colors.accent;
274
+ textColor = colors.accentForeground || FALLBACK_ACCENT_FOREGROUND;
275
+ break;
276
+ case "danger":
277
+ bg = alpha(colors.statusDanger, 0.15);
278
+ border = alpha(colors.statusDanger, 0.4);
279
+ textColor = colors.statusDanger;
280
+ break;
281
+ case "ghost":
282
+ bg = "transparent";
283
+ textColor = colors.foregroundMuted;
284
+ break;
285
+ case "secondary":
286
+ default:
287
+ bg = colors.surface1;
288
+ border = colors.border;
289
+ textColor = colors.foreground;
290
+ break;
291
+ }
292
+
293
+ const effectiveBg = pressed && !disabled ? alpha(bg === "transparent" ? colors.surface2 : bg, 0.8) : bg;
294
+
295
+ const container: ViewStyle = {
296
+ flexDirection: "row",
297
+ alignItems: "center",
298
+ justifyContent: "center",
299
+ gap: 6,
300
+ backgroundColor: effectiveBg,
301
+ borderColor: border,
302
+ borderWidth: border !== "transparent" ? 1 : 0,
303
+ borderRadius: radius,
304
+ paddingVertical: py,
305
+ paddingHorizontal: px,
306
+ minHeight: size === "sm" ? 28 : size === "lg" ? 42 : 34,
307
+ opacity: disabled ? 0.45 : 1,
308
+ };
309
+
310
+ const text: TextStyle = {
311
+ color: textColor,
312
+ fontSize,
313
+ fontWeight: "600",
314
+ textAlign: "center",
315
+ };
316
+
317
+ const subContainer = { ...container };
318
+ return Object.assign(container, {
319
+ container: subContainer,
320
+ text,
321
+ });
322
+ }
323
+
324
+ // ---------------------------------------------------------------------------
325
+ // 4. TAB STRIP & TAB ITEM RECIPES
326
+ // ---------------------------------------------------------------------------
327
+
328
+ export interface TabStripRecipeOptions {
329
+ radius?: number;
330
+ }
331
+
332
+ export interface TabStripRecipeResult extends ViewStyle {
333
+ frame: ViewStyle;
334
+ track: ViewStyle;
335
+ }
336
+
337
+ /**
338
+ * Generates standard React Native styles for tab tracks and bounding frames.
339
+ */
340
+ export function tabStripRecipe(
341
+ themeInput?: ThemeInput,
342
+ options?: TabStripRecipeOptions,
343
+ ): TabStripRecipeResult {
344
+ const colors = resolveThemeColors(themeInput);
345
+ const radius = options?.radius ?? 8;
346
+
347
+ const frame: ViewStyle = {
348
+ width: "100%",
349
+ backgroundColor: colors.surface1,
350
+ borderRadius: radius,
351
+ borderColor: colors.border,
352
+ borderWidth: 1,
353
+ overflow: "hidden",
354
+ justifyContent: "center",
355
+ };
356
+
357
+ const track: ViewStyle = {
358
+ flexDirection: "row",
359
+ alignItems: "center",
360
+ width: "100%",
361
+ padding: 3,
362
+ gap: 2,
363
+ };
364
+
365
+ const subFrame = { ...frame };
366
+ return Object.assign(frame, {
367
+ frame: subFrame,
368
+ track,
369
+ });
370
+ }
371
+
372
+ export interface TabItemRecipeOptions {
373
+ active?: boolean;
374
+ pressed?: boolean;
375
+ compact?: boolean;
376
+ fit?: boolean;
377
+ }
378
+
379
+ export interface TabItemRecipeResult extends ViewStyle {
380
+ container: ViewStyle;
381
+ text: TextStyle;
382
+ badge: ViewStyle;
383
+ badgeText: TextStyle;
384
+ }
385
+
386
+ /**
387
+ * Generates standard React Native styles for individual tab buttons and inner badges.
388
+ */
389
+ export function tabItemRecipe(
390
+ themeInput?: ThemeInput,
391
+ activeOrOptions?: boolean | TabItemRecipeOptions,
392
+ options?: TabItemRecipeOptions,
393
+ ): TabItemRecipeResult {
394
+ const colors = resolveThemeColors(themeInput);
395
+ const mergedOptions: TabItemRecipeOptions =
396
+ typeof activeOrOptions === "boolean"
397
+ ? { active: activeOrOptions, ...options }
398
+ : (activeOrOptions ?? {});
399
+
400
+ const isActive = Boolean(mergedOptions.active);
401
+ const pressed = Boolean(mergedOptions.pressed);
402
+ const compact = Boolean(mergedOptions.compact);
403
+ const fit = mergedOptions.fit ?? true;
404
+
405
+ const bg = isActive
406
+ ? colors.surface2
407
+ : pressed
408
+ ? alpha(colors.surface2, 0.5)
409
+ : "transparent";
410
+
411
+ const container: ViewStyle = {
412
+ flexDirection: "row",
413
+ alignItems: "center",
414
+ justifyContent: "center",
415
+ gap: 4,
416
+ overflow: "hidden",
417
+ borderRadius: 6,
418
+ minHeight: compact ? 26 : 30,
419
+ backgroundColor: bg,
420
+ paddingHorizontal: compact ? 8 : 12,
421
+ paddingVertical: compact ? 4 : 6,
422
+ ...(fit ? { flex: 1, flexShrink: 1, minWidth: 0 } : { flexShrink: 0 }),
423
+ };
424
+
425
+ const text: TextStyle = {
426
+ color: isActive ? colors.foreground : colors.foregroundMuted,
427
+ fontSize: compact ? 11 : 12,
428
+ fontWeight: isActive ? "600" : "500",
429
+ textAlign: "center",
430
+ };
431
+
432
+ const badge: ViewStyle = {
433
+ borderRadius: 9999,
434
+ paddingHorizontal: 5,
435
+ paddingVertical: 1,
436
+ minWidth: 16,
437
+ alignItems: "center",
438
+ justifyContent: "center",
439
+ backgroundColor: isActive ? colors.accent : alpha(colors.foregroundMuted, 0.2),
440
+ };
441
+
442
+ const badgeText: TextStyle = {
443
+ fontSize: 10,
444
+ fontWeight: "700",
445
+ color: isActive ? colors.accentForeground || FALLBACK_ACCENT_FOREGROUND : colors.foregroundMuted,
446
+ };
447
+
448
+ const subContainer = { ...container };
449
+ return Object.assign(container, {
450
+ container: subContainer,
451
+ text,
452
+ badge,
453
+ badgeText,
454
+ });
455
+ }
456
+
457
+ // ---------------------------------------------------------------------------
458
+ // 5. BADGE RECIPE
459
+ // ---------------------------------------------------------------------------
460
+
461
+ export interface BadgeRecipeOptions {
462
+ variant?: StatusVariant;
463
+ styleVariant?: BadgeStyle;
464
+ size?: BadgeSize;
465
+ }
466
+
467
+ export interface BadgeRecipeResult extends ViewStyle {
468
+ container: ViewStyle;
469
+ text: TextStyle;
470
+ dot: ViewStyle;
471
+ }
472
+
473
+ /**
474
+ * Generates standard React Native styles for pill badges, indicators, and labels.
475
+ */
476
+ export function badgeRecipe(
477
+ themeInput?: ThemeInput,
478
+ variantOrOptions?: StatusVariant | BadgeRecipeOptions,
479
+ options?: BadgeRecipeOptions,
480
+ ): BadgeRecipeResult {
481
+ const colors = resolveThemeColors(themeInput);
482
+ const mergedOptions: BadgeRecipeOptions =
483
+ typeof variantOrOptions === "string"
484
+ ? { variant: variantOrOptions, ...options }
485
+ : (variantOrOptions ?? {});
486
+
487
+ const variant = mergedOptions.variant ?? "neutral";
488
+ const styleVariant = mergedOptions.styleVariant ?? "tinted";
489
+ const size = mergedOptions.size ?? "md";
490
+
491
+ const palette = getVariantPalette(variant, colors);
492
+ const solidColor = getStatusColor(variant, colors);
493
+
494
+ let bg = palette.bg;
495
+ let border = palette.border;
496
+ let textColor = palette.text;
497
+
498
+ if (styleVariant === "outline") {
499
+ bg = "transparent";
500
+ border = palette.border;
501
+ textColor = palette.text;
502
+ } else if (styleVariant === "solid") {
503
+ bg = solidColor;
504
+ border = "transparent";
505
+ textColor = getContrastColor(
506
+ solidColor,
507
+ colors.accentForeground || FALLBACK_ACCENT_FOREGROUND,
508
+ colors.foreground,
509
+ );
510
+ }
511
+
512
+ const fontSize = size === "sm" ? 10 : 11;
513
+ const lineHeight = size === "sm" ? 12 : 15;
514
+ const paddingVertical = size === "sm" ? 1 : 2;
515
+ const paddingHorizontal = size === "sm" ? 5 : 8;
516
+
517
+ const container: ViewStyle = {
518
+ flexDirection: "row",
519
+ alignItems: "center",
520
+ alignSelf: "flex-start",
521
+ backgroundColor: bg,
522
+ borderColor: border,
523
+ borderWidth: 1,
524
+ borderRadius: 9999,
525
+ paddingVertical,
526
+ paddingHorizontal,
527
+ gap: 4,
528
+ };
529
+
530
+ const text: TextStyle = {
531
+ color: textColor,
532
+ fontSize,
533
+ lineHeight,
534
+ fontWeight: "600",
535
+ };
536
+
537
+ const dot: ViewStyle = {
538
+ width: 6,
539
+ height: 6,
540
+ borderRadius: 3,
541
+ backgroundColor: textColor,
542
+ };
543
+
544
+ const subContainer = { ...container };
545
+ return Object.assign(container, {
546
+ container: subContainer,
547
+ text,
548
+ dot,
549
+ });
550
+ }
@@ -1,4 +1,5 @@
1
1
  import React, { type ComponentType } from "react";
2
+ import { View } from "react-native";
2
3
  import type { HostSurfaceProps as PluginSurfaceProps } from "./host";
3
4
  import { PluginThemeProvider } from "./theme/provider";
4
5
  import { ModalBodyScrollOwnerContext } from "./layout/ModalBody";
@@ -19,8 +20,54 @@ export interface RegisterSidebarSurfaceOptions {
19
20
  icon: string;
20
21
  Component: ComponentType<PluginSurfaceProps>;
21
22
  flair?: VisualFlair;
23
+ /**
24
+ * Upper bound (px) on the surface's content column, for readability on very
25
+ * wide viewports. `false` opts out entirely, for surfaces that are genuinely
26
+ * canvas-shaped — graph visualisers, wide tables, timeline views — where a
27
+ * centred column would fight the content rather than help it.
28
+ *
29
+ * Defaults to {@link DEFAULT_SIDEBAR_MAX_CONTENT_WIDTH} rather than being
30
+ * per-call opt-in. Ten call sites already pass a `maxContentWidth` to
31
+ * `ModalBody` and ignore it in ten others, so making the cap opt-in would
32
+ * reproduce exactly the inconsistency it is meant to remove — a guard that
33
+ * only fires where someone remembered it.
34
+ */
35
+ maxContentWidth?: number | false;
22
36
  }
23
37
 
38
+ /**
39
+ * Default ceiling for a sidebar surface's content column.
40
+ *
41
+ * A sidebar surface is a full host page, so on a wide monitor it stretches
42
+ * edge to edge and line lengths become unreadable. This is a ceiling, not a
43
+ * target: the column stays fluid below it and is centred above it, exactly as
44
+ * `ModalBody`'s own `maxContentWidth` behaves. 1280 is a common wide-viewport
45
+ * breakpoint rather than a number derived from any plugin's content.
46
+ */
47
+ export const DEFAULT_SIDEBAR_MAX_CONTENT_WIDTH = 1280;
48
+
49
+ /**
50
+ * Caps a surface's content column without dictating its width.
51
+ *
52
+ * `width: "100%"` keeps the column fluid *below* the cap, so a narrow window
53
+ * behaves exactly as it did before this existed. `alignSelf: "center"` only
54
+ * has an effect once the column is capped, and it is the same mechanism
55
+ * `ModalBody` uses — see the comment at ModalBody.tsx:173 for why auto side
56
+ * margins are preferred there and why this is safe here: outside a ScrollView
57
+ * content container there is nothing to stop the column stretching.
58
+ */
59
+ const SurfaceContentColumn = ({
60
+ maxContentWidth,
61
+ children,
62
+ }: {
63
+ maxContentWidth: number;
64
+ children: React.ReactNode;
65
+ }) => (
66
+ <View style={{ width: "100%", maxWidth: maxContentWidth, alignSelf: "center", flex: 1 }}>
67
+ {children}
68
+ </View>
69
+ );
70
+
24
71
  /**
25
72
  * Registers a sidebar icon and corresponding full-page surface in a single call,
26
73
  * automatically injecting `<PluginThemeProvider>` with custom visual flair.
@@ -44,12 +91,25 @@ export function registerSidebarSurface(
44
91
  plugin: SidebarSurfaceRegistrar,
45
92
  options: RegisterSidebarSurfaceOptions,
46
93
  ): () => void {
47
- const { id, title, icon, Component, flair } = options;
94
+ const {
95
+ id,
96
+ title,
97
+ icon,
98
+ Component,
99
+ flair,
100
+ maxContentWidth = DEFAULT_SIDEBAR_MAX_CONTENT_WIDTH,
101
+ } = options;
48
102
 
49
103
  const WrappedComponent: ComponentType<PluginSurfaceProps> = (props) => (
50
104
  <PluginThemeProvider theme={props.theme} layout={props.layout} flair={flair}>
51
105
  <ModalBodyScrollOwnerContext.Provider value="required">
52
- <Component {...props} />
106
+ {maxContentWidth === false ? (
107
+ <Component {...props} />
108
+ ) : (
109
+ <SurfaceContentColumn maxContentWidth={maxContentWidth}>
110
+ <Component {...props} />
111
+ </SurfaceContentColumn>
112
+ )}
53
113
  </ModalBodyScrollOwnerContext.Provider>
54
114
  </PluginThemeProvider>
55
115
  );
@@ -24,7 +24,8 @@ import {
24
24
  readHostThemeVariables,
25
25
  type HostFontVariables,
26
26
  } from "./host-variables";
27
- import { resolveTypography, type TypographyScale } from "./tokens";
27
+ import { defaultDarkTheme, defaultLightTheme, getDefaultTheme, resolveTypography, type TypographyScale } from "./tokens";
28
+ export { defaultDarkTheme, defaultLightTheme, getDefaultTheme };
28
29
  import type { ResponsiveLayout, StatusVariant, ThemeColors } from "../../../../shared/vendor/paseo-plugin-helper/types";
29
30
 
30
31
  export interface PluginThemeContextValue {
@@ -50,44 +51,7 @@ const defaultLayout: ResponsiveLayout = {
50
51
  platform: "web",
51
52
  };
52
53
 
53
- export const defaultDarkTheme: PluginTheme = {
54
- colors: {
55
- surface0: "#18181b",
56
- surface1: "#27272a",
57
- surface2: "#3f3f46",
58
- border: "#3f3f46",
59
- foreground: "#fafafa",
60
- foregroundMuted: "#a1a1aa",
61
- accent: "#3b82f6",
62
- accentForeground: "#ffffff",
63
- statusSuccess: "#22c55e",
64
- statusWarning: "#eab308",
65
- statusDanger: "#ef4444",
66
- },
67
- };
68
-
69
- export const defaultLightTheme: PluginTheme = {
70
- colors: {
71
- surface0: "#ffffff",
72
- surface1: "#f4f4f5",
73
- surface2: "#e4e4e7",
74
- border: "#e4e4e7",
75
- foreground: "#09090b",
76
- foregroundMuted: "#71717a",
77
- accent: "#2563eb",
78
- accentForeground: "#ffffff",
79
- statusSuccess: "#16a34a",
80
- statusWarning: "#ca8a04",
81
- statusDanger: "#dc2626",
82
- },
83
- };
84
-
85
- export function getDefaultTheme(scheme?: string): PluginTheme {
86
- if (scheme === "light") {
87
- return defaultLightTheme;
88
- }
89
- return defaultDarkTheme;
90
- }
54
+ // default themes re-exported from tokens.js
91
55
 
92
56
  export function useAppearanceScheme(): [string | undefined, (s: string | undefined) => void] {
93
57
  const [scheme, setScheme] = useState<string | undefined>(Appearance.getColorScheme?.() ?? undefined);