mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-10-07 11:33:50 +08:00
Implement scoped embeddable styles (#55)
Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
committed by
GitHub
co-authored by
openhands
parent
8bfae71939
commit
ea2135386b
@@ -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`.
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
Generated
+11
@@ -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",
|
||||
|
||||
@@ -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,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";
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user