@xpufx/paseo-x-comms 0.3.0

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 (152) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +149 -0
  3. package/client/attribution.ts +7 -0
  4. package/client/conversations.ts +229 -0
  5. package/client/main.tsx +885 -0
  6. package/client/peer-label.ts +98 -0
  7. package/client/peer-status.tsx +201 -0
  8. package/client/settings-prototype.tsx +395 -0
  9. package/client/tool-call.ts +115 -0
  10. package/client/vendor/paseo-plugin-helper/command-center.ts +43 -0
  11. package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +493 -0
  12. package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +250 -0
  13. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +156 -0
  14. package/client/vendor/paseo-plugin-helper/components/Button.tsx +178 -0
  15. package/client/vendor/paseo-plugin-helper/components/Card.tsx +225 -0
  16. package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +196 -0
  17. package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +277 -0
  18. package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +172 -0
  19. package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +180 -0
  20. package/client/vendor/paseo-plugin-helper/components/DataTable.tsx +200 -0
  21. package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +97 -0
  22. package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +70 -0
  23. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +73 -0
  24. package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +446 -0
  25. package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +247 -0
  26. package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +117 -0
  27. package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +53 -0
  28. package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +118 -0
  29. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +80 -0
  30. package/client/vendor/paseo-plugin-helper/components/Select.tsx +215 -0
  31. package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +80 -0
  32. package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +319 -0
  33. package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +150 -0
  34. package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +163 -0
  35. package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +157 -0
  36. package/client/vendor/paseo-plugin-helper/components/index.ts +25 -0
  37. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +224 -0
  38. package/client/vendor/paseo-plugin-helper/forge-icon.tsx +79 -0
  39. package/client/vendor/paseo-plugin-helper/host.ts +277 -0
  40. package/client/vendor/paseo-plugin-helper/icon.tsx +39 -0
  41. package/client/vendor/paseo-plugin-helper/index.ts +28 -0
  42. package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +49 -0
  43. package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +103 -0
  44. package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +65 -0
  45. package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +378 -0
  46. package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +49 -0
  47. package/client/vendor/paseo-plugin-helper/layout/Row.tsx +39 -0
  48. package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +39 -0
  49. package/client/vendor/paseo-plugin-helper/layout/index.ts +7 -0
  50. package/client/vendor/paseo-plugin-helper/panel.tsx +81 -0
  51. package/client/vendor/paseo-plugin-helper/pill.tsx +884 -0
  52. package/client/vendor/paseo-plugin-helper/query-refresh.ts +79 -0
  53. package/client/vendor/paseo-plugin-helper/query.ts +66 -0
  54. package/client/vendor/paseo-plugin-helper/settings-screen.tsx +372 -0
  55. package/client/vendor/paseo-plugin-helper/settings.ts +181 -0
  56. package/client/vendor/paseo-plugin-helper/shared-settings.ts +46 -0
  57. package/client/vendor/paseo-plugin-helper/snapshot.ts +68 -0
  58. package/client/vendor/paseo-plugin-helper/surface.tsx +80 -0
  59. package/client/vendor/paseo-plugin-helper/theme/color-utils.ts +118 -0
  60. package/client/vendor/paseo-plugin-helper/theme/flair.ts +76 -0
  61. package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +121 -0
  62. package/client/vendor/paseo-plugin-helper/theme/index.ts +7 -0
  63. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +214 -0
  64. package/client/vendor/paseo-plugin-helper/theme/responsive.ts +213 -0
  65. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +161 -0
  66. package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +57 -0
  67. package/client/vendor/paseo-plugin-helper/utils/clipboard.ts +149 -0
  68. package/client/vendor/paseo-plugin-helper/utils/haptics.ts +34 -0
  69. package/client/via-x-comms.tsx +15 -0
  70. package/client/x-comms-conversation.tsx +449 -0
  71. package/client/x-comms-panel.tsx +6 -0
  72. package/client/x-comms-pill.tsx +193 -0
  73. package/client/x-comms-timeline.tsx +176 -0
  74. package/client/x-comms-tool-call.tsx +85 -0
  75. package/docs/mesh.md +115 -0
  76. package/mcp/LICENSE +202 -0
  77. package/mcp/README.md +287 -0
  78. package/mcp/mcp-config.example.json +8 -0
  79. package/mcp/package-lock.json +1186 -0
  80. package/mcp/package.json +43 -0
  81. package/mcp/paseo-cross-daemon-comms.example.json +5 -0
  82. package/mcp/paseo-x-comms.bundled.mjs +36964 -0
  83. package/mcp/paseo-x-comms.mjs +630 -0
  84. package/mcp/test/fixtures/extensions/block/10-block.mjs +4 -0
  85. package/mcp/test/fixtures/extensions/broken-hook/10-thrower.mjs +6 -0
  86. package/mcp/test/fixtures/extensions/broken-hook/20-good.mjs +4 -0
  87. package/mcp/test/fixtures/extensions/broken-load/10-thrower.mjs +4 -0
  88. package/mcp/test/fixtures/extensions/broken-load/20-good.mjs +4 -0
  89. package/mcp/test/fixtures/extensions/custom-tool/10-tool.mjs +12 -0
  90. package/mcp/test/fixtures/extensions/tool-block/10-block.mjs +8 -0
  91. package/mcp/test/fixtures/extensions/transform/10-transform.mjs +15 -0
  92. package/mcp/test/fixtures/fake-paseo.mjs +92 -0
  93. package/mcp/test/register-ts-hooks.mjs +6 -0
  94. package/mcp/test/resolve-ts-hooks.mjs +46 -0
  95. package/package.json +58 -0
  96. package/paseo-plugin.json +7 -0
  97. package/server/conversations-snapshot.ts +249 -0
  98. package/server/handlers.ts +1081 -0
  99. package/server/injection.ts +152 -0
  100. package/server/local-send.ts +153 -0
  101. package/server/mcp-client.ts +47 -0
  102. package/server/outbox.ts +233 -0
  103. package/server/peer-channel.ts +151 -0
  104. package/server/peer-status.ts +233 -0
  105. package/server/presence.ts +204 -0
  106. package/server/registry.ts +215 -0
  107. package/server/relay-status.ts +9 -0
  108. package/server/server-status.ts +138 -0
  109. package/server/settings.ts +81 -0
  110. package/server/snapshot.ts +176 -0
  111. package/server/vendor/paseo-plugin-helper/agent.ts +85 -0
  112. package/server/vendor/paseo-plugin-helper/custom-pills.ts +344 -0
  113. package/server/vendor/paseo-plugin-helper/index.ts +18 -0
  114. package/server/vendor/paseo-plugin-helper/jsonc.ts +78 -0
  115. package/server/vendor/paseo-plugin-helper/logger.ts +210 -0
  116. package/server/vendor/paseo-plugin-helper/mcp/client.ts +349 -0
  117. package/server/vendor/paseo-plugin-helper/mcp/http-client.ts +399 -0
  118. package/server/vendor/paseo-plugin-helper/mcp/index.ts +5 -0
  119. package/server/vendor/paseo-plugin-helper/mcp/process-killer.ts +65 -0
  120. package/server/vendor/paseo-plugin-helper/mcp/ring-buffer.ts +29 -0
  121. package/server/vendor/paseo-plugin-helper/mcp/types.ts +41 -0
  122. package/server/vendor/paseo-plugin-helper/mcp-config.ts +367 -0
  123. package/server/vendor/paseo-plugin-helper/mcp-injection.ts +85 -0
  124. package/server/vendor/paseo-plugin-helper/network.ts +91 -0
  125. package/server/vendor/paseo-plugin-helper/plugins.ts +160 -0
  126. package/server/vendor/paseo-plugin-helper/process.ts +186 -0
  127. package/server/vendor/paseo-plugin-helper/redact.ts +86 -0
  128. package/server/vendor/paseo-plugin-helper/rpc-guard.ts +77 -0
  129. package/server/vendor/paseo-plugin-helper/settings.ts +97 -0
  130. package/server/vendor/paseo-plugin-helper/shared-settings.ts +243 -0
  131. package/server/vendor/paseo-plugin-helper/storage.ts +244 -0
  132. package/server/vendor/paseo-plugin-helper/system.ts +128 -0
  133. package/server/vendor/paseo-plugin-helper/task.ts +116 -0
  134. package/server/vendor/paseo-plugin-helper/version.ts +153 -0
  135. package/server/vendor/paseo-plugin-helper/workspace-beacon.ts +418 -0
  136. package/shared/conversations-snapshot.ts +39 -0
  137. package/shared/envelope.ts +92 -0
  138. package/shared/outbox.ts +21 -0
  139. package/shared/registry.ts +366 -0
  140. package/shared/vendor/paseo-plugin-helper/README.md +11 -0
  141. package/shared/vendor/paseo-plugin-helper/async.ts +35 -0
  142. package/shared/vendor/paseo-plugin-helper/custom-pills.ts +169 -0
  143. package/shared/vendor/paseo-plugin-helper/forge.ts +110 -0
  144. package/shared/vendor/paseo-plugin-helper/formatters.ts +271 -0
  145. package/shared/vendor/paseo-plugin-helper/highlight.ts +184 -0
  146. package/shared/vendor/paseo-plugin-helper/index.ts +10 -0
  147. package/shared/vendor/paseo-plugin-helper/rpc.ts +72 -0
  148. package/shared/vendor/paseo-plugin-helper/settings.ts +138 -0
  149. package/shared/vendor/paseo-plugin-helper/suite-settings.ts +17 -0
  150. package/shared/vendor/paseo-plugin-helper/suppressed.ts +31 -0
  151. package/shared/vendor/paseo-plugin-helper/types.ts +36 -0
  152. package/shared/version.ts +2 -0
