= {
parameters: {
// Snapshot every theme; tokens are the single theming surface, so this
// is where theme regressions show up.
- chromatic: {
- modes: {
- light: { theme: "light" },
- dark: { theme: "dark" },
- "high-contrast": { theme: "high-contrast" },
- "high-contrast-light": { theme: "high-contrast-light" },
- },
- },
+ pixel: PIXEL_ALL_THEMES,
},
};
diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts
index fb38844dba..bb84f45455 100644
--- a/packages/ui/src/index.ts
+++ b/packages/ui/src/index.ts
@@ -1 +1,29 @@
+export {
+ EmptyState,
+ type EmptyStateProps,
+} from "./components/EmptyState/EmptyState";
+export {
+ ErrorState,
+ type ErrorStateProps,
+} from "./components/ErrorState/ErrorState";
+export { Icon, type IconProps } from "./components/Icon/Icon";
+export {
+ IconButton,
+ type IconButtonProps,
+} from "./components/IconButton/IconButton";
+export {
+ ProgressBar,
+ type ProgressBarProps,
+} from "./components/ProgressBar/ProgressBar";
+export {
+ SearchInput,
+ type SearchInputProps,
+} from "./components/SearchInput/SearchInput";
+export { Spinner, type SpinnerProps } from "./components/Spinner/Spinner";
+export {
+ StatusPill,
+ type StatusPillProps,
+ type StatusPillTone,
+} from "./components/StatusPill/StatusPill";
+export type { CodiconName } from "./codicons";
export { useVscodeTheme, type VscodeThemeKind } from "./useVscodeTheme";
diff --git a/packages/ui/src/storybook.ts b/packages/ui/src/storybook.ts
new file mode 100644
index 0000000000..7625fb663f
--- /dev/null
+++ b/packages/ui/src/storybook.ts
@@ -0,0 +1,9 @@
+/**
+ * Pixel matrix override (`parameters.pixel`) that snapshots a story in every
+ * captured VS Code theme; the base matrix in pixel.jsonc is light/dark only.
+ */
+export const PIXEL_ALL_THEMES = {
+ matrix: {
+ themes: ["light", "dark", "high-contrast", "high-contrast-light"],
+ },
+} as const;
diff --git a/packages/ui/src/tokens.css b/packages/ui/src/tokens.css
index e0cae1cbb8..d2fa466503 100644
--- a/packages/ui/src/tokens.css
+++ b/packages/ui/src/tokens.css
@@ -16,36 +16,80 @@
var(--vscode-editor-background)
);
--ui-description-foreground: var(--vscode-descriptionForeground);
- --ui-icon-foreground: var(--vscode-icon-foreground);
--ui-border: var(--vscode-sideBar-border, transparent);
-
- /* Interaction. Hover/selection fills may be absent in high contrast
- themes, which convey state through contrast borders instead. */
- --ui-focus-border: var(--vscode-focusBorder);
- --ui-hover-background: var(--vscode-list-hoverBackground, transparent);
- --ui-hover-foreground: var(
- --vscode-list-hoverForeground,
- var(--ui-foreground)
+ --ui-link-foreground: var(--vscode-textLink-foreground);
+ --ui-focus-border: var(
+ --vscode-focusBorder,
+ var(--vscode-contrastActiveBorder, var(--ui-foreground))
);
- --ui-active-selection-background: var(
- --vscode-list-activeSelectionBackground,
+ --ui-disabled-opacity: 0.4;
+
+ /* Controls */
+ --ui-toolbar-foreground: var(--vscode-foreground);
+ --ui-toolbar-hover-background: var(
+ --vscode-toolbar-hoverBackground,
transparent
);
- --ui-active-selection-foreground: var(
- --vscode-list-activeSelectionForeground,
+ --ui-toolbar-hover-outline: var(--vscode-toolbar-hoverOutline, transparent);
+ --ui-toolbar-active-background: var(
+ --vscode-toolbar-activeBackground,
+ var(--ui-toolbar-hover-background)
+ );
+ --ui-input-option-active-background: var(
+ --vscode-inputOption-activeBackground,
+ var(--ui-toolbar-active-background)
+ );
+ --ui-input-option-active-foreground: var(
+ --vscode-inputOption-activeForeground,
var(--ui-foreground)
);
- --ui-inactive-selection-background: var(
- --vscode-list-inactiveSelectionBackground,
- transparent
+ --ui-input-option-active-border: var(
+ --vscode-inputOption-activeBorder,
+ var(--ui-focus-border)
+ );
+ --ui-input-background: var(--vscode-input-background);
+ --ui-input-foreground: var(--vscode-input-foreground);
+ --ui-input-border: var(
+ --vscode-input-border,
+ var(--vscode-contrastBorder, transparent)
+ );
+ --ui-input-placeholder-foreground: var(--vscode-input-placeholderForeground);
+ --ui-button-background: var(--vscode-button-background, var(--ui-background));
+ --ui-button-foreground: var(--vscode-button-foreground, var(--ui-foreground));
+ --ui-button-border: var(
+ --vscode-button-border,
+ var(--vscode-contrastBorder, transparent)
+ );
+ --ui-button-hover-background: var(
+ --vscode-button-hoverBackground,
+ var(--ui-button-background)
);
- --ui-link-foreground: var(--vscode-textLink-foreground);
- /* Feedback */
+ /* Activity and feedback */
+ --ui-progress-foreground: var(--vscode-progressBar-background);
+ --ui-state-icon-foreground: var(--ui-description-foreground);
--ui-error-foreground: var(--vscode-errorForeground);
- --ui-warning-foreground: var(--vscode-editorWarning-foreground);
+
+ /* Status and badges */
+ --ui-badge-background: var(--vscode-badge-background, transparent);
+ --ui-badge-foreground: var(--vscode-badge-foreground, var(--ui-foreground));
+ --ui-status-info-accent: var(
+ --vscode-notificationsInfoIcon-foreground,
+ var(--ui-foreground)
+ );
+ --ui-status-success-accent: var(
+ --vscode-testing-iconPassed,
+ var(--ui-foreground)
+ );
+ --ui-status-warning-accent: var(
+ --vscode-editorWarning-foreground,
+ var(--ui-foreground)
+ );
+ --ui-status-danger-accent: var(
+ --vscode-errorForeground,
+ var(--ui-foreground)
+ );
/* Only defined by high contrast themes; transparent elsewhere */
--ui-contrast-border: var(--vscode-contrastBorder, transparent);
- --ui-contrast-active-border: var(--vscode-contrastActiveBorder, transparent);
}
diff --git a/packages/ui/src/useVscodeTheme.ts b/packages/ui/src/useVscodeTheme.ts
index cc0b2d7abe..ab3de253d6 100644
--- a/packages/ui/src/useVscodeTheme.ts
+++ b/packages/ui/src/useVscodeTheme.ts
@@ -6,14 +6,27 @@ export type VscodeThemeKind =
const THEME_KIND_ATTRIBUTE = "data-vscode-theme-kind";
+// One shared observer no matter how many components use the hook.
+const listeners = new Set<() => void>();
+let observer: MutationObserver | undefined;
+
function subscribe(onChange: () => void): () => void {
- const observer = new MutationObserver(onChange);
- observer.observe(document.body, {
- attributes: true,
- attributeFilter: [THEME_KIND_ATTRIBUTE],
- });
+ if (!observer) {
+ observer = new MutationObserver(() => {
+ listeners.forEach((listener) => listener());
+ });
+ observer.observe(document.body, {
+ attributes: true,
+ attributeFilter: [THEME_KIND_ATTRIBUTE],
+ });
+ }
+ listeners.add(onChange);
return (): void => {
- observer.disconnect();
+ listeners.delete(onChange);
+ if (listeners.size === 0) {
+ observer?.disconnect();
+ observer = undefined;
+ }
};
}
diff --git a/packages/ui/src/vscode-parity.stories.tsx b/packages/ui/src/vscode-parity.stories.tsx
new file mode 100644
index 0000000000..5921f8927d
--- /dev/null
+++ b/packages/ui/src/vscode-parity.stories.tsx
@@ -0,0 +1,161 @@
+import {
+ VscodeBadge,
+ VscodeButton,
+ VscodeIcon,
+ VscodeProgressBar,
+ VscodeProgressRing,
+ VscodeTextfield,
+ VscodeToolbarButton,
+} from "@vscode-elements/react-elements";
+
+import "./components/control.css";
+import { IconButton } from "./components/IconButton/IconButton";
+import { ProgressBar } from "./components/ProgressBar/ProgressBar";
+import { SearchInput } from "./components/SearchInput/SearchInput";
+import { Spinner } from "./components/Spinner/Spinner";
+import "./components/StatePanel/StatePanel.css";
+import { StatusPill } from "./components/StatusPill/StatusPill";
+import { PIXEL_ALL_THEMES } from "./storybook";
+
+import type { Meta, StoryObj } from "@storybook/react-vite";
+
+/**
+ * Renders every `@repo/ui` control next to its `@vscode-elements`
+ * counterpart under identical theme variables. Pixel snapshots this
+ * in all four themes, so any drift from VS Code's appearance shows up as
+ * a visual diff.
+ */
+const Row = ({
+ label,
+ ours,
+ reference,
+}: {
+ label: string;
+ ours: React.ReactNode;
+ reference: React.ReactNode;
+}): React.JSX.Element => (
+ <>
+
{label}
+
+ {ours}
+
+
+ {reference}
+
+ >
+);
+
+const Parity = (): React.JSX.Element => (
+
+
+
+
+ >
+ }
+ reference={
+ <>
+
+
+ >
+ }
+ />
+ undefined}
+ style={{ width: "180px" }}
+ />
+ }
+ reference={
+
+
+
+ }
+ />
+
+
+
+ }
+ reference={
Try again}
+ />
+
+ Learn more
+
+ }
+ // Styled by the captured VS Code default webview stylesheet.
+ reference={Learn more}
+ />
+
+ }
+ reference={}
+ />
+
}
+ reference={}
+ />
+
+ 42
+
+ Running
+
+ >
+ }
+ reference={
+ <>
+ 42
+ Running
+ >
+ }
+ />
+
+);
+
+const meta: Meta