diff --git a/next-env.d.ts b/next-env.d.ts
index 4f11a03dc6c..254b73c165d 100644
--- a/next-env.d.ts
+++ b/next-env.d.ts
@@ -1,5 +1,6 @@
///
///
+///
// NOTE: This file should not be edited
-// see https://nextjs.org/docs/basic-features/typescript for more information.
+// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
diff --git a/package.json b/package.json
index ae00dbce99e..d2ee2ea9958 100644
--- a/package.json
+++ b/package.json
@@ -114,7 +114,9 @@
"@jridgewell/trace-mapping": "^0.3.14",
"@next/bundle-analyzer": "^12.2.0",
"@replayio/replay": "^0.22.4",
+ "@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.0",
+ "@tailwindcss/typography": "^0.5.19",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^5.16.3",
"@testing-library/react": "^16",
@@ -191,7 +193,7 @@
"style-loader": "^3.3.1",
"stylelint": "^14.16.0",
"stylelint-config-prettier": "^9.0.5",
- "tailwindcss": "^3.2.4",
+ "tailwindcss": "^3.4.17",
"ts-node": "^10.9.2",
"tsconfig-paths": "^3.14.1",
"tsx": "^4.19.2",
diff --git a/packages/e2e-tests/helpers/passport.ts b/packages/e2e-tests/helpers/passport.ts
index 91e7050cd00..a32e9b6b4c4 100644
--- a/packages/e2e-tests/helpers/passport.ts
+++ b/packages/e2e-tests/helpers/passport.ts
@@ -1,13 +1,11 @@
import { Page } from "@playwright/test";
export async function showPassport(page: Page) {
- const passportTab = page.locator(".toolbar-panel-button.passport");
+ const passportTab = page.locator("button.toolbar-panel-button.passport");
await passportTab.waitFor();
- const classes = await passportTab.getAttribute("class");
- if (!classes?.includes("active")) {
- const passportButton = passportTab.locator("button");
- await passportButton.isEnabled();
- await passportButton.click();
+ const pressed = await passportTab.getAttribute("aria-pressed");
+ if (pressed !== "true") {
+ await passportTab.click();
}
}
diff --git a/packages/replay-next/components/console/ConsoleActionsRow.module.css b/packages/replay-next/components/console/ConsoleActionsRow.module.css
index adffac5cbe1..af296a177cc 100644
--- a/packages/replay-next/components/console/ConsoleActionsRow.module.css
+++ b/packages/replay-next/components/console/ConsoleActionsRow.module.css
@@ -18,7 +18,7 @@
}
.MenuToggleButton:focus,
.DeleteTerminalExpressionButton:focus {
- border-color: var(--primary-accent);
+ border-color: var(--focus-ring-input);
}
.MenuToggleButtonIcon,
diff --git a/packages/replay-next/components/console/ConsoleRoot.module.css b/packages/replay-next/components/console/ConsoleRoot.module.css
index b5339773a8c..d9e87f6827e 100644
--- a/packages/replay-next/components/console/ConsoleRoot.module.css
+++ b/packages/replay-next/components/console/ConsoleRoot.module.css
@@ -1,8 +1,8 @@
.ConsoleRoot {
flex: 1 1 auto;
overflow: hidden;
- border-bottom-left-radius: 0.5rem;
- border-bottom-right-radius: 0.5rem;
+ border-bottom-left-radius: var(--radius-lg, 0.625rem);
+ border-bottom-right-radius: var(--radius-lg, 0.625rem);
background-color: var(--body-bgcolor);
word-break: break-all;
font-size: var(--font-size-regular);
@@ -28,6 +28,7 @@
overflow: auto;
height: 100%;
border-top: 1px solid var(--theme-splitter-color);
+ background-color: var(--tab-bgcolor-alt-subtle);
}
.PanelResizeHandleContainer {
@@ -70,6 +71,7 @@
display: flex;
flex-direction: column;
border-top: 1px solid var(--theme-splitter-color);
+ background-color: var(--body-bgcolor);
}
.ConsoleSearchRow {
diff --git a/packages/replay-next/components/console/ConsoleSearch.module.css b/packages/replay-next/components/console/ConsoleSearch.module.css
index be1f25e68ae..63db234dbc2 100644
--- a/packages/replay-next/components/console/ConsoleSearch.module.css
+++ b/packages/replay-next/components/console/ConsoleSearch.module.css
@@ -8,7 +8,7 @@
overflow: hidden;
}
.ContainerFocused {
- border-color: var(--primary-accent);
+ border-color: var(--focus-ring-input);
}
.Icon {
@@ -61,7 +61,7 @@
outline: none;
}
.ResultsIconButton:focus {
- border-color: var(--primary-accent);
+ border-color: var(--focus-ring-input);
}
.ResultsIconButton:hover:not(.ResultsIconButton:disabled):not(.ResultsIconButtonActive) {
color: var(--color-default);
diff --git a/packages/replay-next/components/console/filters/EventsList.module.css b/packages/replay-next/components/console/filters/EventsList.module.css
index 9db3ddaa989..e708d38c372 100644
--- a/packages/replay-next/components/console/filters/EventsList.module.css
+++ b/packages/replay-next/components/console/filters/EventsList.module.css
@@ -25,7 +25,7 @@
font-size: var(--font-size-regular);
}
.FilterInput:focus {
- border-color: var(--primary-accent);
+ border-color: var(--focus-ring-input);
}
.FilterInput:disabled {
opacity: 0.5;
diff --git a/packages/replay-next/components/console/filters/FilterText.module.css b/packages/replay-next/components/console/filters/FilterText.module.css
index 22d52a08505..989d7a87a91 100644
--- a/packages/replay-next/components/console/filters/FilterText.module.css
+++ b/packages/replay-next/components/console/filters/FilterText.module.css
@@ -10,5 +10,5 @@
outline: none;
}
.Input:focus {
- border-color: var(--primary-accent);
+ border-color: var(--focus-ring-input);
}
diff --git a/packages/replay-next/components/elements/ElementsListItem.module.css b/packages/replay-next/components/elements/ElementsListItem.module.css
index 2c848777ecd..c084bc7e53d 100644
--- a/packages/replay-next/components/elements/ElementsListItem.module.css
+++ b/packages/replay-next/components/elements/ElementsListItem.module.css
@@ -22,7 +22,7 @@
color: var(--color-default);
}
.Node[data-selected] {
- background-color: var(--primary-accent-dimmed);
+ background-color: var(--theme-list-selection-background);
}
.Node[data-selected],
.Node[data-selected] .HtmlAttributeName,
@@ -32,7 +32,8 @@
.Node[data-selected] .HTMLTag,
.Node[data-selected] .Separator,
.Node[data-selected] .Icon {
- fill: var(--theme-selection-color);
+ color: var(--body-color);
+ fill: var(--body-color);
}
.Node[data-loading] {
opacity: 0.5;
diff --git a/packages/replay-next/components/elements/ElementsPanel.module.css b/packages/replay-next/components/elements/ElementsPanel.module.css
index be2f7d3f267..ef646fabbfa 100644
--- a/packages/replay-next/components/elements/ElementsPanel.module.css
+++ b/packages/replay-next/components/elements/ElementsPanel.module.css
@@ -36,7 +36,7 @@
border-radius: 0.25rem;
}
.SearchIconAndInput:focus-within {
- border-color: var(--primary-accent);
+ border-color: var(--focus-ring-input);
}
.AdvancedButton {
diff --git a/packages/replay-next/components/errors/SupportForm.module.css b/packages/replay-next/components/errors/SupportForm.module.css
index 6d0b8c4d33c..4aed69b898b 100644
--- a/packages/replay-next/components/errors/SupportForm.module.css
+++ b/packages/replay-next/components/errors/SupportForm.module.css
@@ -19,8 +19,8 @@
.TextArea:focus {
box-shadow: none;
outline-offset: 0;
- outline: 1px solid var(--primary-accent);
- border: 1px solid var(--primary-accent);
+ outline: 1px solid var(--focus-ring-input);
+ border: 1px solid var(--focus-ring-input);
}
.TextArea::placeholder {
color: var(--color-dimmer);
diff --git a/packages/replay-next/components/search-files/SearchFiles.module.css b/packages/replay-next/components/search-files/SearchFiles.module.css
index eeb0db89937..6b59268b608 100644
--- a/packages/replay-next/components/search-files/SearchFiles.module.css
+++ b/packages/replay-next/components/search-files/SearchFiles.module.css
@@ -34,7 +34,7 @@
border-radius: 0.5rem;
}
.InputWrapper:focus-within {
- border-color: var(--primary-accent);
+ border-color: var(--focus-ring-input);
}
.InputWrapper[data-error],
.InputWrapper[data-error]:focus-within {
@@ -86,8 +86,8 @@
}
.SelectedSearchFilterButton {
- background-color: var(--color-hit-counts-label-background-3);
- border-color: var(--primary-accent);
+ background-color: var(--theme-base-100);
+ border-color: var(--focus-ring-input);
}
.SearchFilterIcon {
diff --git a/packages/replay-next/components/sources/SourceSearch.module.css b/packages/replay-next/components/sources/SourceSearch.module.css
index 11821fbc808..c3134b72ef6 100644
--- a/packages/replay-next/components/sources/SourceSearch.module.css
+++ b/packages/replay-next/components/sources/SourceSearch.module.css
@@ -14,7 +14,7 @@
display: flex;
}
.ContainerFocused {
- border-color: var(--primary-accent);
+ border-color: var(--focus-ring-input);
}
.InputAndResults {
@@ -82,7 +82,7 @@
outline: none;
}
.ResultsIconButton:focus {
- border-color: var(--primary-accent);
+ border-color: var(--focus-ring-input);
}
.ResultsIconButton:hover:not(.ResultsIconButton:disabled):not(.ResultsIconButtonActive) {
color: var(--color-default);
diff --git a/packages/replay-next/variables.css b/packages/replay-next/variables.css
index b9b42fe1583..48dcae9a9fd 100644
--- a/packages/replay-next/variables.css
+++ b/packages/replay-next/variables.css
@@ -45,11 +45,11 @@
/* The photon animation curve */
--animation-curve: cubic-bezier(0.07, 0.95, 0, 1);
- --primary-accent: #01acfd;
- --primary-accent-hover: #0194ff;
+ --primary-accent: #f43f5e;
+ --primary-accent-hover: #e11d48;
- --secondary-accent: #d72451;
- --secondary-accent-hover: #c61351;
+ --secondary-accent: #e11d48;
+ --secondary-accent-hover: #be123c;
--secondary-accent-stroke: var(--secondary-accent);
/* used in our codebase, to be refactored */
@@ -121,6 +121,8 @@
--z-index-8--dropdown: var(--z-index-8);
/* The styles below are the same between themes */
+ /* List/tree/source selection (see --theme-selection-background in theme blocks) */
+ --theme-selection-accent: #01acfd;
--support-form-color: var(--grey-90);
--support-form-close-icon-color: #ffffff;
--support-form-modal-background-color: #ffffff;
@@ -134,18 +136,18 @@
--color-transparent: rgba(0, 0, 0, 0);
--primary-accent-foreground-text: #fff;
- --primary-accent-dimmed: #163e58;
- --primary-accent-dimmed-hover: #12374f;
+ --primary-accent-dimmed: #3f1724;
+ --primary-accent-dimmed-hover: #4c1d2a;
--primary-accent-dimmed-foreground-text: var(--body-color);
- /* Color ramp (Dark) */
- --theme-base-100: #000; /* background chrome */
- --theme-base-95: var(--grey-90); /* tab bars */
- --theme-base-90: var(--grey-70); /* editor and selected tab */
- --theme-base-85: var(--grey-60); /* textfields */
- --theme-base-80: #797d81;
- --theme-base-70: #a2a6a8;
- --theme-base-60: #c3c6c8;
+ /* Color ramp (Dark) — aligned with replay-dashboard surfaces */
+ --theme-base-100: #0a0a0a;
+ --theme-base-95: #141414;
+ --theme-base-90: #1a1a1a;
+ --theme-base-85: #262626;
+ --theme-base-80: #404040;
+ --theme-base-70: #737373;
+ --theme-base-60: #a3a3a3;
/* Tabs (Dark) */
--tab-bgcolor: var(--theme-base-100);
@@ -157,13 +159,15 @@
--tab-standard-color: rgba(255, 255, 255, 0.5);
/* Core classes (Dark) */
- --body-bgcolor: var(--theme-base-95);
+ --body-bgcolor: #0e0e0e;
--body-color: var(--color-default);
--body-sub-color: var(--grey-40);
--buttontext-color: #f4f8fa;
--checkbox-border: var(--input-border);
--checkbox: var(--theme-base-80);
- --chrome: #081120; /* bg app */
+ /* Checkmark on checked box (stroke contrasts with --color-default fill) */
+ --checkbox-checked-mark-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%230a0a0a' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'/%3E%3C/svg%3E");
+ --chrome: #0e0e0e; /* bg app */
--icon-color: var(--body-color);
--modal-bgcolor: var(--theme-base-100);
--modal-border: #222;
@@ -171,6 +175,8 @@
--tab-bgcolor-alt-subtle: var(--theme-base-95); /* info, events */
--tab-selected-bgcolor: var(--theme-base-95);
--input-border: var(--theme-base-85);
+ /* Neutral focus for text/search fields (not brand primary) */
+ --focus-ring-input: var(--theme-base-70);
--theme-border: var(--theme-base-100);
--theme-focuser-bgcolor: var(--primary-accent);
--theme-sidebar-background: var(--body-bgcolor);
@@ -553,11 +559,11 @@
/* DevTools left nav (Dark) */
--toolbarbutton-focus-bgcolor: var(--theme-base-95);
- /* Toolbar (Dark) */
- --theme-tab-toolbar-background: var(--grey-90);
- --theme-toolbar-background-alt: var(--grey-85);
- --theme-toolbar-background-hover: #232327;
- --theme-toolbar-background: #18181a;
+ /* Toolbar (Dark) — lifted surfaces + visible splits (dashboard-style) */
+ --theme-tab-toolbar-background: var(--theme-base-95);
+ --theme-toolbar-background-alt: var(--theme-base-90);
+ --theme-toolbar-background-hover: var(--theme-base-90);
+ --theme-toolbar-background: var(--theme-base-95);
--theme-toolbar-color: var(--body-color);
--theme-toolbar-hover-active: #252526;
--theme-toolbar-hover: #232327;
@@ -574,21 +580,23 @@
--theme-button-background: rgba(249, 249, 250, 0.1);
/* Accordion headers (Dark) */
- --theme-accordion-header-background: #232327;
- --theme-accordion-header-hover: #2a2a2e;
+ --theme-accordion-header-background: var(--theme-base-95);
+ --theme-accordion-header-hover: var(--theme-base-90);
- /* Selection (Dark) */
- --theme-selection-background-hover: #353b48;
- --theme-selection-background: var(--primary-accent);
+ /* Selection (Dark) — --theme-selection-accent set in :root */
+ --theme-selection-background-hover: #33bcfd;
+ --theme-selection-background: var(--theme-selection-accent);
--theme-selection-color: #fff;
--theme-selection-focus-background: var(--grey-60);
--theme-selection-focus-color: var(--grey-30);
--theme-table-selection-background-hover: var(--theme-toolbar-background);
+ /* Softer selected row (network, sub-panels) — blue, not --primary-accent-dimmed (rose) */
+ --theme-list-selection-background: rgb(1 172 253 / 0.24);
/* Border color that splits the toolbars/panels/headers. (Dark) */
- --theme-emphasized-splitter-color-hover: var(--grey-50);
- --theme-emphasized-splitter-color: var(--grey-60);
- --theme-splitter-color: black;
+ --theme-emphasized-splitter-color-hover: var(--theme-base-70);
+ --theme-emphasized-splitter-color: var(--theme-base-85);
+ --theme-splitter-color: #2e2e2e;
/* Icon colors (Dark) */
--theme-icon-checked-color: var(--blue-30);
@@ -666,13 +674,13 @@
--focus-mode-capsule-bgcolor: var(--body-bgcolor);
--focus-mode-loaded-indexed-color: var(--primary-accent);
--focus-mode-loading-color: #00567f;
- --focus-mode-popout-icon-background-color: white;
+ --focus-mode-popout-icon-background-color: #fafafa;
--focus-mode-popout-icon-color: var(--theme-base-100);
- --focus-mode-popout-text-input-background-color: var(--theme-base-100);
- --focus-mode-popout-text-input-ring: var(--theme-base-90);
- --focus-mode-popout-background-color: rgba(67, 71, 73, 0.6);
- --focus-mode-popout-background-color-no-transparency: rgba(0, 0, 0, 1);
- --focus-mode-popout-color: white;
+ --focus-mode-popout-text-input-background-color: #161616;
+ --focus-mode-popout-text-input-ring: var(--theme-base-85);
+ --focus-mode-popout-background-color: rgba(14, 14, 14, 0.72);
+ --focus-mode-popout-background-color-no-transparency: #161616;
+ --focus-mode-popout-color: #f2f2f2;
/* Miscellaneous (Dark) */
--jellyfish-bgcolor: rgba(40, 40, 40, 0.8); /* Tailwind class used in upload, sharing, team */
@@ -684,18 +692,18 @@
--color-transparent: rgba(255, 255, 255, 0);
--primary-accent-foreground-text: #fff;
- --primary-accent-dimmed: #cceeff;
- --primary-accent-dimmed-hover: #c5e9fa;
+ --primary-accent-dimmed: #ffe4e6;
+ --primary-accent-dimmed-hover: #fecdd3;
--primary-accent-dimmed-foreground-text: var(--body-color);
- /* Color ramp (Light) */
- --theme-base-100: hsl(0, 0%, 100%);
- --theme-base-95: var(--grey-10);
- --theme-base-90: var(--grey-20);
- --theme-base-85: var(--grey-30);
- --theme-base-80: hsl(0, 0%, 80%);
- --theme-base-70: hsl(0, 0%, 70%);
- --theme-base-60: hsl(0, 0%, 60%);
+ /* Color ramp (Light) — replay-dashboard neutrals */
+ --theme-base-100: #ffffff;
+ --theme-base-95: #fafafa;
+ --theme-base-90: #f5f5f5;
+ --theme-base-85: #e5e5e5;
+ --theme-base-80: #d4d4d4;
+ --theme-base-70: #a3a3a3;
+ --theme-base-60: #737373;
/* Tabs (Light) */
--tab-bgcolor: var(--theme-base-90);
@@ -713,7 +721,8 @@
--buttontext-color: #f4f8fa;
--checkbox-border: var(--input-border);
--checkbox: var(--theme-base-100);
- --chrome: var(--theme-base-95); /* bg app */
+ --checkbox-checked-mark-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'/%3E%3C/svg%3E");
+ --chrome: #fafafa; /* bg app */
--icon-color: var(--body-color);
--modal-bgcolor: rgba(255, 255, 255, 0.98);
--modal-border: none;
@@ -721,7 +730,8 @@
--tab-bgcolor-alt-subtle: var(--theme-base-100); /* info, events */
--tab-selected-bgcolor: var(--theme-base-100); /* current tab */
--input-border: var(--grey-20);
- --theme-border: #d2d5da;
+ --focus-ring-input: var(--theme-base-60);
+ --theme-border: #e5e5e5;
--theme-focuser-bgcolor: var(--primary-accent);
--theme-sidebar-background: var(--theme-base-100);
--theme-text-field-bgcolor-hover: var(--theme-base-95);
@@ -781,7 +791,7 @@
--color-brand-react: #8c65d0;
--color-contrast: var(--theme-base-100);
--color-current: var(--secondary-accent);
- --color-default: #223344;
+ --color-default: #0a0a0a;
--color-dim: rgba(135, 135, 137, 0.9);
--color-dimmer: #aaaaaa;
--color-disabled-button: var(--color-default);
@@ -1110,11 +1120,11 @@
/* DevTools left nav (Light) */
--toolbarbutton-focus-bgcolor: var(--theme-base-90);
- /* Toolbar */
- --theme-tab-toolbar-background: var(--grey-20);
- --theme-toolbar-background-alt: #f5f5f5;
- --theme-toolbar-background-hover: rgba(221, 225, 228, 0.66);
- --theme-toolbar-background: var(--grey-10);
+ /* Toolbar (Light) — softer chrome */
+ --theme-tab-toolbar-background: var(--theme-base-95);
+ --theme-toolbar-background-alt: var(--theme-base-90);
+ --theme-toolbar-background-hover: var(--theme-base-90);
+ --theme-toolbar-background: var(--theme-base-100);
--theme-toolbar-color: var(--body-color);
--theme-toolbar-hover-active: var(--grey-20);
--theme-toolbar-hover: var(--theme-base-100);
@@ -1131,21 +1141,22 @@
--theme-button-background: rgba(12, 12, 13, 0.05);
/* Accordion headers (Light) */
- --theme-accordion-header-background: var(--theme-toolbar-background);
- --theme-accordion-header-hover: var(--theme-toolbar-hover);
+ --theme-accordion-header-background: var(--theme-base-95);
+ --theme-accordion-header-hover: var(--theme-base-90);
/* Selection (Light) */
- --theme-selection-background-hover: #f0f9fe;
- --theme-selection-background: var(--primary-accent);
- --theme-selection-color: var(--theme-base-100);
+ --theme-selection-background-hover: rgb(1 172 253 / 0.22);
+ --theme-selection-background: var(--theme-selection-accent);
+ --theme-selection-color: #fff;
--theme-selection-focus-background: var(--toolbarbutton-hover-background);
--theme-selection-focus-color: var(--grey-70);
--theme-table-selection-background-hover: var(--theme-toolbar-background);
+ --theme-list-selection-background: rgb(1 172 253 / 0.14);
/* Border color that splits the toolbars/panels/headers. */
- --theme-emphasized-splitter-color-hover: var(--grey-40);
- --theme-emphasized-splitter-color: var(--grey-30);
- --theme-splitter-color: #e0e0e2;
+ --theme-emphasized-splitter-color-hover: var(--theme-base-70);
+ --theme-emphasized-splitter-color: var(--theme-base-85);
+ --theme-splitter-color: var(--theme-base-85);
/* Icon colors (Light) */
--theme-icon-checked-color: var(--blue-60);
diff --git a/packages/shared/theme/replayTheme.ts b/packages/shared/theme/replayTheme.ts
new file mode 100644
index 00000000000..1df2f2bede9
--- /dev/null
+++ b/packages/shared/theme/replayTheme.ts
@@ -0,0 +1,57 @@
+import type { Theme } from "./types";
+
+/** Same key as replay-dashboard `src/lib/theme.ts` — keeps theme in sync when embedded next to the dashboard. */
+export const REPLAY_THEME_STORAGE_KEY = "replay_theme";
+
+export const DEFAULT_THEME: Theme = "system";
+
+export function readLocalStorageTheme(): Theme | null {
+ if (typeof window === "undefined") {
+ return null;
+ }
+ const v = localStorage.getItem(REPLAY_THEME_STORAGE_KEY);
+ if (v === "light" || v === "dark" || v === "system") {
+ return v;
+ }
+ return null;
+}
+
+export function storeTheme(theme: Theme) {
+ if (typeof window === "undefined") {
+ return;
+ }
+ localStorage.setItem(REPLAY_THEME_STORAGE_KEY, theme);
+}
+
+export function getEffectiveTheme(theme: Theme): "dark" | "light" {
+ if (typeof window === "undefined") {
+ return "dark";
+ }
+ if (theme === "system") {
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
+ }
+ if (theme === "light" || theme === "dark") {
+ return theme;
+ }
+ return "dark";
+}
+
+export function applyThemeToDOM(effectiveTheme: "dark" | "light") {
+ if (typeof document === "undefined") {
+ return;
+ }
+ const root = document.documentElement;
+ root.setAttribute("data-theme", effectiveTheme);
+ // Never assign className — Next/React may set classes on (hydration, fonts, etc.).
+ root.classList.remove("theme-light", "theme-dark", "dark");
+ root.classList.add(effectiveTheme === "dark" ? "theme-dark" : "theme-light");
+ if (effectiveTheme === "dark") {
+ root.classList.add("dark");
+ }
+}
+
+/**
+ * Inline in _document to reduce theme flash before React + UserData run.
+ * Reads `replay_theme`, then `Replay:UserPreferences.global_theme`, then system.
+ */
+export const themeInitScript = `(function(){try{var k='${REPLAY_THEME_STORAGE_KEY}';var u='Replay:UserPreferences';var t=localStorage.getItem(k);if(t!=='light'&&t!=='dark'&&t!=='system'){try{var raw=localStorage.getItem(u);if(raw){var p=JSON.parse(raw);if(p.global_theme==='light'||p.global_theme==='dark'||p.global_theme==='system')t=p.global_theme;}}catch(e){}}if(t!=='light'&&t!=='dark'&&t!=='system')t='system';var eff=t;if(t==='system')eff=window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light';var r=document.documentElement;r.setAttribute('data-theme',eff);r.classList.remove('theme-light','theme-dark','dark');r.classList.add('theme-'+eff);if(eff==='dark')r.classList.add('dark');}catch(e){}})();`;
diff --git a/packages/shared/user-data/GraphQL/UserData.test.ts b/packages/shared/user-data/GraphQL/UserData.test.ts
index 884e37233e7..750cfea81b5 100644
--- a/packages/shared/user-data/GraphQL/UserData.test.ts
+++ b/packages/shared/user-data/GraphQL/UserData.test.ts
@@ -1,7 +1,13 @@
+import { REPLAY_THEME_STORAGE_KEY } from "shared/theme/replayTheme";
import { ConsoleEventFilterPreferences, config } from "shared/user-data/GraphQL/config";
import { LOCAL_STORAGE_KEY } from "shared/user-data/GraphQL/constants";
import { PreferencesKey } from "shared/user-data/GraphQL/types";
+/** Align with dashboard `replay_theme` bootstrap so UserData does not call storeTheme/saveLocal for theme in tests. */
+function getReplayThemeFromMock(): string {
+ return "system";
+}
+
describe("UserData", () => {
let localStorageMock: {
clear: jest.Mock;
@@ -65,9 +71,12 @@ describe("UserData", () => {
it("should support URL overrides for boolean preferences", () => {
localStorageMock.getItem.mockImplementation((key: string) => {
switch (key) {
+ case REPLAY_THEME_STORAGE_KEY:
+ return getReplayThemeFromMock();
case LOCAL_STORAGE_KEY:
return JSON.stringify({
layout_sidePanelCollapsed: false,
+ global_theme: "system",
});
default:
return null;
@@ -88,9 +97,12 @@ describe("UserData", () => {
it("should read/write values to localStorage", async () => {
localStorageMock.getItem.mockImplementation((key: string) => {
switch (key) {
+ case REPLAY_THEME_STORAGE_KEY:
+ return getReplayThemeFromMock();
case LOCAL_STORAGE_KEY:
return JSON.stringify({
console_showFiltersByDefault: true,
+ global_theme: "system",
});
default:
return null;
@@ -130,10 +142,13 @@ describe("UserData", () => {
it("should read/write values to GraphQL for authenticated users", async () => {
localStorageMock.getItem.mockImplementation((key: string) => {
switch (key) {
+ case REPLAY_THEME_STORAGE_KEY:
+ return getReplayThemeFromMock();
case LOCAL_STORAGE_KEY:
return JSON.stringify({
debugger_frameworkGroupingOn: false,
console_showFiltersByDefault: true,
+ global_theme: "system",
});
default:
return null;
@@ -243,6 +258,8 @@ describe("UserData", () => {
it("should be migrated from useLocalStorage hook", () => {
localStorageMock.getItem.mockImplementation((key: string) => {
switch (key) {
+ case REPLAY_THEME_STORAGE_KEY:
+ return getReplayThemeFromMock();
case config.layout_sidePanelCollapsed.legacyKey:
return JSON.stringify(true);
default:
@@ -258,6 +275,8 @@ describe("UserData", () => {
it("should migrated from Mozilla preferences service", () => {
localStorageMock.getItem.mockImplementation((key: string) => {
switch (key) {
+ case REPLAY_THEME_STORAGE_KEY:
+ return getReplayThemeFromMock();
case `Services.prefs:${config.protocol_repaintEvaluations.legacyKey}`:
return JSON.stringify({
hasUserValue: true,
@@ -276,6 +295,8 @@ describe("UserData", () => {
it("should memoize parsed legacy values", () => {
localStorageMock.getItem.mockImplementation((key: string) => {
switch (key) {
+ case REPLAY_THEME_STORAGE_KEY:
+ return getReplayThemeFromMock();
case config.console_eventFilters.legacyKey:
return JSON.stringify({
keyboard: true,
diff --git a/packages/shared/user-data/GraphQL/UserData.ts b/packages/shared/user-data/GraphQL/UserData.ts
index 4be16498ad8..404db45cf8d 100644
--- a/packages/shared/user-data/GraphQL/UserData.ts
+++ b/packages/shared/user-data/GraphQL/UserData.ts
@@ -7,6 +7,12 @@ import {
UpdateUserPreferences,
UpdateUserPreferencesVariables,
} from "shared/graphql/generated/UpdateUserPreferences";
+import {
+ REPLAY_THEME_STORAGE_KEY,
+ readLocalStorageTheme,
+ storeTheme,
+} from "shared/theme/replayTheme";
+import type { Theme } from "shared/theme/types";
import { config } from "shared/user-data/GraphQL/config";
import { LOCAL_STORAGE_KEY } from "shared/user-data/GraphQL/constants";
import { GET_USER_PREFERENCES, UPDATE_USER_PREFERENCES } from "shared/user-data/GraphQL/queries";
@@ -50,9 +56,31 @@ class UserData implements GraphQLService {
this.subscriberMap = new Map();
try {
- const raw = localStorage.getItem(LOCAL_STORAGE_KEY);
- if (raw !== null) {
- this.cachedUserPreferences = JSON.parse(raw) as any;
+ if (typeof localStorage !== "undefined") {
+ const raw = localStorage.getItem(LOCAL_STORAGE_KEY);
+ if (raw !== null) {
+ this.cachedUserPreferences = JSON.parse(raw) as any;
+ }
+ }
+ } catch (error) {}
+
+ try {
+ const dash = readLocalStorageTheme();
+ if (dash !== null) {
+ const prev = this.cachedUserPreferences.global_theme;
+ this.cachedUserPreferences = { ...this.cachedUserPreferences, global_theme: dash };
+ if (prev !== dash) {
+ this.saveLocal(this.cachedUserPreferences);
+ }
+ } else {
+ const ud: Theme =
+ this.cachedUserPreferences.global_theme ?? config.global_theme.defaultValue;
+ if (
+ typeof localStorage !== "undefined" &&
+ localStorage.getItem(REPLAY_THEME_STORAGE_KEY) === null
+ ) {
+ storeTheme(ud);
+ }
}
} catch (error) {}
@@ -110,6 +138,15 @@ class UserData implements GraphQLService {
}
}
+ try {
+ const dash = readLocalStorageTheme();
+ if (dash !== null && this.cachedUserPreferences.global_theme !== dash) {
+ this.cachedUserPreferences = { ...this.cachedUserPreferences, global_theme: dash };
+ this.saveLocal(this.cachedUserPreferences);
+ this.notifySubscribers("global_theme", dash);
+ }
+ } catch (error) {}
+
this.initialized = true;
}
@@ -193,6 +230,10 @@ class UserData implements GraphQLService {
return;
}
+ if (key === "global_theme") {
+ storeTheme(value as Theme);
+ }
+
this.cachedUserPreferences = {
...this.cachedUserPreferences,
[key]: value,
diff --git a/pages/_document.tsx b/pages/_document.tsx
index 428e4b410a5..c97eea1f1fa 100644
--- a/pages/_document.tsx
+++ b/pages/_document.tsx
@@ -4,6 +4,7 @@ import Document, { Head, Html, Main, NextScript } from "next/document";
import React from "react";
import { setErrorHandler } from "protocol/utils";
+import { themeInitScript } from "shared/theme/replayTheme";
import { isDevelopment } from "shared/utils/environment";
import { getAuthHost } from "ui/utils/auth";
@@ -21,6 +22,7 @@ const cspHashOf = (text: string) => {
};
const isDev = process.env.NODE_ENV !== "production";
+const themeInitCspHash = cspHashOf(themeInitScript);
const csp = (props: any) => {
const hash = cspHashOf(NextScript.getInlineScriptSource(props));
const authHost = getAuthHost();
@@ -34,7 +36,7 @@ const csp = (props: any) => {
}`,
`frame-src replay: https://js.stripe.com https://hooks.stripe.com https://${authHost} https://www.loom.com/`,
// Required by some of our external services
- `script-src 'self' 'unsafe-eval' https://cdn.lr-ingest.io https://cdn.lr-in.com https://js.stripe.com ${hash}`,
+ `script-src 'self' 'unsafe-eval' https://cdn.lr-ingest.io https://cdn.lr-in.com https://js.stripe.com ${hash} ${themeInitCspHash}`,
`form-action https://${authHost}`,
// From vercel's CSP config and Google fonts
@@ -58,14 +60,15 @@ const csp = (props: any) => {
export default class MyDocument extends Document {
render() {
return (
-
+
{/* nosemgrep typescript.react.security.audit.react-http-leak.react-http-leak */}
+
-
+
diff --git a/public/icons/logout.svg b/public/icons/logout.svg
new file mode 100644
index 00000000000..6939a720fa5
--- /dev/null
+++ b/public/icons/logout.svg
@@ -0,0 +1,6 @@
+
diff --git a/scripts/perf-analysis/tests/devtools-object-preview.ts b/scripts/perf-analysis/tests/devtools-object-preview.ts
index 6f14779e5c0..922ec664d37 100644
--- a/scripts/perf-analysis/tests/devtools-object-preview.ts
+++ b/scripts/perf-analysis/tests/devtools-object-preview.ts
@@ -17,7 +17,7 @@ export async function devtoolsObjectPreview(page: Page) {
await page.locator('[data-test-id="ConsoleMessageHoverButton"]').click();
await waitForTime(MsPerSecond);
- await page.locator("div:nth-child(5) > .toolbar-panel-button > button").click();
+ await page.locator('[data-test-name="ToolbarButton-Search"]').click();
await waitForTime(MsPerSecond);
await page.getByRole("button", { name: ": console{debug: ƒ(...r" }).click();
diff --git a/src/base.css b/src/base.css
index b5e3b2b4646..fc06cdef14e 100644
--- a/src/base.css
+++ b/src/base.css
@@ -22,7 +22,7 @@ body {
--faint-red: rgba(255, 0, 0, 0.5);
--light-blue: #61cdff;
--light-grey: #969696;
- font-family: "Inter", sans-serif;
+ font-family: Inter, system-ui, sans-serif;
}
#__next,
@@ -35,10 +35,13 @@ body {
position: relative;
width: 100%;
background: var(--chrome);
+ transition-property: background-color, color;
+ transition-duration: 150ms;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
.inter {
- font-family: "Inter", sans-serif;
+ font-family: Inter, system-ui, sans-serif;
}
img.avatar,
diff --git a/src/design-global.css b/src/design-global.css
new file mode 100644
index 00000000000..202a072b22d
--- /dev/null
+++ b/src/design-global.css
@@ -0,0 +1,425 @@
+/* Shared with replay-dashboard: semantic tokens and helper classes.
+ Plain CSS (no @layer / @apply) so Next can process this file without a @tailwind block. */
+
+:root {
+ --background: 0 0% 100%;
+ --foreground: 0 0% 3.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 0 0% 3.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 0 0% 3.9%;
+ --primary: 0 0% 9%;
+ --primary-foreground: 0 0% 98%;
+ --secondary: 0 0% 96.1%;
+ --secondary-foreground: 0 0% 9%;
+ --muted: 0 0% 96.1%;
+ --muted-foreground: 0 0% 45.1%;
+ --accent: 0 0% 96.1%;
+ --accent-foreground: 0 0% 9%;
+ --destructive: 0 72.2% 50.6%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 0 0% 89.8%;
+ --input: 0 0% 89.8%;
+ --ring: 0 0% 63.9%;
+ --code: 220 13% 18%;
+ --code-foreground: 210 40% 96%;
+ --sidebar: 0 0% 98%;
+ --sidebar-foreground: 0 0% 3.9%;
+ --sidebar-primary: 0 0% 9%;
+ --sidebar-primary-foreground: 0 0% 98%;
+ --sidebar-accent: 0 0% 96.1%;
+ --sidebar-accent-foreground: 0 0% 9%;
+ --sidebar-border: 0 0% 89.8%;
+ --sidebar-ring: 0 0% 63.9%;
+ --chart-1: 25 95% 53%;
+ --chart-2: 173 80% 40%;
+ --chart-3: 197 37% 24%;
+ --chart-4: 43 96% 56%;
+ --chart-5: 43 96% 56%;
+ --radius-xs: 0.125rem;
+ --radius-sm: 0.375rem;
+ --radius-md: 0.5rem;
+ --radius-lg: 0.625rem;
+ --radius-xl: 0.875rem;
+ --radius-2xl: 1rem;
+ --radius-3xl: 1.5rem;
+ --radius-4xl: 2rem;
+ --radius: 0.5rem;
+}
+
+:root.theme-dark {
+ color-scheme: dark;
+ --background: 0 0% 5.5%;
+ --foreground: 0 0% 95%;
+ --card: 0 0% 8.5%;
+ --card-foreground: 0 0% 95%;
+ --popover: 0 0% 10%;
+ --popover-foreground: 0 0% 95%;
+ --primary: 0 0% 90%;
+ --primary-foreground: 0 0% 7%;
+ --secondary: 0 0% 13%;
+ --secondary-foreground: 0 0% 95%;
+ --muted: 0 0% 12%;
+ --muted-foreground: 0 0% 55%;
+ --accent: 0 0% 16%;
+ --accent-foreground: 0 0% 95%;
+ --destructive: 0 62.8% 50.6%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 0 0% 16%;
+ --input: 0 0% 16%;
+ --ring: 0 0% 55%;
+ --code: 222 47% 11%;
+ --code-foreground: 210 40% 96%;
+ --sidebar: 0 0% 7%;
+ --sidebar-foreground: 0 0% 95%;
+ --sidebar-primary: 217 91% 60%;
+ --sidebar-primary-foreground: 0 0% 98%;
+ --sidebar-accent: 0 0% 16%;
+ --sidebar-accent-foreground: 0 0% 95%;
+ --sidebar-border: 0 0% 16%;
+ --sidebar-ring: 0 0% 40%;
+ --chart-1: 217 91% 60%;
+ --chart-2: 160 84% 39%;
+ --chart-3: 43 96% 56%;
+ --chart-4: 271 81% 56%;
+ --chart-5: 343 77% 50%;
+}
+
+.dark,
+[data-theme="dark"] {
+ color-scheme: dark;
+ --background: 0 0% 5.5%;
+ --foreground: 0 0% 95%;
+ --card: 0 0% 8.5%;
+ --card-foreground: 0 0% 95%;
+ --popover: 0 0% 10%;
+ --popover-foreground: 0 0% 95%;
+ --primary: 0 0% 90%;
+ --primary-foreground: 0 0% 7%;
+ --secondary: 0 0% 13%;
+ --secondary-foreground: 0 0% 95%;
+ --muted: 0 0% 12%;
+ --muted-foreground: 0 0% 55%;
+ --accent: 0 0% 16%;
+ --accent-foreground: 0 0% 95%;
+ --destructive: 0 62.8% 50.6%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 0 0% 16%;
+ --input: 0 0% 16%;
+ --ring: 0 0% 55%;
+ --code: 222 47% 11%;
+ --code-foreground: 210 40% 96%;
+ --sidebar: 0 0% 7%;
+ --sidebar-foreground: 0 0% 95%;
+ --sidebar-primary: 217 91% 60%;
+ --sidebar-primary-foreground: 0 0% 98%;
+ --sidebar-accent: 0 0% 16%;
+ --sidebar-accent-foreground: 0 0% 95%;
+ --sidebar-border: 0 0% 16%;
+ --sidebar-ring: 0 0% 40%;
+ --chart-1: 217 91% 60%;
+ --chart-2: 160 84% 39%;
+ --chart-3: 43 96% 56%;
+ --chart-4: 271 81% 56%;
+ --chart-5: 343 77% 50%;
+}
+
+.ease-cubic-bezier {
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.transition-theme {
+ transition-property: background-color, border-color, color;
+ transition-duration: 150ms;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.kdb {
+ border-radius: var(--radius-md);
+ padding: 0.25rem 0.375rem;
+ background-color: hsl(var(--code));
+ color: hsl(var(--code-foreground));
+}
+
+.max-w-chat {
+ max-width: var(--chat-max-width);
+}
+
+.flex-center {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.flex-between {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.flex-start {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+}
+
+.flex-end {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+}
+
+.card-base {
+ border-radius: 0.75rem;
+ border: 1px solid hsl(var(--border));
+ background-color: hsl(var(--card));
+ box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
+}
+
+.card-hover {
+ transition-property: all;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 200ms;
+}
+
+.card-hover:hover {
+ transform: scale(1.01);
+ box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
+}
+
+.card-interactive {
+ border-radius: 0.75rem;
+ border: 1px solid hsl(var(--border));
+ background-color: hsl(var(--card));
+ box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
+ cursor: pointer;
+ transition-property: all;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 200ms;
+}
+
+.card-interactive:hover {
+ transform: scale(1.01);
+ box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
+}
+
+.gradient-primary {
+ background-image: linear-gradient(to right, #f43f5e, #ec4899);
+ color: #fff;
+}
+
+.gradient-primary:hover {
+ background-image: linear-gradient(to right, #e11d48, #db2777);
+}
+
+.gradient-danger {
+ background-image: linear-gradient(to right, #ef4444, #f43f5e);
+ color: #fff;
+}
+
+.gradient-danger:hover {
+ background-image: linear-gradient(to right, #dc2626, #e11d48);
+}
+
+.focus-ring:focus {
+ outline: none;
+}
+
+.focus-ring:focus-visible {
+ outline: 2px solid transparent;
+ outline-offset: 2px;
+ box-shadow: 0 0 0 2px hsl(var(--background)), 0 0 0 4px hsl(var(--ring));
+}
+
+.icon-container {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: var(--radius-md);
+ background-color: hsl(var(--muted));
+ color: hsl(var(--muted-foreground));
+}
+
+.truncate-2 {
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+
+.truncate-3 {
+ display: -webkit-box;
+ -webkit-line-clamp: 3;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+
+@keyframes design-fade-in {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+.animate-in {
+ animation: design-fade-in 0.2s ease-out;
+}
+
+.hover-scale {
+ transition-property: transform;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 200ms;
+}
+
+.hover-scale:hover {
+ transform: scale(1.05);
+}
+
+.hover-scale-sm {
+ transition-property: transform;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 200ms;
+}
+
+.hover-scale-sm:hover {
+ transform: scale(1.02);
+}
+
+.bg-muted-80 {
+ background-color: hsl(var(--muted) / 0.8);
+}
+
+.bg-muted-50 {
+ background-color: hsl(var(--muted) / 0.5);
+}
+
+.bg-muted-30 {
+ background-color: hsl(var(--muted) / 0.3);
+}
+
+.bg-card-80 {
+ background-color: hsl(var(--card) / 0.8);
+}
+
+.bg-card-50 {
+ background-color: hsl(var(--card) / 0.5);
+}
+
+.bg-card-30 {
+ background-color: hsl(var(--card) / 0.3);
+}
+
+:root {
+ font-size: 16px;
+ color-scheme: light dark;
+ --context-menu-item-font-size: 0.9375rem;
+ --context-menu-item-padding: 0.5rem 1rem;
+ --header-height: 54px;
+ --chat-max-width: 600px;
+ --gradient-opacity: 0.8;
+ --primary-color: rgba(244, 63, 94, var(--gradient-opacity));
+ --secondary-color: rgba(225, 29, 72, var(--gradient-opacity));
+ --accent-color: rgba(190, 18, 60, var(--gradient-opacity));
+ --login-bg: #ffffff;
+ --login-fg: #0a0a0a;
+ --login-fg-secondary: #6b7280;
+ --login-card: #ffffff;
+ --login-card-border: #e5e5e5;
+ --login-input-bg: #ffffff;
+ --login-input-border: #d1d5db;
+ --login-input-focus: #9ca3af;
+ --login-btn-primary-bg: #171717;
+ --login-btn-primary-hover: #262626;
+ --login-btn-primary-fg: #fafafa;
+ --login-btn-outline-border: #d1d5db;
+ --login-btn-outline-hover: #f3f4f6;
+ --login-divider: #e5e7eb;
+ --login-link: #171717;
+ --login-error-bg: #fdf4f3;
+ --login-error-text: #c93545;
+ --login-error-border: #f8d3d4;
+}
+
+html {
+ background-color: transparent;
+}
+
+html,
+body {
+ height: 100%;
+ width: 100%;
+ margin: 0;
+ padding: 0;
+}
+
+.app-height {
+ height: 100vh;
+ height: var(--app-height, 100vh);
+ height: 100dvh;
+}
+
+@supports (padding: env(safe-area-inset-top)) {
+ .app-height {
+ padding-top: env(safe-area-inset-top);
+ padding-bottom: env(safe-area-inset-bottom);
+ }
+}
+
+*:focus-visible {
+ outline-color: hsl(var(--ring));
+ outline-offset: -2px;
+}
+
+input[type="checkbox"]:focus-visible {
+ outline-offset: 0;
+}
+
+a:focus-visible {
+ color: hsl(var(--ring));
+ outline: none;
+}
+
+button {
+ text-align: left;
+}
+
+input::placeholder {
+ color: #9ca3af;
+}
+
+.indeterminate {
+ animation: indeterminate 1s infinite linear;
+}
+
+@keyframes indeterminate {
+ 0% {
+ transform: translateX(0) scaleX(0);
+ }
+ 40% {
+ transform: translateX(0) scaleX(0.4);
+ }
+ 100% {
+ transform: translateX(100%) scaleX(0.5);
+ }
+}
+
+/* Primary sidebar (Toolbar): hover labels — light = white card + subtle border; dark = raised surface */
+.replay-toolbar-tooltip {
+ background-color: hsl(var(--card)) !important;
+ border: 1px solid hsl(var(--border)) !important;
+ border-radius: 0.5rem !important;
+ color: hsl(var(--card-foreground)) !important;
+ padding: 0.375rem 0.75rem !important;
+ font-size: 0.8125rem !important;
+ font-weight: 500 !important;
+ line-height: 1.25rem !important;
+ filter: none !important;
+ box-shadow: 0 1px 2px 0 rgb(15 23 42 / 0.05), 0 1px 3px 0 rgb(15 23 42 / 0.08) !important;
+}
+
+.theme-dark .replay-toolbar-tooltip,
+.dark .replay-toolbar-tooltip {
+ box-shadow: 0 2px 10px rgb(0 0 0 / 0.4) !important;
+}
diff --git a/src/devtools/client/debugger/src/components/WelcomeBox.tsx b/src/devtools/client/debugger/src/components/WelcomeBox.tsx
index 82711760b82..676d2aab1b3 100644
--- a/src/devtools/client/debugger/src/components/WelcomeBox.tsx
+++ b/src/devtools/client/debugger/src/components/WelcomeBox.tsx
@@ -22,47 +22,62 @@ export default function WelcomeBox() {
};
return (
-
-
-
-
-
+
+
+
Keyboard shortcuts
+
+
-
Command palette
+
+
{" "}
+
+
+
+
-
Go to file
+
+
{" "}
+
+
+
+
+
Find in file
+
+
{" "}
+
{" "}
+
+
+
-
-
-
+
diff --git a/src/devtools/client/themes/common.css b/src/devtools/client/themes/common.css
index c05f488f877..2d44823a151 100644
--- a/src/devtools/client/themes/common.css
+++ b/src/devtools/client/themes/common.css
@@ -308,7 +308,11 @@ input[type="checkbox"] {
input[type="checkbox"]:checked,
input[type="checkbox"]:checked:hover {
- background-color: var(--primary-accent);
+ background-color: var(--color-default);
+ background-image: var(--checkbox-checked-mark-image);
+ background-size: 0.75rem;
+ background-position: center;
+ background-repeat: no-repeat;
}
input[type="checkbox"] {
border-radius: 0.25rem;
diff --git a/src/global-css.ts b/src/global-css.ts
index 3ac7c65d304..e71d4cf235e 100644
--- a/src/global-css.ts
+++ b/src/global-css.ts
@@ -1,6 +1,7 @@
import "image/image.css";
import "image/icon.css";
import "tailwindcss/tailwind.css";
+import "design-global.css";
import "devtools/client/debugger/src/components/variables.css";
import "replay-next/variables.css";
import "devtools/client/themes/variables.css";
diff --git a/src/image/icon.css b/src/image/icon.css
index b6a68633b56..bccb56107c3 100644
--- a/src/image/icon.css
+++ b/src/image/icon.css
@@ -33,6 +33,10 @@ but without a background color so we can use hover effects
-webkit-mask-image: url(/recording/icons/settings.svg);
}
+.icon.logout {
+ -webkit-mask-image: url(/recording/icons/logout.svg);
+}
+
.icon.replay-logo {
-webkit-mask-image: url(/recording/icons/replay-logo.svg);
}
diff --git a/src/ui/components/App.tsx b/src/ui/components/App.tsx
index 23ede6163ec..ab88e3edefe 100644
--- a/src/ui/components/App.tsx
+++ b/src/ui/components/App.tsx
@@ -1,7 +1,11 @@
import { PropsWithChildren, useEffect } from "react";
import Spinner from "replay-next/components/Spinner";
-import { getSystemColorScheme } from "shared/theme/getSystemColorScheme";
+import {
+ REPLAY_THEME_STORAGE_KEY,
+ applyThemeToDOM,
+ getEffectiveTheme,
+} from "shared/theme/replayTheme";
import { Theme } from "shared/theme/types";
import { userData } from "shared/user-data/GraphQL/UserData";
import { isTest } from "shared/utils/environment";
@@ -53,16 +57,38 @@ export default function App({ children }: PropsWithChildren) {
}, []);
useEffect(() => {
- const updateTheme = (theme: Theme) => {
- if (theme === "system") {
- theme = getSystemColorScheme();
- }
- document.body.parentElement!.className = `theme-${theme}`;
+ const updateTheme = (preference: Theme) => {
+ applyThemeToDOM(getEffectiveTheme(preference));
};
updateTheme(userData.get("global_theme"));
- userData.subscribe("global_theme", updateTheme);
+ const unsub = userData.subscribe("global_theme", updateTheme);
+
+ const onStorage = (e: StorageEvent) => {
+ if (e.storageArea !== localStorage || e.key !== REPLAY_THEME_STORAGE_KEY) {
+ return;
+ }
+ const v = e.newValue;
+ if (v === "light" || v === "dark" || v === "system") {
+ void userData.set("global_theme", v);
+ }
+ };
+ window.addEventListener("storage", onStorage);
+
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
+ const onOsThemeChange = () => {
+ if (userData.get("global_theme") === "system") {
+ applyThemeToDOM(getEffectiveTheme("system"));
+ }
+ };
+ mq.addEventListener("change", onOsThemeChange);
+
+ return () => {
+ unsub();
+ window.removeEventListener("storage", onStorage);
+ mq.removeEventListener("change", onOsThemeChange);
+ };
}, []);
if (userInfo.loading) {
diff --git a/src/ui/components/Header/Header.module.css b/src/ui/components/Header/Header.module.css
index 6b2c45ea059..fdb4c5031ed 100644
--- a/src/ui/components/Header/Header.module.css
+++ b/src/ui/components/Header/Header.module.css
@@ -1,11 +1,15 @@
.Header {
- background-color: var(--chrome);
+ background-color: hsl(var(--card));
+ border-bottom: 1px solid hsl(var(--border) / 0.5);
height: 50px;
display: flex;
align-items: center;
flex-shrink: 0;
- padding: 0 8px;
+ padding: 0 12px;
justify-content: space-between;
+ transition-property: background-color, border-color;
+ transition-duration: 150ms;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
.Links {
@@ -27,13 +31,15 @@
.ReadOnlyTitle {
flex: 0 1 auto;
margin: 0 0.25em;
- padding: 0.25em;
+ padding: 0.25em 0.375em;
border: 2px solid transparent;
- border-radius: 0.25em;
+ border-radius: var(--radius-md);
- font-family: Inter, sans-serif;
- font-size: 18px;
- line-height: 22px;
+ font-family: Inter, system-ui, sans-serif;
+ font-size: 1.125rem;
+ font-weight: 500;
+ line-height: 1.5rem;
+ color: hsl(var(--foreground));
overflow: hidden;
text-overflow: ellipsis;
@@ -44,11 +50,11 @@
cursor: pointer;
}
.EditableTitle:hover {
- background-color: var(--title-hover-bgcolor);
+ background-color: hsl(var(--accent));
}
.EditableTitle:focus {
outline: none;
- border-color: var(--primary-accent);
+ border-color: hsl(var(--ring));
}
.BackButton {
@@ -69,14 +75,18 @@
flex-direction: row;
align-items: center;
gap: 1ch;
- font-size: var(--font-size-regular);
- background-color: var(--body-bgcolor);
- border-radius: 1rem;
+ font-size: 0.9375rem;
+ line-height: 1.375rem;
+ color: hsl(var(--muted-foreground));
+ background-color: hsl(var(--muted));
+ border: 1px solid hsl(var(--border) / 0.6);
+ border-radius: var(--radius-lg);
overflow: hidden;
- padding: 0.25rem 0.5rem;
+ padding: 0.25rem 0.625rem;
cursor: pointer;
}
.VisibilityIconAndText:hover {
- background-color: var(--title-hover-bgcolor);
+ background-color: hsl(var(--accent));
+ color: hsl(var(--accent-foreground));
}
diff --git a/src/ui/components/Header/ShareButton.tsx b/src/ui/components/Header/ShareButton.tsx
index 8939f7e225d..2ed0d7d742c 100644
--- a/src/ui/components/Header/ShareButton.tsx
+++ b/src/ui/components/Header/ShareButton.tsx
@@ -25,14 +25,8 @@ function ShareButton({ setModal }: PropsFromRedux) {
};
return (
-