@xpufx/paseo-forges 0.1.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.
- package/LICENSE +21 -0
- package/README.md +109 -0
- package/client/active-forge.ts +47 -0
- package/client/board-alert.tsx +236 -0
- package/client/foreign-link.tsx +33 -0
- package/client/hook-queue-panel.tsx +412 -0
- package/client/issues-pill.tsx +1976 -0
- package/client/label-chip.tsx +116 -0
- package/client/linkifier.tsx +280 -0
- package/client/pill-label.ts +183 -0
- package/client/vendor/paseo-plugin-helper/command-center.ts +43 -0
- package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +493 -0
- package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +250 -0
- package/client/vendor/paseo-plugin-helper/components/Badge.tsx +156 -0
- package/client/vendor/paseo-plugin-helper/components/Button.tsx +178 -0
- package/client/vendor/paseo-plugin-helper/components/Card.tsx +225 -0
- package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +196 -0
- package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +277 -0
- package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +172 -0
- package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +180 -0
- package/client/vendor/paseo-plugin-helper/components/DataTable.tsx +200 -0
- package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +97 -0
- package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +70 -0
- package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +73 -0
- package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +446 -0
- package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +247 -0
- package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +117 -0
- package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +53 -0
- package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +118 -0
- package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/components/Select.tsx +215 -0
- package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +319 -0
- package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +150 -0
- package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +163 -0
- package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +157 -0
- package/client/vendor/paseo-plugin-helper/components/index.ts +25 -0
- package/client/vendor/paseo-plugin-helper/custom-pills.tsx +224 -0
- package/client/vendor/paseo-plugin-helper/forge-icon.tsx +79 -0
- package/client/vendor/paseo-plugin-helper/host.ts +277 -0
- package/client/vendor/paseo-plugin-helper/icon.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/index.ts +28 -0
- package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +49 -0
- package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +103 -0
- package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +65 -0
- package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +378 -0
- package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +49 -0
- package/client/vendor/paseo-plugin-helper/layout/Row.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/layout/index.ts +7 -0
- package/client/vendor/paseo-plugin-helper/panel.tsx +81 -0
- package/client/vendor/paseo-plugin-helper/pill.tsx +884 -0
- package/client/vendor/paseo-plugin-helper/query-refresh.ts +79 -0
- package/client/vendor/paseo-plugin-helper/query.ts +66 -0
- package/client/vendor/paseo-plugin-helper/settings-screen.tsx +372 -0
- package/client/vendor/paseo-plugin-helper/settings.ts +181 -0
- package/client/vendor/paseo-plugin-helper/shared-settings.ts +46 -0
- package/client/vendor/paseo-plugin-helper/snapshot.ts +68 -0
- package/client/vendor/paseo-plugin-helper/surface.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/theme/color-utils.ts +118 -0
- package/client/vendor/paseo-plugin-helper/theme/flair.ts +76 -0
- package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +121 -0
- package/client/vendor/paseo-plugin-helper/theme/index.ts +7 -0
- package/client/vendor/paseo-plugin-helper/theme/provider.tsx +214 -0
- package/client/vendor/paseo-plugin-helper/theme/responsive.ts +213 -0
- package/client/vendor/paseo-plugin-helper/theme/tokens.ts +161 -0
- package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +57 -0
- package/client/vendor/paseo-plugin-helper/utils/clipboard.ts +149 -0
- package/client/vendor/paseo-plugin-helper/utils/haptics.ts +34 -0
- package/client/webhook-card.tsx +198 -0
- package/docs/specs/forge-workflow-gui.md +820 -0
- package/docs/workflow.md +339 -0
- package/examples/README.md +108 -0
- package/examples/hook-service/README.md +66 -0
- package/examples/hook-service/forge-hook.service +28 -0
- package/examples/hook-service/hook-server.mjs +265 -0
- package/examples/hook-service/hook.env.example +21 -0
- package/examples/labels/README.md +64 -0
- package/examples/labels/label-base.yaml +117 -0
- package/examples/skills/coding-agent/SKILL.md +262 -0
- package/examples/skills/coding-agent-fgjx/SKILL.md +271 -0
- package/examples/skills/orchestrator/SKILL.md +133 -0
- package/examples/skills/orchestrator-fgjx/SKILL.md +139 -0
- package/examples/tools/README.md +68 -0
- package/examples/tools/fgjx +464 -0
- package/package.json +38 -0
- package/paseo-plugin.json +4 -0
- package/server/forge-client.ts +484 -0
- package/server/forge-guard.ts +70 -0
- package/server/git-origin.ts +70 -0
- package/server/hook-queue.ts +127 -0
- package/server/issues.ts +542 -0
- package/server/settings.ts +52 -0
- package/server/vendor/paseo-plugin-helper/agent.ts +85 -0
- package/server/vendor/paseo-plugin-helper/custom-pills.ts +344 -0
- package/server/vendor/paseo-plugin-helper/index.ts +18 -0
- package/server/vendor/paseo-plugin-helper/jsonc.ts +78 -0
- package/server/vendor/paseo-plugin-helper/logger.ts +210 -0
- package/server/vendor/paseo-plugin-helper/mcp-config.ts +367 -0
- package/server/vendor/paseo-plugin-helper/mcp-injection.ts +85 -0
- package/server/vendor/paseo-plugin-helper/network.ts +91 -0
- package/server/vendor/paseo-plugin-helper/plugins.ts +160 -0
- package/server/vendor/paseo-plugin-helper/process.ts +186 -0
- package/server/vendor/paseo-plugin-helper/redact.ts +86 -0
- package/server/vendor/paseo-plugin-helper/rpc-guard.ts +77 -0
- package/server/vendor/paseo-plugin-helper/settings.ts +97 -0
- package/server/vendor/paseo-plugin-helper/shared-settings.ts +243 -0
- package/server/vendor/paseo-plugin-helper/storage.ts +244 -0
- package/server/vendor/paseo-plugin-helper/system.ts +128 -0
- package/server/vendor/paseo-plugin-helper/task.ts +116 -0
- package/server/vendor/paseo-plugin-helper/version.ts +153 -0
- package/server/vendor/paseo-plugin-helper/workspace-beacon.ts +418 -0
- package/shared/hook-queue.ts +140 -0
- package/shared/issues.ts +1750 -0
- package/shared/vendor/paseo-plugin-helper/README.md +11 -0
- package/shared/vendor/paseo-plugin-helper/async.ts +35 -0
- package/shared/vendor/paseo-plugin-helper/custom-pills.ts +169 -0
- package/shared/vendor/paseo-plugin-helper/forge.ts +110 -0
- package/shared/vendor/paseo-plugin-helper/formatters.ts +271 -0
- package/shared/vendor/paseo-plugin-helper/highlight.ts +184 -0
- package/shared/vendor/paseo-plugin-helper/index.ts +10 -0
- package/shared/vendor/paseo-plugin-helper/rpc.ts +72 -0
- package/shared/vendor/paseo-plugin-helper/settings.ts +138 -0
- package/shared/vendor/paseo-plugin-helper/suite-settings.ts +17 -0
- package/shared/vendor/paseo-plugin-helper/suppressed.ts +31 -0
- package/shared/vendor/paseo-plugin-helper/types.ts +36 -0
- package/shared/version.ts +2 -0
- package/shared/webhook.ts +223 -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
|
+
}
|