Implement scoped embeddable styles (#55)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Graham Neubig
2026-04-30 22:46:04 -07:00
committed by GitHub
co-authored by openhands
parent 8bfae71939
commit ea2135386b
19 changed files with 580 additions and 70 deletions
+5
View File
@@ -93,6 +93,11 @@
- Route decoupling note: `src/components/` should stay free of direct `react-router` imports. Route state now flows through `src/context/navigation-context.tsx`, the standalone app bridges router state with `src/routes/react-router-navigation-provider.tsx`, and link-like UI should use `src/components/shared/navigation-link.tsx`.
- Test helper note: `test-utils.tsx` now wraps renders with a default `NavigationProvider` (`currentPath: "/"`, `conversationId: "test-conversation-id"`). Navigation-sensitive tests can override that via `renderWithProviders(..., { navigation: { ... } })`.
- CSS isolation for embeddable/hosted use now relies on a scoped wrapper attribute: all bundled CSS is prefixed under `[data-agent-server-ui]` via `postcss-prefix-selector` in `vite.config.ts`, with selector exceptions handled by `transformAgentServerUISelector()` in `src/styles/agent-server-ui-style-scope.ts`. That transform must remap global selectors like `:root`, `html`, and `body` directly onto the scoped shell instead of emitting impossible descendants such as `[data-agent-server-ui] :root`.
- Public embedding entry points should use `AgentServerUIProviders` (scoped root on by default) or `AgentServerUIRoot` for manual control. The standalone app already renders its own scoped root in `src/root.tsx`, so `src/entry.client.tsx` must pass `withStyleRoot={false}` to avoid nesting duplicate shells. Keep `AgentServerUIRoot` and the scoping constants re-exported from `src/lib/index.ts` so library consumers can customize the host wrapper without reaching into private paths.
- Theme/customization tokens for the embedded shell are exposed as `--oh-*` CSS variables. Override them through `styleOverrides`, `style`, or host CSS targeting `[data-agent-server-ui]`; Tailwind theme tokens in `src/tailwind.css` should continue to reference those variables with `@theme inline` so host apps can restyle the UI without reworking component class names.
- Regression coverage for the CSS isolation work lives in `__tests__/agent-server-ui-providers.test.tsx`, `__tests__/agent-server-ui-style-scope.test.ts`, and the browser-level `tests/css-isolation.spec.ts` Playwright test.
- Library packaging notes:
- Public npm entrypoints now come from `src/index.ts` → `src/lib/index.ts`, with domain barrels under `src/components/{conversation,terminal,browser,files,settings,sidebar}/index.ts`.
+28
View File
@@ -67,6 +67,34 @@ Useful targeted verification for the isolated dev launcher:
npm run test -- __tests__/api/agent-server-config.test.ts __tests__/scripts/dev-safe.test.ts
```
## CSS isolation and host-app customization
The standalone app and the exported provider/root wrapper now scope all bundled CSS under a dedicated shell element with the `data-agent-server-ui` attribute. That means Tailwind utilities, HeroUI component styles, xterm styles, and local CSS only apply inside the OpenHands UI subtree instead of leaking into a host app.
### Embedding strategy
- Use `AgentServerUIProviders` in host apps. It renders a scoped style root by default.
- For direct wrapper control, use `AgentServerUIRoot`.
- The standalone app opts out of the provider wrapper because the router layout already renders the scoped root.
### Customization strategy
Theme and surface tokens are exposed as CSS custom properties on the scoped root. You can override them either through the provider/root `styleOverrides` prop or with host CSS targeting `[data-agent-server-ui]`.
```tsx
<AgentServerUIProviders
styleOverrides={{
"--oh-color-base": "#101820",
"--oh-color-content-2": "#f5f7ff",
"--oh-accent": "#8b5cf6",
}}
>
<App />
</AgentServerUIProviders>
```
If you want Tailwind layout utilities on the inner themed container, pass `contentClassName` instead of `className`, because the outer scope element is what all generated selectors key off of.
## Environment variables
You can create a `.env` file in the project directory with these variables based on `.env.sample`.
@@ -11,6 +11,8 @@ vi.mock("react-i18next", async (importOriginal) =>
import OptionService from "#/api/option-service/option-service.api";
import {
AGENT_SERVER_UI_SCOPE_SELECTOR,
AgentServerUIRoot,
AgentServerUIProviders,
DEFAULT_AGENT_SERVER_ANALYTICS,
OPENHANDS_I18N_NAMESPACE,
@@ -189,4 +191,70 @@ describe("AgentServerUIProviders", () => {
expect(getConfigSpy).toHaveBeenCalledTimes(1);
});
});
it("wraps children in a scoped, customizable style root by default", () => {
const { unmount } = render(
<AgentServerUIProviders
contentClassName="min-h-screen"
styleOverrides={{ "--oh-color-base": "#010203" }}
>
<div data-testid="styled-child">child</div>
</AgentServerUIProviders>,
);
const scopeRoot = document.querySelector<HTMLDivElement>(
AGENT_SERVER_UI_SCOPE_SELECTOR,
);
expect(scopeRoot).toBeInTheDocument();
expect(scopeRoot?.style.getPropertyValue("--oh-color-base")).toBe(
"#010203",
);
const themedContainer =
scopeRoot?.firstElementChild as HTMLDivElement | null;
expect(themedContainer).toHaveAttribute("data-theme", "dark");
expect(themedContainer).toHaveClass("dark", "min-h-screen");
expect(themedContainer).toContainElement(
screen.getByTestId("styled-child"),
);
unmount();
render(
<AgentServerUIProviders withStyleRoot={false}>
<div data-testid="unstyled-child">child</div>
</AgentServerUIProviders>,
);
expect(document.querySelector(AGENT_SERVER_UI_SCOPE_SELECTOR)).toBeNull();
});
it("exposes a standalone style root for host-controlled customization", () => {
render(
<AgentServerUIRoot
className="outer-shell"
contentClassName="inner-shell"
theme="light"
styleOverrides={{ "--oh-color-primary": "#abcdef" }}
>
<div data-testid="root-child">child</div>
</AgentServerUIRoot>,
);
const scopeRoot = document.querySelector<HTMLDivElement>(
AGENT_SERVER_UI_SCOPE_SELECTOR,
);
expect(scopeRoot).toHaveClass("outer-shell");
expect(scopeRoot?.style.getPropertyValue("--oh-color-primary")).toBe(
"#abcdef",
);
const themedContainer =
scopeRoot?.firstElementChild as HTMLDivElement | null;
expect(themedContainer).toHaveAttribute("data-theme", "light");
expect(themedContainer).toHaveClass("light", "inner-shell");
expect(themedContainer).toContainElement(screen.getByTestId("root-child"));
});
});
@@ -0,0 +1,52 @@
import { describe, expect, it } from "vitest";
import {
AGENT_SERVER_UI_SCOPE_SELECTOR,
transformAgentServerUISelector,
} from "#/styles/agent-server-ui-style-scope";
describe("transformAgentServerUISelector", () => {
it("prefixes ordinary selectors under the scoped root", () => {
expect(
transformAgentServerUISelector(
AGENT_SERVER_UI_SCOPE_SELECTOR,
".button-base",
`${AGENT_SERVER_UI_SCOPE_SELECTOR} .button-base`,
),
).toBe(`${AGENT_SERVER_UI_SCOPE_SELECTOR} .button-base`);
});
it("replaces :host selectors with the scoped root", () => {
expect(
transformAgentServerUISelector(
AGENT_SERVER_UI_SCOPE_SELECTOR,
":host",
`${AGENT_SERVER_UI_SCOPE_SELECTOR} :host`,
),
).toBe(AGENT_SERVER_UI_SCOPE_SELECTOR);
});
it.each([":root", "body", "html"])(
"maps %s selectors directly to the scoped root",
(selector) => {
expect(
transformAgentServerUISelector(
AGENT_SERVER_UI_SCOPE_SELECTOR,
selector,
`${AGENT_SERVER_UI_SCOPE_SELECTOR} ${selector}`,
),
).toBe(AGENT_SERVER_UI_SCOPE_SELECTOR);
},
);
it("does not double-prefix selectors that are already scoped", () => {
const selector = `${AGENT_SERVER_UI_SCOPE_SELECTOR} .xterm`;
expect(
transformAgentServerUISelector(
AGENT_SERVER_UI_SCOPE_SELECTOR,
selector,
`${AGENT_SERVER_UI_SCOPE_SELECTOR} ${selector}`,
),
).toBe(selector);
});
});
+5
View File
@@ -19,6 +19,11 @@ describe("library public entrypoints", () => {
expect(publicApi.Sidebar).toBeTypeOf("function");
expect(publicApi.ConversationPanel).toBeTypeOf("function");
expect(publicApi.AgentServerUIProviders).toBeTypeOf("function");
expect(publicApi.AgentServerUIRoot).toBeTypeOf("function");
expect(publicApi.AGENT_SERVER_UI_SCOPE_SELECTOR).toBe(
"[data-agent-server-ui]",
);
expect(publicApi.AGENT_SERVER_UI_DEFAULT_THEME).toBe("dark");
});
it("keeps each component-domain barrel importable", () => {
Vendored
+15
View File
@@ -25,3 +25,18 @@ interface Window {
};
};
}
declare module "postcss-prefix-selector" {
interface PrefixerOptions {
prefix: string;
transform?: (
prefix: string,
selector: string,
prefixedSelector: string,
) => string;
}
export default function prefixer(
options: PrefixerOptions,
): import("postcss").AcceptedPlugin;
}
+11
View File
@@ -85,6 +85,7 @@
"jsdom": "29.0.2",
"lint-staged": "16.4.0",
"msw": "2.13.4",
"postcss-prefix-selector": "2.1.1",
"prettier": "3.8.3",
"tailwindcss": "4.2.2",
"typescript": "6.0.3",
@@ -12043,6 +12044,16 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/postcss-prefix-selector": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/postcss-prefix-selector/-/postcss-prefix-selector-2.1.1.tgz",
"integrity": "sha512-ZBgf427Et6+XnrnJ9VXtJEKCjJwTvn2wn/qMg+wvvlRhIeFIAxdbrlZZ0CSsWYMJfcyPLBh8ogj5O1kb/Mcx3g==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"postcss": "^8.0.0"
}
},
"node_modules/postcss-selector-parser": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
+1
View File
@@ -121,6 +121,7 @@
"jsdom": "29.0.2",
"lint-staged": "16.4.0",
"msw": "2.13.4",
"postcss-prefix-selector": "2.1.1",
"prettier": "3.8.3",
"tailwindcss": "4.2.2",
"typescript": "6.0.3",
@@ -14,6 +14,10 @@ import {
setI18n,
} from "#/i18n";
import { PostHogWrapper } from "./posthog-wrapper";
import {
AgentServerUIRoot,
type AgentServerUIRootProps,
} from "./agent-server-ui-root";
export type AgentServerUIAnalyticsConfig =
| {
@@ -26,11 +30,15 @@ export const DEFAULT_AGENT_SERVER_ANALYTICS: AgentServerUIAnalyticsConfig = {
provider: "posthog",
};
export interface AgentServerUIProvidersProps {
export interface AgentServerUIProvidersProps extends Pick<
AgentServerUIRootProps,
"className" | "contentClassName" | "style" | "styleOverrides" | "theme"
> {
children: React.ReactNode;
queryClient?: QueryClient;
analytics?: AgentServerUIAnalyticsConfig;
i18n?: I18nInstance;
withStyleRoot?: boolean;
}
export function AgentServerUIProviders({
@@ -38,6 +46,12 @@ export function AgentServerUIProviders({
queryClient,
analytics,
i18n,
className,
contentClassName,
style,
styleOverrides,
theme,
withStyleRoot = true,
}: AgentServerUIProvidersProps) {
const resolvedQueryClient = React.useMemo(
() => queryClient ?? getDefaultQueryClient(),
@@ -76,10 +90,24 @@ export function AgentServerUIProviders({
children
);
const wrappedContent = withStyleRoot ? (
<AgentServerUIRoot
className={className}
contentClassName={contentClassName}
style={style}
styleOverrides={styleOverrides}
theme={theme}
>
{content}
</AgentServerUIRoot>
) : (
content
);
return (
<I18nextProvider i18n={resolvedI18n} defaultNS={OPENHANDS_I18N_NAMESPACE}>
<QueryClientProvider client={resolvedQueryClient}>
{content}
{wrappedContent}
</QueryClientProvider>
</I18nextProvider>
);
@@ -0,0 +1,53 @@
/* eslint-disable react/jsx-props-no-spreading */
import React from "react";
import { cn } from "#/utils/utils";
import {
AGENT_SERVER_UI_DEFAULT_CSS_VARIABLES,
AGENT_SERVER_UI_DEFAULT_THEME,
type AgentServerUIStyleOverrides,
type AgentServerUITheme,
} from "#/styles/agent-server-ui-style-scope";
export interface AgentServerUIRootProps extends Omit<
React.HTMLAttributes<HTMLDivElement>,
"style"
> {
children: React.ReactNode;
theme?: AgentServerUITheme;
style?: React.CSSProperties;
styleOverrides?: AgentServerUIStyleOverrides;
contentClassName?: string;
}
export function AgentServerUIRoot({
children,
theme = AGENT_SERVER_UI_DEFAULT_THEME,
className,
style,
styleOverrides,
contentClassName,
...divProps
}: AgentServerUIRootProps) {
const scopedStyle = React.useMemo(
() =>
({
...AGENT_SERVER_UI_DEFAULT_CSS_VARIABLES,
...styleOverrides,
...style,
}) as React.CSSProperties,
[style, styleOverrides],
);
return (
<div
data-agent-server-ui=""
{...divProps}
className={className}
style={scopedStyle}
>
<div className={cn(theme, contentClassName)} data-theme={theme}>
{children}
</div>
</div>
);
}
+4
View File
@@ -4,4 +4,8 @@ export {
type AgentServerUIAnalyticsConfig,
type AgentServerUIProvidersProps,
} from "./agent-server-ui-providers";
export {
AgentServerUIRoot,
type AgentServerUIRootProps,
} from "./agent-server-ui-root";
export { PostHogWrapper } from "./posthog-wrapper";
+4 -1
View File
@@ -32,7 +32,10 @@ prepareApp().then(() =>
hydrateRoot(
document,
<StrictMode>
<AgentServerUIProviders analytics={DEFAULT_AGENT_SERVER_ANALYTICS}>
<AgentServerUIProviders
analytics={DEFAULT_AGENT_SERVER_ANALYTICS}
withStyleRoot={false}
>
<HydratedRouter />
</AgentServerUIProviders>
</StrictMode>,
+23 -14
View File
@@ -1,19 +1,28 @@
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&family=Outfit:wght@100..900&display=swap');
@import url("https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&display=swap");
@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&family=Outfit:wght@100..900&display=swap");
:root {
--bg-dark: #0c0e10;
--bg-light: #292929;
--bg-input: #393939;
--bg-workspace: #1f2228;
--border: #3c3c4a;
--text-editor-base: #9099ac;
--text-editor-active: #c4cbda;
--bg-editor-sidebar: #24272e;
--bg-editor-active: #31343d;
--border-editor-sidebar: #3c3c4a;
background-color: var(--base) !important;
--bg-neutral-muted: #afb8c133;
--oh-bg-dark: #0c0e10;
--oh-bg-light: #292929;
--oh-bg-input: #393939;
--oh-bg-workspace: #1f2228;
--oh-text-editor-base: #9099ac;
--oh-text-editor-active: #c4cbda;
--oh-bg-editor-sidebar: #24272e;
--oh-bg-editor-active: #31343d;
--oh-border-editor-sidebar: #3c3c4a;
--oh-bg-neutral-muted: #afb8c133;
--bg-dark: var(--oh-bg-dark);
--bg-light: var(--oh-bg-light);
--bg-input: var(--oh-bg-input);
--bg-workspace: var(--oh-bg-workspace);
--text-editor-base: var(--oh-text-editor-base);
--text-editor-active: var(--oh-text-editor-active);
--bg-editor-sidebar: var(--oh-bg-editor-sidebar);
--bg-editor-active: var(--oh-bg-editor-active);
--border-editor-sidebar: var(--oh-border-editor-sidebar);
--bg-neutral-muted: var(--oh-bg-neutral-muted);
background-color: var(--oh-color-base) !important;
}
body {
+11
View File
@@ -6,9 +6,11 @@ export * from "../components/sidebar";
export * from "../components/terminal";
export {
AgentServerUIProviders,
AgentServerUIRoot,
DEFAULT_AGENT_SERVER_ANALYTICS,
type AgentServerUIAnalyticsConfig,
type AgentServerUIProvidersProps,
type AgentServerUIRootProps,
} from "../components/providers";
export {
createAgentServerQueryClient,
@@ -27,3 +29,12 @@ export {
translationResources,
waitForI18n,
} from "../i18n";
export {
AGENT_SERVER_UI_DEFAULT_CSS_VARIABLES,
AGENT_SERVER_UI_DEFAULT_THEME,
AGENT_SERVER_UI_SCOPE_ATTRIBUTE,
AGENT_SERVER_UI_SCOPE_SELECTOR,
type AgentServerUICssVariableName,
type AgentServerUIStyleOverrides,
type AgentServerUITheme,
} from "../styles/agent-server-ui-style-scope";
+7 -4
View File
@@ -22,6 +22,7 @@ import {
import { AgentServerConnectionForm } from "#/components/features/settings/agent-server-onboarding";
import { LoadingSpinner } from "#/components/shared/loading-spinner";
import { useConfig } from "#/hooks/query/use-config";
import { AgentServerUIRoot } from "#/components/providers";
export function Layout({ children }: { children: React.ReactNode }) {
return (
@@ -32,12 +33,14 @@ export function Layout({ children }: { children: React.ReactNode }) {
<Meta />
<Links />
</head>
<body>
{children}
<body style={{ margin: 0 }}>
<AgentServerUIRoot contentClassName="min-h-screen">
{children}
<Toaster />
<div id="modal-portal-exit" />
</AgentServerUIRoot>
<ScrollRestoration />
<Scripts />
<Toaster />
<div id="modal-portal-exit" />
</body>
</html>
);
+89
View File
@@ -0,0 +1,89 @@
export const AGENT_SERVER_UI_SCOPE_ATTRIBUTE = "data-agent-server-ui";
export const AGENT_SERVER_UI_SCOPE_SELECTOR = `[${AGENT_SERVER_UI_SCOPE_ATTRIBUTE}]`;
export const AGENT_SERVER_UI_DEFAULT_THEME = "dark" as const;
export type AgentServerUITheme = "dark" | "light" | "default";
export const AGENT_SERVER_UI_DEFAULT_CSS_VARIABLES = {
"--oh-color-primary": "#c9b974",
"--oh-color-logo": "#cfb755",
"--oh-color-base": "#0d0f11",
"--oh-color-base-secondary": "#24272e",
"--oh-color-danger": "#e76a5e",
"--oh-color-success": "#a5e75e",
"--oh-color-basic": "#9099ac",
"--oh-color-tertiary": "#454545",
"--oh-color-tertiary-light": "#b7bdc2",
"--oh-color-content": "#ecedee",
"--oh-color-content-2": "#f9fbfe",
"--oh-background": "#0d0f11",
"--oh-foreground": "#ecedee",
"--oh-surface": "#24272e",
"--oh-surface-foreground": "#ecedee",
"--oh-overlay": "#24272e",
"--oh-overlay-foreground": "#ecedee",
"--oh-muted": "#9099ac",
"--oh-scrollbar": "rgba(208, 217, 250, 0.3)",
"--oh-default": "#454545",
"--oh-default-foreground": "#ecedee",
"--oh-accent": "#c9b974",
"--oh-accent-foreground": "#0d0f11",
"--oh-success": "#a5e75e",
"--oh-success-foreground": "#0d0f11",
"--oh-warning": "#c9b974",
"--oh-warning-foreground": "#0d0f11",
"--oh-danger": "#e76a5e",
"--oh-danger-foreground": "#f9fbfe",
"--oh-segment": "#24272e",
"--oh-segment-foreground": "#ecedee",
"--oh-border-width": "1px",
"--oh-field-border-width": "1px",
"--oh-border": "#717888",
"--oh-separator": "rgba(113, 120, 136, 0.5)",
"--oh-focus": "#c9b974",
"--oh-link": "#ecedee",
"--oh-radius": "5px",
"--oh-field-radius": "5px",
"--oh-surface-shadow": "none",
"--oh-overlay-shadow": "none",
"--oh-field-shadow": "none",
"--oh-bg-dark": "#0c0e10",
"--oh-bg-light": "#292929",
"--oh-bg-input": "#393939",
"--oh-bg-workspace": "#1f2228",
"--oh-text-editor-base": "#9099ac",
"--oh-text-editor-active": "#c4cbda",
"--oh-bg-editor-sidebar": "#24272e",
"--oh-bg-editor-active": "#31343d",
"--oh-border-editor-sidebar": "#3c3c4a",
"--oh-bg-neutral-muted": "#afb8c133",
} as const;
export type AgentServerUICssVariableName =
keyof typeof AGENT_SERVER_UI_DEFAULT_CSS_VARIABLES;
export type AgentServerUIStyleOverrides = Partial<
Record<AgentServerUICssVariableName, string>
>;
const GLOBAL_SCOPE_SELECTORS = new Set([":root", "body", "html"]);
export function transformAgentServerUISelector(
prefix: string,
selector: string,
prefixedSelector: string,
): string {
if (selector.includes(prefix)) {
return selector;
}
if (selector.includes(":host")) {
return selector.replaceAll(":host", prefix);
}
if (GLOBAL_SCOPE_SELECTORS.has(selector.trim())) {
return prefix;
}
return prefixedSelector;
}
+117 -49
View File
@@ -8,53 +8,95 @@
preferredstrategy: "pseudoelements";
}
@theme {
--color-primary: #c9b974;
--color-logo: #cfb755;
--color-base: #0d0f11;
--color-base-secondary: #24272e;
--color-danger: #e76a5e;
--color-success: #a5e75e;
--color-basic: #9099ac;
--color-tertiary: #454545;
--color-tertiary-light: #b7bdc2;
--color-content: #ecedee;
--color-content-2: #f9fbfe;
@theme inline {
--color-primary: var(--oh-color-primary);
--color-logo: var(--oh-color-logo);
--color-base: var(--oh-color-base);
--color-base-secondary: var(--oh-color-base-secondary);
--color-danger: var(--oh-color-danger);
--color-success: var(--oh-color-success);
--color-basic: var(--oh-color-basic);
--color-tertiary: var(--oh-color-tertiary);
--color-tertiary-light: var(--oh-color-tertiary-light);
--color-content: var(--oh-color-content);
--color-content-2: var(--oh-color-content-2);
}
:root {
color-scheme: dark;
--background: #0d0f11;
--foreground: #ecedee;
--surface: #24272e;
--surface-foreground: #ecedee;
--overlay: #24272e;
--overlay-foreground: #ecedee;
--muted: #9099ac;
--scrollbar: rgba(208, 217, 250, 0.3);
--default: #454545;
--default-foreground: #ecedee;
--accent: #c9b974;
--accent-foreground: #0d0f11;
--success: #a5e75e;
--success-foreground: #0d0f11;
--warning: #c9b974;
--warning-foreground: #0d0f11;
--danger: #e76a5e;
--danger-foreground: #f9fbfe;
--segment: #24272e;
--segment-foreground: #ecedee;
--border-width: 1px;
--field-border-width: 1px;
--border: #717888;
--separator: rgba(113, 120, 136, 0.5);
--focus: #c9b974;
--link: #ecedee;
--radius: 5px;
--field-radius: 5px;
--surface-shadow: none;
--overlay-shadow: none;
--field-shadow: none;
--oh-color-primary: #c9b974;
--oh-color-logo: #cfb755;
--oh-color-base: #0d0f11;
--oh-color-base-secondary: #24272e;
--oh-color-danger: #e76a5e;
--oh-color-success: #a5e75e;
--oh-color-basic: #9099ac;
--oh-color-tertiary: #454545;
--oh-color-tertiary-light: #b7bdc2;
--oh-color-content: #ecedee;
--oh-color-content-2: #f9fbfe;
--oh-background: #0d0f11;
--oh-foreground: #ecedee;
--oh-surface: #24272e;
--oh-surface-foreground: #ecedee;
--oh-overlay: #24272e;
--oh-overlay-foreground: #ecedee;
--oh-muted: #9099ac;
--oh-scrollbar: rgba(208, 217, 250, 0.3);
--oh-default: #454545;
--oh-default-foreground: #ecedee;
--oh-accent: #c9b974;
--oh-accent-foreground: #0d0f11;
--oh-success: #a5e75e;
--oh-success-foreground: #0d0f11;
--oh-warning: #c9b974;
--oh-warning-foreground: #0d0f11;
--oh-danger: #e76a5e;
--oh-danger-foreground: #f9fbfe;
--oh-segment: #24272e;
--oh-segment-foreground: #ecedee;
--oh-border-width: 1px;
--oh-field-border-width: 1px;
--oh-border: #717888;
--oh-separator: rgba(113, 120, 136, 0.5);
--oh-focus: #c9b974;
--oh-link: #ecedee;
--oh-radius: 5px;
--oh-field-radius: 5px;
--oh-surface-shadow: none;
--oh-overlay-shadow: none;
--oh-field-shadow: none;
--background: var(--oh-background);
--foreground: var(--oh-foreground);
--surface: var(--oh-surface);
--surface-foreground: var(--oh-surface-foreground);
--overlay: var(--oh-overlay);
--overlay-foreground: var(--oh-overlay-foreground);
--muted: var(--oh-muted);
--scrollbar: var(--oh-scrollbar);
--default: var(--oh-default);
--default-foreground: var(--oh-default-foreground);
--accent: var(--oh-accent);
--accent-foreground: var(--oh-accent-foreground);
--success: var(--oh-success);
--success-foreground: var(--oh-success-foreground);
--warning: var(--oh-warning);
--warning-foreground: var(--oh-warning-foreground);
--danger: var(--oh-danger);
--danger-foreground: var(--oh-danger-foreground);
--segment: var(--oh-segment);
--segment-foreground: var(--oh-segment-foreground);
--border-width: var(--oh-border-width);
--field-border-width: var(--oh-field-border-width);
--border: var(--oh-border);
--separator: var(--oh-separator);
--focus: var(--oh-focus);
--link: var(--oh-link);
--radius: var(--oh-radius);
--field-radius: var(--oh-field-radius);
--surface-shadow: var(--oh-surface-shadow);
--overlay-shadow: var(--oh-overlay-shadow);
--field-shadow: var(--oh-field-shadow);
}
.button-base {
@@ -377,11 +419,21 @@
}
.modal-box-shadow {
box-shadow: 0 183px 51px 0 rgba(0, 0, 0, 0.00), 0 117px 47px 0 rgba(0, 0, 0, 0.01), 0 66px 40px 0 rgba(0, 0, 0, 0.03), 0 29px 29px 0 rgba(0, 0, 0, 0.04), 0 7px 16px 0 rgba(0, 0, 0, 0.05);
box-shadow:
0 183px 51px 0 rgba(0, 0, 0, 0),
0 117px 47px 0 rgba(0, 0, 0, 0.01),
0 66px 40px 0 rgba(0, 0, 0, 0.03),
0 29px 29px 0 rgba(0, 0, 0, 0.04),
0 7px 16px 0 rgba(0, 0, 0, 0.05);
}
.table-box-shadow {
box-shadow: 0 26px 7px 0 rgba(0, 0, 0, 0.00), 0 17px 7px 0 rgba(0, 0, 0, 0.01), 0 9px 6px 0 rgba(0, 0, 0, 0.03), 0 4px 4px 0 rgba(0, 0, 0, 0.04), 0 1px 2px 0 rgba(0, 0, 0, 0.05);
box-shadow:
0 26px 7px 0 rgba(0, 0, 0, 0),
0 17px 7px 0 rgba(0, 0, 0, 0.01),
0 9px 6px 0 rgba(0, 0, 0, 0.03),
0 4px 4px 0 rgba(0, 0, 0, 0.04),
0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
.git-external-link-icon {
@@ -389,7 +441,12 @@
}
.api-configuration-modal {
box-shadow: 0 183px 51px 0 rgba(0, 0, 0, 0.00), 0 117px 47px 0 rgba(0, 0, 0, 0.01), 0 66px 40px 0 rgba(0, 0, 0, 0.03), 0 29px 29px 0 rgba(0, 0, 0, 0.04), 0 7px 16px 0 rgba(0, 0, 0, 0.05);
box-shadow:
0 183px 51px 0 rgba(0, 0, 0, 0),
0 117px 47px 0 rgba(0, 0, 0, 0.01),
0 66px 40px 0 rgba(0, 0, 0, 0.03),
0 29px 29px 0 rgba(0, 0, 0, 0.04),
0 7px 16px 0 rgba(0, 0, 0, 0.05);
}
/* Shine text effect for streaming content */
@@ -407,11 +464,22 @@
-webkit-background-clip: text !important;
background-clip: text !important;
animation: shine 2s linear infinite;
background: radial-gradient(circle at center, rgb(24 24 27 / 85%), transparent) -200% 50% / 200% 100% no-repeat, #f4f4f5;
background:
radial-gradient(circle at center, rgb(24 24 27 / 85%), transparent) -200%
50% / 200% 100% no-repeat,
#f4f4f5;
}
/* CTA card gradient and shadow */
.cta-card-gradient {
background: radial-gradient(85.36% 123.38% at 50% 0%, rgba(255, 255, 255, 0.14) 0%, rgba(0, 0, 0, 0) 100%), #000000;
box-shadow: 0px 4px 6px -4px rgba(0, 0, 0, 0.1), 0px 10px 15px -3px rgba(0, 0, 0, 0.1);
background:
radial-gradient(
85.36% 123.38% at 50% 0%,
rgba(255, 255, 255, 0.14) 0%,
rgba(0, 0, 0, 0) 100%
),
#000000;
box-shadow:
0px 4px 6px -4px rgba(0, 0, 0, 0.1),
0px 10px 15px -3px rgba(0, 0, 0, 0.1);
}
+32
View File
@@ -0,0 +1,32 @@
import { expect, test } from "@playwright/test";
test("scopes standalone styles to the agent server UI shell", async ({
page,
}) => {
await page.goto("/");
await expect(page.locator("[data-agent-server-ui]")).toBeVisible();
const layout = page.getByTestId("root-layout");
await expect(layout).toBeVisible();
const insideBackground = await layout.evaluate(
(element) => getComputedStyle(element).backgroundColor,
);
const outsideStyles = await page.evaluate(() => {
const hostProbe = document.createElement("div");
hostProbe.className = "bg-base text-content-2";
hostProbe.textContent = "host";
document.body.appendChild(hostProbe);
const styles = getComputedStyle(hostProbe);
return {
backgroundColor: styles.backgroundColor,
color: styles.color,
};
});
expect(insideBackground).not.toBe("rgba(0, 0, 0, 0)");
expect(outsideStyles.backgroundColor).not.toBe(insideBackground);
});
+25
View File
@@ -7,6 +7,11 @@ import svgr from "vite-plugin-svgr";
import { reactRouter } from "@react-router/dev/vite";
import { configDefaults } from "vitest/config";
import tailwindcss from "@tailwindcss/vite";
import prefixer from "postcss-prefix-selector";
import {
AGENT_SERVER_UI_SCOPE_SELECTOR,
transformAgentServerUISelector,
} from "./src/styles/agent-server-ui-style-scope";
const LIB_ENTRY = fileURLToPath(new URL("./src/index.ts", import.meta.url));
const LIB_EXTERNALS = [
@@ -42,6 +47,26 @@ export default defineConfig(({ mode }) => {
svgr(),
tailwindcss(),
],
css: {
postcss: {
plugins: [
prefixer({
prefix: AGENT_SERVER_UI_SCOPE_SELECTOR,
transform(
prefix: string,
selector: string,
prefixedSelector: string,
) {
return transformAgentServerUISelector(
prefix,
selector,
prefixedSelector,
);
},
}),
],
},
},
build: isLibraryBuild
? {
outDir: "dist",