@xpufx/paseo-top 0.4.4 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/OVERVIEW.md +17 -0
  2. package/client/host-ui.tsx +184 -0
  3. package/client/multi-host-view.tsx +57 -43
  4. package/client/multi-host.ts +36 -13
  5. package/client/pill-labels.ts +94 -1
  6. package/client/pill.tsx +690 -614
  7. package/client/resources-query.ts +1 -1
  8. package/client/settings-events.ts +1 -1
  9. package/client/settings-ui.tsx +86 -9
  10. package/client/surface.tsx +308 -276
  11. package/client/telemetry-copy.ts +8 -2
  12. package/client/telemetry.tsx +372 -321
  13. package/client/turn-panel.tsx +126 -104
  14. package/client/vendor/paseo-plugin-helper/{host.ts → core/host.ts} +9 -3
  15. package/client/vendor/paseo-plugin-helper/core/index.ts +76 -0
  16. package/client/vendor/paseo-plugin-helper/core/plugin-load-count.ts +89 -0
  17. package/client/vendor/paseo-plugin-helper/{query-refresh.ts → core/query-refresh.ts} +2 -2
  18. package/client/vendor/paseo-plugin-helper/{query.ts → core/query.ts} +2 -2
  19. package/client/vendor/paseo-plugin-helper/{settings.ts → core/settings.ts} +1 -1
  20. package/client/vendor/paseo-plugin-helper/{shared-settings.ts → core/shared-settings.ts} +2 -2
  21. package/client/vendor/paseo-plugin-helper/{utils → lifecycle}/clipboard.ts +2 -2
  22. package/client/vendor/paseo-plugin-helper/lifecycle/flair.ts +26 -0
  23. package/client/vendor/paseo-plugin-helper/{theme/color-utils.ts → lifecycle/host-color.ts} +95 -38
  24. package/client/vendor/paseo-plugin-helper/lifecycle/host-theme.tsx +109 -0
  25. package/client/vendor/paseo-plugin-helper/lifecycle/index.ts +23 -0
  26. package/client/vendor/paseo-plugin-helper/lifecycle/multi-host.ts +66 -0
  27. package/client/vendor/paseo-plugin-helper/{panel.tsx → lifecycle/panel.tsx} +7 -7
  28. package/client/vendor/paseo-plugin-helper/{pill.tsx → lifecycle/pill.tsx} +51 -18
  29. package/client/vendor/paseo-plugin-helper/lifecycle/providers.tsx +46 -0
  30. package/client/vendor/paseo-plugin-helper/lifecycle/scroll-owner.ts +26 -0
  31. package/client/vendor/paseo-plugin-helper/{surface.tsx → lifecycle/surface.tsx} +6 -6
  32. package/client/vendor/paseo-plugin-helper/ui/color.ts +1 -0
  33. package/client/vendor/paseo-plugin-helper/ui/content.tsx +1326 -0
  34. package/client/vendor/paseo-plugin-helper/ui/controls.tsx +1018 -0
  35. package/client/vendor/paseo-plugin-helper/{components/DataTable.tsx → ui/data.tsx} +50 -58
  36. package/client/vendor/paseo-plugin-helper/ui/index.ts +44 -0
  37. package/client/vendor/paseo-plugin-helper/ui/layout.tsx +263 -0
  38. package/client/vendor/paseo-plugin-helper/ui/modal.tsx +111 -0
  39. package/client/vendor/paseo-plugin-helper/ui/plugin-reload-banner.tsx +119 -0
  40. package/client/vendor/paseo-plugin-helper/{settings-screen.tsx → ui/settings-screen.tsx} +3 -3
  41. package/client/vendor/paseo-plugin-helper/ui/settings.ts +36 -0
  42. package/client/vendor/paseo-plugin-helper/ui/text.tsx +821 -0
  43. package/client/vendor/paseo-plugin-helper/ui/theme.tsx +1 -0
  44. package/client/vendor/permission-audit/client.tsx +706 -0
  45. package/client/vendor/permission-audit/index.tsx +1 -0
  46. package/index.client.tsx +58 -37
  47. package/index.server.ts +256 -234
  48. package/package.json +7 -4
  49. package/paseo-plugin.json +2 -0
  50. package/screenshots/1-paseo-top-modal-system.jpg +0 -0
  51. package/screenshots/2-paseo-top-modal-workspace.jpg +0 -0
  52. package/screenshots/3-paseo-top-modal-settings.jpg +0 -0
  53. package/screenshots/4-paseo-top-modal-about.jpg +0 -0
  54. package/screenshots/paseo-top-dashboard.jpg +0 -0
  55. package/screenshots/paseo-top-general-view.jpg +0 -0
  56. package/server/resources.ts +98 -0
  57. package/server/vendor/paseo-plugin-helper/custom-pills.ts +70 -0
  58. package/server/vendor/paseo-plugin-helper/index.ts +2 -0
  59. package/server/vendor/paseo-plugin-helper/logger.ts +5 -16
  60. package/server/vendor/paseo-plugin-helper/rpc-guard.ts +2 -2
  61. package/server/vendor/paseo-plugin-helper/storage-guard.ts +294 -0
  62. package/server/vendor/paseo-plugin-helper/storage.ts +36 -2
  63. package/server/vendor/paseo-plugin-helper/telemetry.ts +486 -0
  64. package/server/vendor/paseo-plugin-helper/tickets.ts +18 -67
  65. package/server/vendor/permission-audit/index.ts +1 -0
  66. package/server/vendor/permission-audit/server.ts +839 -0
  67. package/shared/helper-version.ts +1 -1
  68. package/shared/multi-host.ts +153 -159
  69. package/shared/resources.ts +145 -0
  70. package/shared/vendor/paseo-plugin-helper/async.ts +14 -7
  71. package/shared/vendor/paseo-plugin-helper/features.ts +244 -0
  72. package/shared/vendor/paseo-plugin-helper/forge.ts +27 -0
  73. package/shared/vendor/paseo-plugin-helper/index.ts +1 -0
  74. package/shared/vendor/paseo-plugin-helper/tickets.ts +122 -28
  75. package/shared/vendor/permission-audit/filter.ts +61 -0
  76. package/shared/vendor/permission-audit/index.ts +2 -0
  77. package/shared/vendor/permission-audit/shared.ts +119 -0
  78. package/shared/version.ts +1 -1
  79. package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +0 -493
  80. package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +0 -250
  81. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +0 -168
  82. package/client/vendor/paseo-plugin-helper/components/Button.tsx +0 -204
  83. package/client/vendor/paseo-plugin-helper/components/Card.tsx +0 -257
  84. package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +0 -196
  85. package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +0 -277
  86. package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +0 -172
  87. package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +0 -180
  88. package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +0 -97
  89. package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +0 -70
  90. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +0 -88
  91. package/client/vendor/paseo-plugin-helper/components/InteractiveRow.tsx +0 -142
  92. package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +0 -446
  93. package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +0 -247
  94. package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +0 -117
  95. package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +0 -53
  96. package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +0 -132
  97. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +0 -87
  98. package/client/vendor/paseo-plugin-helper/components/Select.tsx +0 -283
  99. package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +0 -80
  100. package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +0 -319
  101. package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +0 -150
  102. package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +0 -163
  103. package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +0 -157
  104. package/client/vendor/paseo-plugin-helper/components/index.ts +0 -26
  105. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +0 -225
  106. package/client/vendor/paseo-plugin-helper/forge-icon.tsx +0 -79
  107. package/client/vendor/paseo-plugin-helper/icon.tsx +0 -39
  108. package/client/vendor/paseo-plugin-helper/index.ts +0 -30
  109. package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +0 -49
  110. package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +0 -103
  111. package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +0 -65
  112. package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +0 -368
  113. package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +0 -51
  114. package/client/vendor/paseo-plugin-helper/layout/Row.tsx +0 -39
  115. package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +0 -39
  116. package/client/vendor/paseo-plugin-helper/layout/index.ts +0 -7
  117. package/client/vendor/paseo-plugin-helper/styles/index.ts +0 -1
  118. package/client/vendor/paseo-plugin-helper/styles/recipes.ts +0 -550
  119. package/client/vendor/paseo-plugin-helper/theme/flair.ts +0 -76
  120. package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +0 -121
  121. package/client/vendor/paseo-plugin-helper/theme/index.ts +0 -7
  122. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +0 -178
  123. package/client/vendor/paseo-plugin-helper/theme/responsive.ts +0 -213
  124. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +0 -200
  125. package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +0 -57
  126. package/client/vendor/paseo-plugin-helper/tickets.tsx +0 -766
  127. /package/client/vendor/paseo-plugin-helper/{command-center.ts → core/command-center.ts} +0 -0
  128. /package/client/vendor/paseo-plugin-helper/{snapshot.ts → core/snapshot.ts} +0 -0
  129. /package/client/vendor/paseo-plugin-helper/{utils → lifecycle}/haptics.ts +0 -0