@@ -0,0 +1,214 @@
1
+ import React, {
2
+ createContext,
3
+ useCallback,
4
+ useContext,
5
+ useMemo,
6
+ useState,
7
+ useEffect,
8
+ type ReactNode,
9
+ } from "react";
10
+ import { StyleSheet, View, type LayoutChangeEvent } from "react-native";
11
+ import type { PluginTheme } from "../../../../shared/vendor/paseo-plugin-helper/types";
12
+ import type { HostLayout } from "../host";
13
+ import { Appearance } from "react-native";
14
+ import { defaultFlair, resolveRadius, type VisualFlair } from "./flair";
15
+ import { alpha, getContrastColor, getStatusColor, getVariantPalette } from "./color-utils";
16
+ import {
17
+ getTouchTargetMin,
18
+ isMobilePlatform,
19
+ resolveEffectiveCompact,
20
+ resolvePadding,
21
+ } from "./responsive";
22
+ import {
23
+ mergeThemeColors,
24
+ readHostThemeVariables,
25
+ type HostFontVariables,
26
+ } from "./host-variables";
27
+ import { resolveTypography, type TypographyScale } from "./tokens";
28
+ import type { ResponsiveLayout, StatusVariant, ThemeColors } from "../../../../shared/vendor/paseo-plugin-helper/types";
29
+
30
+ export interface PluginThemeContextValue {
31
+ theme: PluginTheme;
32
+ colors: ThemeColors;
33
+ fonts: HostFontVariables;
34
+ layout: ResponsiveLayout;
35
+ flair: VisualFlair;
36
+ isCompact: boolean;
37
+ isMobile: boolean;
38
+ touchTargetMin: number;
39
+ alpha: (color: string, opacity: number) => string;
40
+ getContrastColor: (bgHex: string, light?: string, dark?: string) => string;
41
+ getStatusColor: (variant: StatusVariant) => string;
42
+ getVariantPalette: (variant: StatusVariant) => { bg: string; text: string; border: string };
43
+ resolveRadius: (size?: "xs" | "sm" | "md" | "lg" | "pill") => number;
44
+ padding: { horizontal: number; vertical: number; gap: number };
45
+ typography: TypographyScale;
46
+ }
47
+
48
+ const defaultLayout: ResponsiveLayout = {
49
+ compact: false,
50
+ platform: "web",
51
+ };
52
+
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
+ }
91
+
92
+ export function useAppearanceScheme(): [string | undefined, (s: string | undefined) => void] {
93
+ const [scheme, setScheme] = useState<string | undefined>(Appearance.getColorScheme?.() ?? undefined);
94
+ useEffect(() => {
95
+ const sub = Appearance.addChangeListener?.(({colorScheme}) => {
96
+ setScheme(colorScheme ?? undefined);
97
+ });
98
+ return () => {
99
+ // remove listener if possible
100
+ // Appearance.addChangeListener returns an object with remove method in RN
101
+ // but in web we may not have it; ignore safely
102
+ if (sub && typeof (sub as any).remove === "function") {
103
+ (sub as any).remove();
104
+ }
105
+ };
106
+ }, []);
107
+ return [scheme, setScheme];
108
+ }
109
+
110
+ const initialDefaultTheme = getDefaultTheme();
111
+ const PluginThemeContext = createContext<PluginThemeContextValue>({
112
+ theme: initialDefaultTheme,
113
+ colors: initialDefaultTheme.colors,
114
+ fonts: {},
115
+ layout: defaultLayout,
116
+ flair: defaultFlair,
117
+ isCompact: false,
118
+ isMobile: false,
119
+ touchTargetMin: 28,
120
+ alpha: (color, op) => alpha(color, op),
121
+ getContrastColor: (bg, l, d) => getContrastColor(bg, l, d),
122
+ getStatusColor: (v) => getStatusColor(v, initialDefaultTheme.colors),
123
+ getVariantPalette: (v) => getVariantPalette(v, initialDefaultTheme.colors),
124
+ resolveRadius: (s) => resolveRadius("rounded", s),
125
+ padding: resolvePadding(defaultLayout, "comfortable"),
126
+ typography: resolveTypography(defaultLayout, "comfortable"),
127
+ });
128
+
129
+ export interface PluginThemeProviderProps {
130
+ theme: PluginTheme;
131
+ layout?: HostLayout;
132
+ flair?: Partial<VisualFlair>;
133
+ children: ReactNode;
134
+ }
135
+
136
+ export function PluginThemeProvider({
137
+ theme,
138
+ layout = defaultLayout,
139
+ flair: userFlair,
140
+ children,
141
+ }: PluginThemeProviderProps) {
142
+ const hostWidth =
143
+ typeof layout.width === "number" && layout.width > 0 ? layout.width : undefined;
144
+ const needsMeasurement = hostWidth === undefined;
145
+ const [measuredWidth, setMeasuredWidth] = useState<number | undefined>(undefined);
146
+ const [appearanceScheme] = useAppearanceScheme();
147
+
148
+ const handleLayout = useCallback((event: LayoutChangeEvent) => {
149
+ const width = event.nativeEvent?.layout?.width;
150
+ if (typeof width !== "number" || width <= 0) return;
151
+ setMeasuredWidth((prev) => (prev === width ? prev : width));
152
+ }, []);
153
+
154
+ const effectiveWidth = hostWidth ?? measuredWidth;
155
+
156
+ const value = useMemo<PluginThemeContextValue>(() => {
157
+ const flair: VisualFlair = { ...defaultFlair, ...userFlair };
158
+ const hostVariables = readHostThemeVariables();
159
+ const effectiveColors = mergeThemeColors(
160
+ getDefaultTheme(appearanceScheme).colors,
161
+ hostVariables.colors,
162
+ theme.colors,
163
+ flair.accentColor,
164
+ );
165
+
166
+ const isCompact = resolveEffectiveCompact(layout, effectiveWidth);
167
+ const effectiveLayout: ResponsiveLayout =
168
+ effectiveWidth !== undefined
169
+ ? { ...layout, width: effectiveWidth, compact: isCompact }
170
+ : { ...layout, compact: isCompact };
171
+ const isMobile = isMobilePlatform(effectiveLayout.platform);
172
+ const touchTargetMin = getTouchTargetMin(effectiveLayout);
173
+ const padding = resolvePadding(effectiveLayout, flair.density);
174
+ const typography = resolveTypography(effectiveLayout, flair.density);
175
+
176
+ return {
177
+ theme,
178
+ colors: effectiveColors,
179
+ fonts: hostVariables.fonts,
180
+ layout: effectiveLayout,
181
+ flair,
182
+ isCompact,
183
+ isMobile,
184
+ touchTargetMin,
185
+ alpha: (c, o) => alpha(c, o),
186
+ getContrastColor: (bg, l, d) => getContrastColor(bg, l, d),
187
+ getStatusColor: (v) => getStatusColor(v, effectiveColors, flair.accentColor),
188
+ getVariantPalette: (v) => getVariantPalette(v, effectiveColors, flair.accentColor),
189
+ resolveRadius: (size = "md") => resolveRadius(flair.radius, size),
190
+ padding,
191
+ typography,
192
+ };
193
+ }, [theme, layout, userFlair, effectiveWidth, appearanceScheme]);
194
+
195
+ return (
196
+ <PluginThemeContext.Provider value={value}>
197
+ {needsMeasurement ? (
198
+ <View style={styles.measureContainer} onLayout={handleLayout}>
199
+ {children}
200
+ </View>
201
+ ) : (
202
+ children
203
+ )}
204
+ </PluginThemeContext.Provider>
205
+ );
206
+ }
207
+
208
+ const styles = StyleSheet.create({
209
+ measureContainer: { flex: 1 },
210
+ });
211
+
212
+ export function usePluginTheme(): PluginThemeContextValue {
213
+ return useContext(PluginThemeContext);
214
+ }
@@ -0,0 +1,213 @@
1
+ import type { DensityStyle } from "./flair";
2
+ import type { PlatformType, ResponsiveLayout } from "../../../../shared/vendor/paseo-plugin-helper/types";
3
+
4
+ /**
5
+ * Container width (px) at or below which a surface steps down to the compact
6
+ * scale. Mirrors the host's `COMPACT_FORM_FACTOR_WIDTH` so plugin typography
7
+ * in narrow popovers matches full-screen mobile surfaces.
8
+ */
9
+ export const COMPACT_FORM_FACTOR_WIDTH = 500;
10
+
11
+ /**
12
+ * Resolves compact mode from the actual container width when it is known,
13
+ * falling back to the host's viewport-derived `compact` flag otherwise.
14
+ *
15
+ * A host-declared compact surface stays compact at any width; a known width at
16
+ * or below {@link COMPACT_FORM_FACTOR_WIDTH} forces compact even when the host
17
+ * viewport is wide (e.g. a narrow header-button popover on desktop).
18
+ */
19
+ export function resolveEffectiveCompact(
20
+ layout: ResponsiveLayout,
21
+ widthOverride?: number,
22
+ ): boolean {
23
+ const width = widthOverride ?? layout.width;
24
+ if (typeof width === "number" && width > 0) {
25
+ return width <= COMPACT_FORM_FACTOR_WIDTH || Boolean(layout.compact);
26
+ }
27
+ return Boolean(layout.compact);
28
+ }
29
+
30
+ /**
31
+ * Checks if the current platform is mobile (iOS or Android).
32
+ */
33
+ export function isMobilePlatform(platform: PlatformType): boolean {
34
+ return platform === "ios" || platform === "android";
35
+ }
36
+
37
+ /**
38
+ * Interactive target floor for a compact surface on a non-mobile platform
39
+ * (e.g. a narrow desktop popover). Sits between the full-desktop floor and the
40
+ * 44pt touch target: a mouse pointer needs a little more room than a wide
41
+ * panel, but nothing like a finger-sized hit area.
42
+ */
43
+ export const COMPACT_DESKTOP_TOUCH_TARGET = 36;
44
+
45
+ /**
46
+ * Returns the recommended minimum interactive target size (in pt/px).
47
+ * Real touch platforms follow Apple HIG / Android Material (min 44pt); a
48
+ * compact desktop surface gets a modest bump over the 28pt desktop floor.
49
+ */
50
+ export function getTouchTargetMin(layout: ResponsiveLayout): number {
51
+ if (isMobilePlatform(layout.platform)) return 44;
52
+ return resolveEffectiveCompact(layout) ? COMPACT_DESKTOP_TOUCH_TARGET : 28;
53
+ }
54
+
55
+ /**
56
+ * Selects a value based on compact/mobile vs desktop screen constraints.
57
+ */
58
+ export function responsiveValue<T>(layout: ResponsiveLayout, desktopVal: T, compactVal: T): T {
59
+ return resolveEffectiveCompact(layout) ? compactVal : desktopVal;
60
+ }
61
+
62
+ /**
63
+ * Calculates adaptive padding based on compact mode and density preset.
64
+ */
65
+ export function resolvePadding(
66
+ layout: ResponsiveLayout,
67
+ density: DensityStyle,
68
+ ): { horizontal: number; vertical: number; gap: number } {
69
+ const isCompact = resolveEffectiveCompact(layout);
70
+
71
+ switch (density) {
72
+ case "compact":
73
+ return {
74
+ horizontal: isCompact ? 10 : 12,
75
+ vertical: isCompact ? 6 : 8,
76
+ gap: isCompact ? 6 : 8,
77
+ };
78
+ case "spacious":
79
+ return {
80
+ horizontal: isCompact ? 16 : 24,
81
+ vertical: isCompact ? 14 : 20,
82
+ gap: isCompact ? 12 : 16,
83
+ };
84
+ case "comfortable":
85
+ default:
86
+ return {
87
+ horizontal: isCompact ? 12 : 16,
88
+ vertical: isCompact ? 10 : 14,
89
+ gap: isCompact ? 8 : 12,
90
+ };
91
+ }
92
+ }
93
+
94
+ export interface GridColumnOptions {
95
+ /** Requested (and maximum) column count. */
96
+ columns: number;
97
+ /** Horizontal gap between cells, in px. Default: 0. */
98
+ gap?: number;
99
+ /** Measured container width, when the host reports one. */
100
+ width?: number;
101
+ /**
102
+ * Minimum width each column should keep before wrapping to fewer columns.
103
+ * Only applied when a positive container width is known; `columns` stays the
104
+ * upper bound.
105
+ */
106
+ minColumnWidth?: number;
107
+ /**
108
+ * How a compact surface treats the requested column count.
109
+ * - "never" (default): keep the requested columns; flexbox wraps as needed.
110
+ * - "compact": collapse to a single column on a compact surface.
111
+ */
112
+ collapse?: "compact" | "never";
113
+ /** Whether the current surface is compact. */
114
+ isCompact?: boolean;
115
+ }
116
+
117
+ /**
118
+ * Resolves the effective column count for a responsive grid.
119
+ *
120
+ * A `minColumnWidth` plus a known container width yields as many columns as
121
+ * fit, capped at the requested count — so a narrow container wraps down to
122
+ * fewer columns instead of the requested count being forced through.
123
+ */
124
+ export function resolveGridColumns(options: GridColumnOptions): number {
125
+ const requested = Math.max(1, Math.floor(options.columns));
126
+ if ((options.collapse ?? "never") === "compact" && options.isCompact) return 1;
127
+
128
+ const { minColumnWidth, width } = options;
129
+ const gap = options.gap ?? 0;
130
+ if (
131
+ typeof minColumnWidth === "number" &&
132
+ minColumnWidth > 0 &&
133
+ typeof width === "number" &&
134
+ width > 0
135
+ ) {
136
+ const fit = Math.floor((width + gap) / (minColumnWidth + gap));
137
+ return Math.max(1, Math.min(requested, fit));
138
+ }
139
+ return requested;
140
+ }
141
+
142
+ export interface ResponsiveSelectOptions<T> {
143
+ /**
144
+ * Default fallback value, used on desktop/wide viewports if no more specific option matches.
145
+ */
146
+ desktop?: T;
147
+ /**
148
+ * Value to use on mobile platforms ('ios' | 'android').
149
+ */
150
+ mobile?: T;
151
+ /**
152
+ * Value to use when in compact mode (layout.compact === true), such as on mobile or narrow split panes.
153
+ */
154
+ compact?: T;
155
+ /**
156
+ * Value to use when not in compact mode (layout.compact === false).
157
+ */
158
+ wide?: T;
159
+ /**
160
+ * Platform-specific overrides ('web', 'desktop', 'ios', 'android', 'macos', 'windows', 'linux').
161
+ */
162
+ platform?: Partial<Record<PlatformType, T>>;
163
+ }
164
+
165
+ /**
166
+ * Selects a value based on responsive criteria with priority:
167
+ * 1. Platform-specific override (`options.platform?.[platform]`)
168
+ * 2. Mobile platform match (`options.mobile` if mobile OS)
169
+ * 3. Compact mode match (`options.compact` if layout.compact)
170
+ * 4. Wide mode match (`options.wide` if !layout.compact)
171
+ * 5. Desktop / Default (`options.desktop`)
172
+ */
173
+ export function responsiveSelect<T>(
174
+ layout: ResponsiveLayout,
175
+ options: ResponsiveSelectOptions<T>,
176
+ ): T | undefined {
177
+ const isMobile = isMobilePlatform(layout.platform);
178
+ const isCompact = resolveEffectiveCompact(layout);
179
+
180
+ // 1. Specific platform override
181
+ if (options.platform && layout.platform in options.platform) {
182
+ const val = options.platform[layout.platform];
183
+ if (val !== undefined) return val;
184
+ }
185
+
186
+ // 2. Mobile platform match
187
+ if (isMobile && options.mobile !== undefined) {
188
+ return options.mobile;
189
+ }
190
+
191
+ // 3. Compact layout match
192
+ if (isCompact && options.compact !== undefined) {
193
+ return options.compact;
194
+ }
195
+
196
+ // 4. Wide (non-compact) layout match
197
+ if (!isCompact && options.wide !== undefined) {
198
+ return options.wide;
199
+ }
200
+
201
+ // 5. Desktop fallback
202
+ if (options.desktop !== undefined) {
203
+ return options.desktop;
204
+ }
205
+
206
+ // 6. Secondary fallbacks
207
+ if (options.wide !== undefined) return options.wide;
208
+ if (options.compact !== undefined) return options.compact;
209
+ if (options.mobile !== undefined) return options.mobile;
210
+
211
+ return undefined;
212
+ }
213
+
@@ -0,0 +1,161 @@
1
+ import type { PlatformType } from "../../../../shared/vendor/paseo-plugin-helper/types";
2
+ import type { DensityStyle } from "./flair";
3
+ import type { ResponsiveLayout } from "../../../../shared/vendor/paseo-plugin-helper/types";
4
+ import { resolveEffectiveCompact } from "./responsive";
5
+
6
+ /**
7
+ * Standard spacing scale (pt/px) shared by every helper surface.
8
+ * Density rule: compact viewports step exactly one rung down the scale.
9
+ */
10
+ export const spacing = {
11
+ xxs: 2,
12
+ xs: 4,
13
+ sm: 8,
14
+ md: 12,
15
+ lg: 16,
16
+ xl: 24,
17
+ } as const;
18
+
19
+ export type SpacingKey = keyof typeof spacing;
20
+
21
+ /**
22
+ * A gap/size value: either a named spacing token or a raw px number. Layout
23
+ * primitives accept this so callers never have to invent their own scale.
24
+ */
25
+ export type SpacingValue = SpacingKey | number;
26
+
27
+ /**
28
+ * Resolves a {@link SpacingValue} to px, falling back to the theme-derived
29
+ * value when the caller did not specify one.
30
+ */
31
+ export function resolveSpacing(value: SpacingValue | undefined, fallback: number): number {
32
+ if (value === undefined) return fallback;
33
+ return typeof value === "number" ? value : spacing[value];
34
+ }
35
+
36
+ export interface TypographyToken {
37
+ fontSize: number;
38
+ lineHeight: number;
39
+ fontWeight: "400" | "500" | "600" | "700";
40
+ }
41
+
42
+ export interface TypographyScale {
43
+ title: TypographyToken;
44
+ heading: TypographyToken;
45
+ body: TypographyToken;
46
+ bodyStrong: TypographyToken;
47
+ /**
48
+ * Value text that pairs with a {@link TypographyScale.label}: same size as
49
+ * the label, normal weight, so a value never outranks its own label.
50
+ */
51
+ bodySmall: TypographyToken;
52
+ caption: TypographyToken;
53
+ label: TypographyToken;
54
+ }
55
+
56
+ /**
57
+ * Semantic text sizes keep helper components visually coherent while still
58
+ * allowing compact panes and plugin density preferences to breathe.
59
+ */
60
+ export function resolveTypography(
61
+ layout: ResponsiveLayout,
62
+ density: DensityStyle,
63
+ ): TypographyScale {
64
+ const compact = resolveEffectiveCompact(layout);
65
+ const densityStep = density === "compact" ? -1 : density === "spacious" ? 1 : 0;
66
+ const size = (regular: number, minimum = 10) =>
67
+ Math.max(minimum, regular + (compact ? -1 : 0) + densityStep);
68
+
69
+ const label: TypographyToken = {
70
+ fontSize: size(12, 11),
71
+ lineHeight: size(16, 14),
72
+ fontWeight: "600",
73
+ };
74
+ // Derived, not a fresh literal: a paired value shares the label's metrics
75
+ // and drops to normal weight, so it can never render larger than its label.
76
+ const bodySmall: TypographyToken = {
77
+ fontSize: label.fontSize,
78
+ lineHeight: label.lineHeight,
79
+ fontWeight: "400",
80
+ };
81
+
82
+ return {
83
+ title: { fontSize: size(16, 14), lineHeight: size(22, 18), fontWeight: "600" },
84
+ heading: { fontSize: size(14, 12), lineHeight: size(20, 16), fontWeight: "600" },
85
+ body: { fontSize: size(13, 12), lineHeight: size(19, 16), fontWeight: "400" },
86
+ bodyStrong: { fontSize: size(13, 12), lineHeight: size(19, 16), fontWeight: "600" },
87
+ bodySmall,
88
+ caption: { fontSize: size(11, 10), lineHeight: size(15, 14), fontWeight: "400" },
89
+ label,
90
+ };
91
+ }
92
+
93
+ /** Fallback text color on accent fills when the host omits accentForeground. */
94
+ export const FALLBACK_ACCENT_FOREGROUND = "#ffffff";
95
+
96
+ export type ElevationLevel = "none" | "sm" | "md" | "lg";
97
+
98
+ export interface ElevationStyle {
99
+ shadowColor: string;
100
+ shadowOpacity: number;
101
+ shadowRadius: number;
102
+ shadowOffset: { width: number; height: number };
103
+ elevation: number;
104
+ }
105
+
106
+ /**
107
+ * Standard shadow/elevation presets. shadowColor lives here once so client
108
+ * components never hardcode their own.
109
+ */
110
+ export function resolveElevation(level: ElevationLevel): ElevationStyle {
111
+ switch (level) {
112
+ case "none":
113
+ return {
114
+ shadowColor: "#000",
115
+ shadowOpacity: 0,
116
+ shadowRadius: 0,
117
+ shadowOffset: { width: 0, height: 0 },
118
+ elevation: 0,
119
+ };
120
+ case "sm":
121
+ return {
122
+ shadowColor: "#000",
123
+ shadowOpacity: 0.12,
124
+ shadowRadius: 3,
125
+ shadowOffset: { width: 0, height: 1 },
126
+ elevation: 1,
127
+ };
128
+ case "lg":
129
+ return {
130
+ shadowColor: "#000",
131
+ shadowOpacity: 0.24,
132
+ shadowRadius: 12,
133
+ shadowOffset: { width: 0, height: 4 },
134
+ elevation: 4,
135
+ };
136
+ case "md":
137
+ default:
138
+ return {
139
+ shadowColor: "#000",
140
+ shadowOpacity: 0.18,
141
+ shadowRadius: 6,
142
+ shadowOffset: { width: 0, height: 2 },
143
+ elevation: 2,
144
+ };
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Native `elevation` is Android-only; iOS renders the shadow props.
150
+ * Kept as a helper so call sites read consistently.
151
+ */
152
+ export function elevationForPlatform(
153
+ level: ElevationLevel,
154
+ platform: PlatformType,
155
+ ): ElevationStyle {
156
+ const style = resolveElevation(level);
157
+ if (platform !== "android") {
158
+ return { ...style, elevation: 0 };
159
+ }
160
+ return style;
161
+ }
@@ -0,0 +1,57 @@
1
+ import { usePluginTheme } from "./provider";
2
+ import {
3
+ responsiveSelect,
4
+ type ResponsiveSelectOptions,
5
+ } from "./responsive";
6
+ import type { PlatformType } from "../../../../shared/vendor/paseo-plugin-helper/types";
7
+
8
+ export interface UseResponsiveResult {
9
+ /**
10
+ * Whether the container or viewport is in compact mode (narrow trackbar, mobile screen, or split-pane).
11
+ */
12
+ isCompact: boolean;
13
+ /**
14
+ * Whether the current OS platform is mobile ('ios' | 'android').
15
+ */
16
+ isMobile: boolean;
17
+ /**
18
+ * The platform identifier ('web' | 'desktop' | 'ios' | 'android' | 'macos' | 'windows' | 'linux').
19
+ */
20
+ platform: PlatformType;
21
+ /**
22
+ * Container width if reported by Paseo's layout.
23
+ */
24
+ width?: number;
25
+ /**
26
+ * Container height if reported by Paseo's layout.
27
+ */
28
+ height?: number;
29
+ /**
30
+ * Recommended minimum interactive touch target (44pt on mobile/compact, 28pt on desktop).
31
+ */
32
+ touchTargetMin: number;
33
+ /**
34
+ * Helper function to select a value based on the current responsive environment.
35
+ */
36
+ select: <T>(options: ResponsiveSelectOptions<T>) => T | undefined;
37
+ }
38
+
39
+ /**
40
+ * Universal hook for responsive plugin UI across composer pills, modals, panels, and surfaces.
41
+ * Automatically adapts based on Paseo's layout context (compact mode, mobile OS, touch targets).
42
+ */
43
+ export function useResponsive(): UseResponsiveResult {
44
+ const { layout, isCompact, isMobile, touchTargetMin } = usePluginTheme();
45
+
46
+ return {
47
+ isCompact,
48
+ isMobile,
49
+ platform: layout.platform,
50
+ width: layout.width,
51
+ height: layout.height,
52
+ touchTargetMin,
53
+ select: <T>(options: ResponsiveSelectOptions<T>): T | undefined => {
54
+ return responsiveSelect(layout, options);
55
+ },
56
+ };
57
+ }