@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,46 @@
1
+ import type { SettingsContract } from "../../../shared/vendor/paseo-plugin-helper/settings";
2
+ import { SuiteSettingsContract, type SuiteSettings } from "../../../shared/vendor/paseo-plugin-helper/suite-settings";
3
+ import {
4
+ usePluginSettings,
5
+ type UsePluginSettingsOptions,
6
+ type UsePluginSettingsResult,
7
+ } from "./settings";
8
+
9
+ export interface UseSharedPluginSettingsOptions<TSettings>
10
+ extends UsePluginSettingsOptions<TSettings> {
11
+ /**
12
+ * Background polling interval keeping sibling plugins in sync when one of
13
+ * them writes the shared file outside this client's RPC round-trip.
14
+ * Defaults to 2000ms. Pass `false` to disable polling.
15
+ */
16
+ pollIntervalMs?: number | false;
17
+ }
18
+
19
+ /**
20
+ * Reactive hook for suite-wide settings shared across independent sibling plugins.
21
+ * Wraps usePluginSettings with sync-friendly defaults: always re-verifies on
22
+ * mount and polls in the background so an update written by Plugin A appears
23
+ * in Plugin B without a manual refresh.
24
+ */
25
+ export function useSharedPluginSettings<TSettings extends Record<string, any>>(
26
+ contract: SettingsContract<TSettings>,
27
+ options: UseSharedPluginSettingsOptions<TSettings> = {},
28
+ ): UsePluginSettingsResult<TSettings> {
29
+ const { pollIntervalMs = 2000, ...rest } = options;
30
+ return usePluginSettings(contract, {
31
+ staleTime: 0,
32
+ refetchOnMount: "always",
33
+ refetchOnWindowFocus: true,
34
+ ...rest,
35
+ refetchInterval: rest.refetchInterval ?? pollIntervalMs,
36
+ });
37
+ }
38
+
39
+ /**
40
+ * Convenience hook bound to the canonical xpufx suite settings contract.
41
+ */
42
+ export function useSuiteSettings(
43
+ options: UseSharedPluginSettingsOptions<SuiteSettings> = {},
44
+ ): UsePluginSettingsResult<SuiteSettings> {
45
+ return useSharedPluginSettings(SuiteSettingsContract, options);
46
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Generic workspace-scoped shared snapshot helpers.
3
+ *
4
+ * When several surfaces (composer pill, modal, dashboard) render different
5
+ * views of one host snapshot, they must share a single React Query cache
6
+ * identity per workspace. Field-specific query keys fragment that cache and
7
+ * let every visible pill start its own poller, so build one stable key per
8
+ * workspace scope and keep selective server-side field collection out of the
9
+ * client cache key.
10
+ *
11
+ * The no-op guards here keep settings listeners and live-label caches from
12
+ * emitting redundant updates: React Query hands out fresh object identities
13
+ * on every refetch, even when values are unchanged.
14
+ */
15
+
16
+ /** Normalized workspace scope: trimmed directory, or "" for host-wide. */
17
+ export function normalizeSnapshotScope(directory?: string | null): string {
18
+ if (typeof directory !== "string") return "";
19
+ return directory.trim();
20
+ }
21
+
22
+ /**
23
+ * Stable shared cache key for one RPC snapshot in one workspace scope.
24
+ * Scopes normalize so `undefined`, `null`, and `""` all map to the
25
+ * host-wide entry instead of three separate caches.
26
+ */
27
+ export function sharedSnapshotKey(
28
+ contractName: string,
29
+ directory?: string | null,
30
+ ): readonly [string, { directory?: string }] {
31
+ const scope = normalizeSnapshotScope(directory);
32
+ return scope ? [contractName, { directory: scope }] : [contractName, {}];
33
+ }
34
+
35
+ /**
36
+ * Shallow record equality for plain settings/snapshot objects.
37
+ * Returns true when both sides hold the same keys with Object.is-equal
38
+ * values. Nested objects compare by reference, which is enough to detect
39
+ * refetch-fresh copies of unchanged flat settings.
40
+ */
41
+ export function shallowEqualRecord(
42
+ a: Record<string, unknown> | null | undefined,
43
+ b: Record<string, unknown> | null | undefined,
44
+ ): boolean {
45
+ if (Object.is(a, b)) return true;
46
+ if (!a || !b) return false;
47
+ const aKeys = Object.keys(a);
48
+ const bKeys = Object.keys(b);
49
+ if (aKeys.length !== bKeys.length) return false;
50
+ for (const key of aKeys) {
51
+ if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
52
+ if (!Object.is(a[key], b[key])) return false;
53
+ }
54
+ return true;
55
+ }
56
+
57
+ /**
58
+ * True when listeners should be notified: the snapshot reference changed
59
+ * and its shallow contents differ. Suppresses the no-op fan-out that
60
+ * otherwise fires on every background refetch.
61
+ */
62
+ export function shouldEmitSnapshotUpdate(
63
+ prev: Record<string, unknown> | null | undefined,
64
+ next: Record<string, unknown> | null | undefined,
65
+ ): boolean {
66
+ if (Object.is(prev, next)) return false;
67
+ return !shallowEqualRecord(prev, next);
68
+ }
@@ -0,0 +1,80 @@
1
+ import React, { type ComponentType } from "react";
2
+ import type { HostSurfaceProps as PluginSurfaceProps } from "./host";
3
+ import { PluginThemeProvider } from "./theme/provider";
4
+ import { ModalBodyScrollOwnerContext } from "./layout/ModalBody";
5
+ import type { VisualFlair } from "./theme/flair";
6
+
7
+ /**
8
+ * Structural registrar interface satisfied by both Paseo v0.7 PluginContext
9
+ * and Paseo v0.8 PluginClientContext.
10
+ */
11
+ export interface SidebarSurfaceRegistrar {
12
+ addSurface(surfaceId: string, Component: ComponentType<PluginSurfaceProps>): any;
13
+ addSidebarItem(contribution: any): any;
14
+ }
15
+
16
+ export interface RegisterSidebarSurfaceOptions {
17
+ id: string;
18
+ title: string;
19
+ icon: string;
20
+ Component: ComponentType<PluginSurfaceProps>;
21
+ flair?: VisualFlair;
22
+ }
23
+
24
+ /**
25
+ * Registers a sidebar icon and corresponding full-page surface in a single call,
26
+ * automatically injecting `<PluginThemeProvider>` with custom visual flair.
27
+ * Works with both Paseo v0.7 PluginContext and Paseo v0.8 PluginClientContext.
28
+ *
29
+ * A sidebar surface is a full host page: Paseo routes to it directly and does
30
+ * NOT wrap the surface body in a host scroller (unlike `<Modal.Content>`, which
31
+ * bounds the dialog and supplies the outer scroll). Any `ModalBody` in the
32
+ * subtree would therefore pick the plain, non-scrolling branch on a
33
+ * non-compact desktop and clip overflowing content — the recurring
34
+ * "plugin page does not scroll" bug. Marking the subtree with the
35
+ * `"required"` scroll-owner context makes every `ModalBody` inside own the
36
+ * scroll on every surface, so plugins inherit working scroll with no
37
+ * per-plugin workaround.
38
+ *
39
+ * Returns an idempotent disposer that removes both the surface and the sidebar
40
+ * item, matching every other helper `add*` registration. Existing callers that
41
+ * ignore the return value are unaffected.
42
+ */
43
+ export function registerSidebarSurface(
44
+ plugin: SidebarSurfaceRegistrar,
45
+ options: RegisterSidebarSurfaceOptions,
46
+ ): () => void {
47
+ const { id, title, icon, Component, flair } = options;
48
+
49
+ const WrappedComponent: ComponentType<PluginSurfaceProps> = (props) => (
50
+ <PluginThemeProvider theme={props.theme} layout={props.layout} flair={flair}>
51
+ <ModalBodyScrollOwnerContext.Provider value="required">
52
+ <Component {...props} />
53
+ </ModalBodyScrollOwnerContext.Provider>
54
+ </PluginThemeProvider>
55
+ );
56
+
57
+ const surfaceRegistration = plugin.addSurface(id, WrappedComponent);
58
+ const itemRegistration = plugin.addSidebarItem({
59
+ id,
60
+ title,
61
+ icon,
62
+ surface: id,
63
+ });
64
+
65
+ let disposed = false;
66
+ return () => {
67
+ if (disposed) return;
68
+ disposed = true;
69
+ // Host registrations expose `remove()`; tolerate hosts that return a bare
70
+ // function or nothing so cleanup never throws.
71
+ for (const registration of [itemRegistration, surfaceRegistration]) {
72
+ if (!registration) continue;
73
+ if (typeof registration === "function") {
74
+ (registration as () => void)();
75
+ } else if (typeof registration.remove === "function") {
76
+ registration.remove();
77
+ }
78
+ }
79
+ };
80
+ }
@@ -0,0 +1,118 @@
1
+ import type { StatusVariant, ThemeColors } from "../../../../shared/vendor/paseo-plugin-helper/types";
2
+
3
+ /**
4
+ * Converts a hex color and opacity (0.0 to 1.0) into an 8-character hex or rgba string.
5
+ */
6
+ export function alpha(color: string, opacity: number): string {
7
+ if (!color) return `rgba(0, 0, 0, ${opacity})`;
8
+ const clampedOpacity = Math.max(0, Math.min(1, opacity));
9
+
10
+ if (color.startsWith("#")) {
11
+ let cleanHex = color.replace("#", "");
12
+ if (cleanHex.length === 3) {
13
+ cleanHex = cleanHex
14
+ .split("")
15
+ .map((c) => c + c)
16
+ .join("");
17
+ } else if (cleanHex.length === 4) {
18
+ cleanHex = cleanHex
19
+ .slice(0, 3)
20
+ .split("")
21
+ .map((c) => c + c)
22
+ .join("");
23
+ } else if (cleanHex.length === 8) {
24
+ cleanHex = cleanHex.slice(0, 6);
25
+ }
26
+ const alphaHex = Math.round(clampedOpacity * 255)
27
+ .toString(16)
28
+ .padStart(2, "0");
29
+ return `#${cleanHex}${alphaHex}`;
30
+ }
31
+
32
+ if (color.startsWith("rgb")) {
33
+ const parts = color.replace(/[^\d,.]/g, "").split(",");
34
+ if (parts.length >= 3) {
35
+ return `rgba(${parts[0]}, ${parts[1]}, ${parts[2]}, ${clampedOpacity})`;
36
+ }
37
+ }
38
+
39
+ return color;
40
+ }
41
+
42
+ /**
43
+ * Calculates relative luminance (WCAG 2.0 formula) from a hex color.
44
+ */
45
+ export function getLuminance(hexColor: string): number {
46
+ let hex = hexColor.replace("#", "");
47
+ if (hex.length === 3) {
48
+ hex = hex
49
+ .split("")
50
+ .map((c) => c + c)
51
+ .join("");
52
+ }
53
+ const r = parseInt(hex.slice(0, 2), 16) / 255;
54
+ const g = parseInt(hex.slice(2, 4), 16) / 255;
55
+ const b = parseInt(hex.slice(4, 6), 16) / 255;
56
+
57
+ const toLinear = (c: number) => (c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
58
+
59
+ return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
60
+ }
61
+
62
+ /**
63
+ * Returns either lightText or darkText depending on which has highest contrast against bgHex.
64
+ */
65
+ export function getContrastColor(
66
+ bgHex: string,
67
+ lightText = "#ffffff",
68
+ darkText = "#0f172a",
69
+ ): string {
70
+ try {
71
+ const lum = getLuminance(bgHex);
72
+ return lum > 0.45 ? darkText : lightText;
73
+ } catch {
74
+ return lightText;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Resolves a StatusVariant to a corresponding theme color hex string.
80
+ */
81
+ export function getStatusColor(
82
+ variant: StatusVariant,
83
+ colors: ThemeColors,
84
+ customAccent?: string,
85
+ ): string {
86
+ const accent = customAccent || colors.accent;
87
+ switch (variant) {
88
+ case "success":
89
+ return colors.statusSuccess;
90
+ case "warning":
91
+ return colors.statusWarning;
92
+ case "danger":
93
+ return colors.statusDanger;
94
+ case "accent":
95
+ return accent;
96
+ case "info":
97
+ return colors.accent;
98
+ case "neutral":
99
+ default:
100
+ return colors.foregroundMuted;
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Returns background, text, and border styling colors for a given status variant.
106
+ */
107
+ export function getVariantPalette(
108
+ variant: StatusVariant,
109
+ colors: ThemeColors,
110
+ customAccent?: string,
111
+ ): { bg: string; text: string; border: string } {
112
+ const base = getStatusColor(variant, colors, customAccent);
113
+ return {
114
+ bg: alpha(base, 0.12),
115
+ text: base,
116
+ border: alpha(base, 0.3),
117
+ };
118
+ }
@@ -0,0 +1,76 @@
1
+ export type RadiusStyle = "sharp" | "rounded" | "pill";
2
+ export type DensityStyle = "compact" | "comfortable" | "spacious";
3
+ export type SurfaceStyle = "flat" | "tinted" | "elevated";
4
+ export type HeadingTransform = "none" | "uppercase";
5
+
6
+ export interface VisualFlair {
7
+ /**
8
+ * Corner radius preset for interactive elements and containers.
9
+ * - "sharp": 2-3px (terminal / technical flair)
10
+ * - "rounded": 6-8px (default Paseo native flair)
11
+ * - "pill": 9999px (soft / playful flair)
12
+ */
13
+ radius: RadiusStyle;
14
+
15
+ /**
16
+ * Spacing and typography density.
17
+ * - "compact": tight padding and smaller fonts (the default — plugin UI is
18
+ * dense by nature and generous padding wastes vertical space)
19
+ * - "comfortable": balanced, roomier defaults (opt in per plugin)
20
+ * - "spacious": generous breathing room
21
+ */
22
+ density: DensityStyle;
23
+
24
+ /**
25
+ * Surface background styling for cards, panels, and modal boxes.
26
+ * - "flat": pure surface0 with border
27
+ * - "tinted": subtle tinted foreground / accent wash
28
+ * - "elevated": uses surface1 / surface2 hierarchy
29
+ */
30
+ surfaceStyle: SurfaceStyle;
31
+
32
+ /**
33
+ * Optional custom brand accent color (e.g. "#10b981", "#3b82f6").
34
+ * Overrides Paseo's theme.colors.accent within this plugin.
35
+ */
36
+ accentColor?: string;
37
+
38
+ /**
39
+ * Default border width for cards and bordered elements (default: 1).
40
+ */
41
+ borderWidth: number;
42
+
43
+ /**
44
+ * Text transform for section headers and meta labels.
45
+ */
46
+ headingTransform: HeadingTransform;
47
+ }
48
+
49
+ export const defaultFlair: VisualFlair = {
50
+ radius: "rounded",
51
+ // Compact by default: plugin surfaces are information-dense, and the
52
+ // comfortable/spacious presets are opt-in per plugin. See xpufx-org/paseo#213.
53
+ density: "compact",
54
+ surfaceStyle: "flat",
55
+ borderWidth: 1,
56
+ headingTransform: "none",
57
+ };
58
+
59
+ export function resolveRadius(radius: RadiusStyle, size: "xs" | "sm" | "md" | "lg" | "pill" = "md"): number {
60
+ if (size === "pill" || radius === "pill") return 9999;
61
+ if (radius === "sharp") {
62
+ switch (size) {
63
+ case "xs": return 1;
64
+ case "sm": return 2;
65
+ case "md": return 3;
66
+ case "lg": return 4;
67
+ }
68
+ }
69
+ // "rounded"
70
+ switch (size) {
71
+ case "xs": return 4;
72
+ case "sm": return 6;
73
+ case "md": return 8;
74
+ case "lg": return 12;
75
+ }
76
+ }
@@ -0,0 +1,121 @@
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
+ }
@@ -0,0 +1,7 @@
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";