@@ -1,121 +0,0 @@
1
- import type { ThemeColors } from "../../../../shared/vendor/paseo-plugin-helper/types";
2
-
3
- /**
4
- * Paseo 0.8 host theme variables. On web hosts the live theme is exposed as
5
- * CSS custom properties on the document root; this map binds each variable
6
- * to the semantic ThemeColors slot it feeds.
7
- */
8
- export const PASEO_HOST_CSS_VARIABLES = {
9
- "--background": "surface0",
10
- "--foreground": "foreground",
11
- "--muted": "foregroundMuted",
12
- "--accent": "accent",
13
- "--accent-foreground": "accentForeground",
14
- "--border": "border",
15
- } as const satisfies Record<string, keyof ThemeColors>;
16
-
17
- export type PaseoHostCssVariable = keyof typeof PASEO_HOST_CSS_VARIABLES;
18
-
19
- export interface HostFontVariables {
20
- sans?: string;
21
- mono?: string;
22
- }
23
-
24
- export interface HostThemeVariables {
25
- colors: Partial<ThemeColors>;
26
- fonts: HostFontVariables;
27
- }
28
-
29
- interface StyleReader {
30
- getPropertyValue(name: string): string;
31
- }
32
-
33
- interface DomRuntime {
34
- document?: {
35
- documentElement?: object;
36
- defaultView?: {
37
- getComputedStyle(target: object): StyleReader;
38
- };
39
- };
40
- getComputedStyle?: (target: object) => StyleReader;
41
- }
42
-
43
- type MutableColors = { -readonly [K in keyof ThemeColors]: ThemeColors[K] };
44
-
45
- const HOST_FONT_VARIABLES: Record<"sans" | "mono", string[]> = {
46
- sans: ["--font-sans", "--font-family"],
47
- mono: ["--font-mono", "--font-family-monospace"],
48
- };
49
-
50
- function readVariable(names: string[]): string | undefined {
51
- const runtime = globalThis as unknown as DomRuntime;
52
- const doc = runtime.document;
53
- if (!doc?.documentElement) return undefined;
54
- const getStyle =
55
- runtime.getComputedStyle ?? doc.defaultView?.getComputedStyle.bind(doc.defaultView);
56
- if (!getStyle) return undefined;
57
- try {
58
- const computed = getStyle(doc.documentElement);
59
- for (const name of names) {
60
- const value = computed
61
- .getPropertyValue(name)
62
- .trim()
63
- .replace(/^["']|["']$/g, "");
64
- if (value) return value;
65
- }
66
- } catch {
67
- // Non-DOM runtimes (native, tests) have no computed style to read.
68
- }
69
- return undefined;
70
- }
71
-
72
- /**
73
- * Reads the live Paseo 0.8 host variables. Returns empty slots outside a DOM
74
- * runtime so native callers merge to static defaults untouched.
75
- */
76
- export function readHostThemeVariables(): HostThemeVariables {
77
- const colors: Partial<MutableColors> = {};
78
- for (const [variable, slot] of Object.entries(PASEO_HOST_CSS_VARIABLES)) {
79
- const value = readVariable([variable]);
80
- if (value) {
81
- colors[slot] = value;
82
- }
83
- }
84
- const fonts: HostFontVariables = {};
85
- const sans = readVariable(HOST_FONT_VARIABLES.sans);
86
- if (sans) fonts.sans = sans;
87
- const mono = readVariable(HOST_FONT_VARIABLES.mono);
88
- if (mono) fonts.mono = mono;
89
- return { colors, fonts };
90
- }
91
-
92
- function definedValues<T extends object>(obj: T): Partial<T> {
93
- const out: Partial<T> = {};
94
- for (const [key, value] of Object.entries(obj)) {
95
- if (value !== undefined) {
96
- (out as Record<string, unknown>)[key] = value;
97
- }
98
- }
99
- return out;
100
- }
101
-
102
- /**
103
- * Pure merge for the provider: static defaults lose to live host variables,
104
- * which lose to the injected host theme, which loses to the flair accent.
105
- * Runtime `undefined` slots are skipped so a partial host theme falls back
106
- * instead of blanking a slot. Exported for tests; the provider applies it
107
- * inside useMemo.
108
- */
109
- export function mergeThemeColors(
110
- defaults: ThemeColors,
111
- hostVariables: Partial<ThemeColors>,
112
- injected: ThemeColors,
113
- accentOverride?: string,
114
- ): ThemeColors {
115
- return {
116
- ...defaults,
117
- ...definedValues(hostVariables),
118
- ...definedValues(injected),
119
- ...(accentOverride ? { accent: accentOverride } : {}),
120
- };
121
- }
@@ -1,7 +0,0 @@
1
- export * from "./flair";
2
- export * from "./color-utils";
3
- export * from "./responsive";
4
- export * from "./tokens";
5
- export * from "./host-variables";
6
- export * from "./provider";
7
- export * from "./useResponsive";
@@ -1,178 +0,0 @@
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 { defaultDarkTheme, defaultLightTheme, getDefaultTheme, resolveTypography, type TypographyScale } from "./tokens";
28
- export { defaultDarkTheme, defaultLightTheme, getDefaultTheme };
29
- import type { ResponsiveLayout, StatusVariant, ThemeColors } from "../../../../shared/vendor/paseo-plugin-helper/types";
30
-
31
- export interface PluginThemeContextValue {
32
- theme: PluginTheme;
33
- colors: ThemeColors;
34
- fonts: HostFontVariables;
35
- layout: ResponsiveLayout;
36
- flair: VisualFlair;
37
- isCompact: boolean;
38
- isMobile: boolean;
39
- touchTargetMin: number;
40
- alpha: (color: string, opacity: number) => string;
41
- getContrastColor: (bgHex: string, light?: string, dark?: string) => string;
42
- getStatusColor: (variant: StatusVariant) => string;
43
- getVariantPalette: (variant: StatusVariant) => { bg: string; text: string; border: string };
44
- resolveRadius: (size?: "xs" | "sm" | "md" | "lg" | "pill") => number;
45
- padding: { horizontal: number; vertical: number; gap: number };
46
- typography: TypographyScale;
47
- }
48
-
49
- const defaultLayout: ResponsiveLayout = {
50
- compact: false,
51
- platform: "web",
52
- };
53
-
54
- // default themes re-exported from tokens.js
55
-
56
- export function useAppearanceScheme(): [string | undefined, (s: string | undefined) => void] {
57
- const [scheme, setScheme] = useState<string | undefined>(Appearance.getColorScheme?.() ?? undefined);
58
- useEffect(() => {
59
- const sub = Appearance.addChangeListener?.(({colorScheme}) => {
60
- setScheme(colorScheme ?? undefined);
61
- });
62
- return () => {
63
- // remove listener if possible
64
- // Appearance.addChangeListener returns an object with remove method in RN
65
- // but in web we may not have it; ignore safely
66
- if (sub && typeof (sub as any).remove === "function") {
67
- (sub as any).remove();
68
- }
69
- };
70
- }, []);
71
- return [scheme, setScheme];
72
- }
73
-
74
- const initialDefaultTheme = getDefaultTheme();
75
- const PluginThemeContext = createContext<PluginThemeContextValue>({
76
- theme: initialDefaultTheme,
77
- colors: initialDefaultTheme.colors,
78
- fonts: {},
79
- layout: defaultLayout,
80
- flair: defaultFlair,
81
- isCompact: false,
82
- isMobile: false,
83
- touchTargetMin: 28,
84
- alpha: (color, op) => alpha(color, op),
85
- getContrastColor: (bg, l, d) => getContrastColor(bg, l, d),
86
- getStatusColor: (v) => getStatusColor(v, initialDefaultTheme.colors),
87
- getVariantPalette: (v) => getVariantPalette(v, initialDefaultTheme.colors),
88
- resolveRadius: (s) => resolveRadius("rounded", s),
89
- padding: resolvePadding(defaultLayout, "comfortable"),
90
- typography: resolveTypography(defaultLayout, "comfortable"),
91
- });
92
-
93
- export interface PluginThemeProviderProps {
94
- theme: PluginTheme;
95
- layout?: HostLayout;
96
- flair?: Partial<VisualFlair>;
97
- children: ReactNode;
98
- }
99
-
100
- export function PluginThemeProvider({
101
- theme,
102
- layout = defaultLayout,
103
- flair: userFlair,
104
- children,
105
- }: PluginThemeProviderProps) {
106
- const hostWidth =
107
- typeof layout.width === "number" && layout.width > 0 ? layout.width : undefined;
108
- const needsMeasurement = hostWidth === undefined;
109
- const [measuredWidth, setMeasuredWidth] = useState<number | undefined>(undefined);
110
- const [appearanceScheme] = useAppearanceScheme();
111
-
112
- const handleLayout = useCallback((event: LayoutChangeEvent) => {
113
- const width = event.nativeEvent?.layout?.width;
114
- if (typeof width !== "number" || width <= 0) return;
115
- setMeasuredWidth((prev) => (prev === width ? prev : width));
116
- }, []);
117
-
118
- const effectiveWidth = hostWidth ?? measuredWidth;
119
-
120
- const value = useMemo<PluginThemeContextValue>(() => {
121
- const flair: VisualFlair = { ...defaultFlair, ...userFlair };
122
- const hostVariables = readHostThemeVariables();
123
- const effectiveColors = mergeThemeColors(
124
- getDefaultTheme(appearanceScheme).colors,
125
- hostVariables.colors,
126
- theme.colors,
127
- flair.accentColor,
128
- );
129
-
130
- const isCompact = resolveEffectiveCompact(layout, effectiveWidth);
131
- const effectiveLayout: ResponsiveLayout =
132
- effectiveWidth !== undefined
133
- ? { ...layout, width: effectiveWidth, compact: isCompact }
134
- : { ...layout, compact: isCompact };
135
- const isMobile = isMobilePlatform(effectiveLayout.platform);
136
- const touchTargetMin = getTouchTargetMin(effectiveLayout);
137
- const padding = resolvePadding(effectiveLayout, flair.density);
138
- const typography = resolveTypography(effectiveLayout, flair.density);
139
-
140
- return {
141
- theme,
142
- colors: effectiveColors,
143
- fonts: hostVariables.fonts,
144
- layout: effectiveLayout,
145
- flair,
146
- isCompact,
147
- isMobile,
148
- touchTargetMin,
149
- alpha: (c, o) => alpha(c, o),
150
- getContrastColor: (bg, l, d) => getContrastColor(bg, l, d),
151
- getStatusColor: (v) => getStatusColor(v, effectiveColors, flair.accentColor),
152
- getVariantPalette: (v) => getVariantPalette(v, effectiveColors, flair.accentColor),
153
- resolveRadius: (size = "md") => resolveRadius(flair.radius, size),
154
- padding,
155
- typography,
156
- };
157
- }, [theme, layout, userFlair, effectiveWidth, appearanceScheme]);
158
-
159
- return (
160
- <PluginThemeContext.Provider value={value}>
161
- {needsMeasurement ? (
162
- <View style={styles.measureContainer} onLayout={handleLayout}>
163
- {children}
164
- </View>
165
- ) : (
166
- children
167
- )}
168
- </PluginThemeContext.Provider>
169
- );
170
- }
171
-
172
- const styles = StyleSheet.create({
173
- measureContainer: { flex: 1 },
174
- });
175
-
176
- export function usePluginTheme(): PluginThemeContextValue {
177
- return useContext(PluginThemeContext);
178
- }
@@ -1,213 +0,0 @@
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
-