chore(html): add html settings dialog (#37565)

This commit is contained in:
Pavel Feldman 2025-09-25 15:29:13 -07:00 committed by GitHub
parent ef9ce58723
commit 888da0f89d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 599 additions and 503 deletions

View File

@ -454,8 +454,12 @@ SOFTWARE. */
--color-scale-coral-9: #510901
}
@media(prefers-color-scheme: dark) {
:root {
:root.light-mode {
color-scheme: light;
}
:root.dark-mode {
color-scheme: dark;
--color-canvas-default-transparent: rgba(13,17,23,0);
--color-marketing-icon-primary: #79c0ff;
--color-marketing-icon-secondary: #1f6feb;
@ -887,5 +891,4 @@ SOFTWARE. */
--color-scale-coral-7: #872012;
--color-scale-coral-8: #640D04;
--color-scale-coral-9: #460701
}
}

View File

@ -40,6 +40,12 @@ svg {
position: relative;
}
.cursor-pointer,
.cursor-pointer > * {
cursor: pointer;
user-select: none;
}
.hbox {
display: flex;
flex: auto;
@ -296,6 +302,22 @@ article, aside, details, figcaption, figure, footer, header, main, menu, nav, se
background-color: var(--color-btn-hover-bg);
}
input[type="checkbox"] {
outline: var(--color-focus-border);
height: 24px;
}
dialog {
background-color: var(--color-canvas-subtle);
border: 1px solid var(--color-border-default);
border-radius: 6px;
padding: 6px;
}
.subnav-item .octicon.octicon-settings {
margin-right: 0;
}
@media only screen and (max-width: 600px) {
.subnav-item, .form-control {
border-radius: 0 !important;

View File

@ -24,6 +24,8 @@ import { Link, navigate, SearchParamsContext } from './links';
import { statusIcon } from './statusIcon';
import { filterWithQuery } from './filter';
import { linkifyText } from '@web/renderUtils';
import { Dialog } from '@web/shared/dialog';
import { useDarkModeSetting } from '@web/theme';
export const HeaderView: React.FC<{
title: string | undefined,
@ -90,6 +92,7 @@ const StatsNavView: React.FC<{
<NavLink token='failed' count={stats.unexpected} />
<NavLink token='flaky' count={stats.flaky} />
<NavLink token='skipped' count={stats.skipped} />
<SettingsButton />
</nav>;
};
@ -112,3 +115,42 @@ const NavLink: React.FC<{
<span className='d-inline counter'>{count}</span>
</Link>;
};
const SettingsButton: React.FC = () => {
const settingsRef = React.useRef<HTMLAnchorElement>(null);
const [settingsOpen, setSettingsOpen] = React.useState(false);
const [darkMode, setDarkMode] = useDarkModeSetting();
return <>
<Dialog
open={settingsOpen}
width={200}
verticalOffset={8}
requestClose={() => setSettingsOpen(false)}
anchor={settingsRef}
dataTestId='settings-dialog'
>
<div className='hbox cursor-pointer' style={{ alignItems: 'center' }}>
<input type='checkbox' id='dark-mode-setting' checked={darkMode} onChange={() => setDarkMode(!darkMode)}></input>
<label htmlFor='dark-mode-setting'>Dark mode</label>
</div>
</Dialog>
<a
ref={settingsRef}
style={{ cursor: 'pointer' }}
className='subnav-item'
title='Settings'
onClick={e => {
setSettingsOpen(!settingsOpen);
e.preventDefault();
}}
onMouseDown={preventDefault}>
{icons.settings()}
</a>
</>;
};
const preventDefault = (e: any) => {
e.stopPropagation();
e.preventDefault();
};

View File

@ -103,3 +103,9 @@ export const copy = () => {
<path d='M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z'></path>
</svg>;
};
export const settings = () => {
return <svg className='octicon octicon-settings' viewBox='0 0 16 16' width='16' height='16' aria-hidden='true'>
<path d='M8 0a8.2 8.2 0 0 1 .701.031C9.444.095 9.99.645 10.16 1.29l.288 1.107c.018.066.079.158.212.224.231.114.454.243.668.386.123.082.233.09.299.071l1.103-.303c.644-.176 1.392.021 1.82.63.27.385.506.792.704 1.218.315.675.111 1.422-.364 1.891l-.814.806c-.049.048-.098.147-.088.294.016.257.016.515 0 .772-.01.147.038.246.088.294l.814.806c.475.469.679 1.216.364 1.891a7.977 7.977 0 0 1-.704 1.217c-.428.61-1.176.807-1.82.63l-1.102-.302c-.067-.019-.177-.011-.3.071a5.909 5.909 0 0 1-.668.386c-.133.066-.194.158-.211.224l-.29 1.106c-.168.646-.715 1.196-1.458 1.26a8.006 8.006 0 0 1-1.402 0c-.743-.064-1.289-.614-1.458-1.26l-.289-1.106c-.018-.066-.079-.158-.212-.224a5.738 5.738 0 0 1-.668-.386c-.123-.082-.233-.09-.299-.071l-1.103.303c-.644.176-1.392-.021-1.82-.63a8.12 8.12 0 0 1-.704-1.218c-.315-.675-.111-1.422.363-1.891l.815-.806c.05-.048.098-.147.088-.294a6.214 6.214 0 0 1 0-.772c.01-.147-.038-.246-.088-.294l-.815-.806C.635 6.045.431 5.298.746 4.623a7.92 7.92 0 0 1 .704-1.217c.428-.61 1.176-.807 1.82-.63l1.102.302c.067.019.177.011.3-.071.214-.143.437-.272.668-.386.133-.066.194-.158.211-.224l.29-1.106C6.009.645 6.556.095 7.299.03 7.53.01 7.764 0 8 0Zm-.571 1.525c-.036.003-.108.036-.137.146l-.289 1.105c-.147.561-.549.967-.998 1.189-.173.086-.34.183-.5.29-.417.278-.97.423-1.529.27l-1.103-.303c-.109-.03-.175.016-.195.045-.22.312-.412.644-.573.99-.014.031-.021.11.059.19l.815.806c.411.406.562.957.53 1.456a4.709 4.709 0 0 0 0 .582c.032.499-.119 1.05-.53 1.456l-.815.806c-.081.08-.073.159-.059.19.162.346.353.677.573.989.02.03.085.076.195.046l1.102-.303c.56-.153 1.113-.008 1.53.27.161.107.328.204.501.29.447.222.85.629.997 1.189l.289 1.105c.029.109.101.143.137.146a6.6 6.6 0 0 0 1.142 0c.036-.003.108-.036.137-.146l.289-1.105c.147-.561.549-.967.998-1.189.173-.086.34-.183.5-.29.417-.278.97-.423 1.529-.27l1.103.303c.109.029.175-.016.195-.045.22-.313.411-.644.573-.99.014-.031.021-.11-.059-.19l-.815-.806c-.411-.406-.562-.957-.53-1.456a4.709 4.709 0 0 0 0-.582c-.032-.499.119-1.05.53-1.456l.815-.806c.081-.08.073-.159.059-.19a6.464 6.464 0 0 0-.573-.989c-.02-.03-.085-.076-.195-.046l-1.102.303c-.56.153-1.113.008-1.53-.27a4.44 4.44 0 0 0-.501-.29c-.447-.222-.85-.629-.997-1.189l-.289-1.105c-.029-.11-.101-.143-.137-.146a6.6 6.6 0 0 0-1.142 0ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0ZM9.5 8a1.5 1.5 0 1 0-3.001.001A1.5 1.5 0 0 0 9.5 8Z'></path>
</svg>;
};

View File

@ -28,6 +28,8 @@ const zipjs = zipImport as typeof zip;
import logo from '@web/assets/playwright-logo.svg';
import { SearchParamsProvider } from './links';
import { applyTheme } from '@web/theme';
const link = document.createElement('link');
link.rel = 'shortcut icon';
link.href = logo;
@ -49,6 +51,7 @@ const ReportLoader: React.FC = () => {
};
window.onload = () => {
applyTheme();
ReactDOM.createRoot(document.querySelector('#root')!).render(<ReportLoader />);
};

View File

@ -35,7 +35,7 @@
color: var(--color-fg-default);
}
@media(prefers-color-scheme: light) {
:root.light-mode {
.label-color-0 {
background-color: var(--color-scale-blue-0);
color: var(--color-scale-blue-6);
@ -68,7 +68,7 @@
}
}
@media(prefers-color-scheme: dark) {
:root.dark-mode {
.label-color-0 {
background-color: var(--color-scale-blue-9);
color: var(--color-scale-blue-2);

View File

@ -45,13 +45,13 @@
padding: 2px 8px;
}
@media(prefers-color-scheme: light) {
:root.light-mode {
.test-result-counter {
background: var(--color-scale-gray-5);
}
}
@media(prefers-color-scheme: dark) {
:root.dark-mode {
.test-result-counter {
background: var(--color-scale-gray-3);
}

View File

@ -40,7 +40,7 @@
color: #a1260d;
}
body.dark-mode .recorder .toolbar-button.toggled.circle-large-filled {
:root:dark-mode .recorder .toolbar-button.toggled.circle-large-filled {
color: #f48771;
}

View File

@ -30,7 +30,7 @@ import { useDarkModeSetting } from '@web/theme';
import { copy, useSetting } from '@web/uiUtils';
import yaml from 'yaml';
import { parseAriaSnapshot } from '@isomorphic/ariaSnapshot';
import { Dialog } from '@web/components/dialog';
import { Dialog } from '@web/shared/dialog';
export interface RecorderProps {
sources: Source[],

View File

@ -63,7 +63,7 @@
height: var(--browser-frame-header-height);
}
body.dark-mode .browser-frame-header {
:root:dark-mode .browser-frame-header {
background: #444950;
}

View File

@ -108,7 +108,7 @@
--action-background-color: #1a85ff66;
}
body.dark-mode .timeline-bar.action.error {
:root:dark-mode .timeline-bar.action.error {
--action-color: var(--vscode-errorForeground);
--action-background-color: #f4877166;
}
@ -162,7 +162,7 @@ body.dark-mode .timeline-bar.action.error {
background-color: #3879d91a;
}
body.dark-mode .timeline-window-curtain {
:root:dark-mode .timeline-window-curtain {
background-color: #161718bf;
}

View File

@ -45,7 +45,7 @@ import type { HighlightedElement } from './snapshotTab';
import type { TestAnnotation } from '@playwright/test';
import { MetadataWithCommitInfo } from '@testIsomorphic/types';
import type { ActionGroup } from '@isomorphic/protocolFormatter';
import { DialogToolbarButton } from '@web/components/dialog';
import { DialogToolbarButton } from '@web/components/dialogToolbarButton';
import { SettingsView } from './settingsView';
export const Workbench: React.FunctionComponent<{

View File

@ -34,7 +34,7 @@ body .drop-target {
background: rgba(255, 255, 255, 0.8);
}
body.dark-mode .drop-target {
:root:dark-mode .drop-target {
background: rgba(0, 0, 0, 0.8);
}

View File

@ -20,7 +20,8 @@ import { MultiTraceModel } from './modelUtil';
import './workbenchLoader.css';
import { Workbench } from './workbench';
import { TestServerConnection, WebSocketTestServerTransport } from '@testIsomorphic/testServerConnection';
import { Dialog, DialogToolbarButton } from '@web/components/dialog';
import { DialogToolbarButton } from '@web/components/dialogToolbarButton';
import { Dialog } from '@web/shared/dialog';
import { DefaultSettingsView } from './defaultSettingsView';
export const WorkbenchLoader: React.FunctionComponent<{

View File

@ -79,33 +79,33 @@
color: #267f99;
}
body.dark-mode .CodeMirror span.cm-def,
body.dark-mode .CodeMirror span.cm-tag {
:root:dark-mode .CodeMirror span.cm-def,
:root:dark-mode .CodeMirror span.cm-tag {
color: var(--vscode-debugView-valueChangedHighlight);
}
body.dark-mode .CodeMirror span.cm-comment,
body.dark-mode .CodeMirror span.cm-link {
:root:dark-mode .CodeMirror span.cm-comment,
:root:dark-mode .CodeMirror span.cm-link {
color: #6a9955;
}
body.dark-mode .CodeMirror span.cm-variable,
body.dark-mode .CodeMirror span.cm-variable-2,
body.dark-mode .CodeMirror span.cm-atom {
:root:dark-mode .CodeMirror span.cm-variable,
:root:dark-mode .CodeMirror span.cm-variable-2,
:root:dark-mode .CodeMirror span.cm-atom {
color: #4fc1ff;
}
body.dark-mode .CodeMirror span.cm-property {
:root:dark-mode .CodeMirror span.cm-property {
color: #dcdcaa;
}
body.dark-mode .CodeMirror span.cm-qualifier,
body.dark-mode .CodeMirror span.cm-attribute {
:root:dark-mode .CodeMirror span.cm-qualifier,
:root:dark-mode .CodeMirror span.cm-attribute {
color: #9cdcfe;
}
body.dark-mode .CodeMirror span.cm-variable-3,
body.dark-mode .CodeMirror span.cm-type {
:root:dark-mode .CodeMirror span.cm-variable-3,
:root:dark-mode .CodeMirror span.cm-type {
color: #4ec9b0;
}

View File

@ -0,0 +1,55 @@
/*
Copyright (c) Microsoft Corporation.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as React from 'react';
import { ToolbarButton } from './toolbarButton';
import { Dialog } from '../shared/dialog';
export interface DialogToolbarButtonProps {
title?: string;
icon?: string;
dialogDataTestId?: string;
}
export const DialogToolbarButton: React.FC<React.PropsWithChildren<DialogToolbarButtonProps>> = ({ title, icon, dialogDataTestId, children }) => {
const hostingRef = React.useRef<HTMLButtonElement>(null);
const [open, setOpen] = React.useState(false);
return (
<>
<ToolbarButton
ref={hostingRef}
icon={icon}
title={title}
onClick={() => setOpen(current => !current)}
/>
<Dialog
style={{
backgroundColor: 'var(--vscode-sideBar-background)',
padding: '4px 8px'
}}
open={open}
width={200}
// TODO: Temporary spacing until design of toolbar buttons is revisited
verticalOffset={8}
requestClose={() => setOpen(false)}
anchor={hostingRef}
dataTestId={dialogDataTestId}
>
{children}
</Dialog>
</>
);
};

View File

@ -15,7 +15,6 @@
*/
import * as React from 'react';
import { ToolbarButton } from './toolbarButton';
export interface DialogProps {
className?: string;
@ -164,39 +163,3 @@ const buildTopLeftCoordWithAlignment = (
};
}
};
export interface DialogToolbarButtonProps {
title?: string;
icon?: string;
dialogDataTestId?: string;
}
export const DialogToolbarButton: React.FC<React.PropsWithChildren<DialogToolbarButtonProps>> = ({ title, icon, dialogDataTestId, children }) => {
const hostingRef = React.useRef<HTMLButtonElement>(null);
const [open, setOpen] = React.useState(false);
return (
<>
<ToolbarButton
ref={hostingRef}
icon={icon}
title={title}
onClick={() => setOpen(current => !current)}
/>
<Dialog
style={{
backgroundColor: 'var(--vscode-sideBar-background)',
padding: '4px 8px'
}}
open={open}
width={200}
// TODO: Temporary spacing until design of toolbar buttons is revisited
verticalOffset={8}
requestClose={() => setOpen(false)}
anchor={hostingRef}
dataTestId={dialogDataTestId}
>
{children}
</Dialog>
</>
);
};

View File

@ -41,7 +41,9 @@ export function applyTheme() {
const currentTheme = settings.getString('theme', defaultTheme);
if (currentTheme === 'dark-mode')
document.body.classList.add('dark-mode');
document.documentElement.classList.add('dark-mode');
else
document.documentElement.classList.add('light-mode');
}
type Theme = 'dark-mode' | 'light-mode';
@ -52,8 +54,8 @@ export function toggleTheme() {
const newTheme = oldTheme === 'dark-mode' ? 'light-mode' : 'dark-mode';
if (oldTheme)
document.body.classList.remove(oldTheme);
document.body.classList.add(newTheme);
document.documentElement.classList.remove(oldTheme);
document.documentElement.classList.add(newTheme);
settings.setString('theme', newTheme);
for (const listener of listeners)
listener(newTheme);
@ -68,7 +70,7 @@ export function removeThemeListener(listener: (theme: Theme) => void) {
}
export function currentTheme(): Theme {
return document.body.classList.contains('dark-mode') ? 'dark-mode' : 'light-mode';
return document.documentElement.classList.contains('dark-mode') ? 'dark-mode' : 'light-mode';
}
export function useDarkModeSetting(): [boolean, (value: boolean) => void] {

View File

@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
body {
:root {
--vscode-font-family: system-ui, "Ubuntu", "Droid Sans", sans-serif;
--vscode-font-weight: normal;
--vscode-font-size: 13px;
@ -544,13 +544,12 @@ body {
--vscode-gitDecoration-submoduleResourceForeground: #1258a7;
}
body.light-mode {
:root.light-mode {
color-scheme: light;
}
body.dark-mode {
:root.dark-mode {
color-scheme: dark;
--vscode-font-family: system-ui, "Ubuntu", "Droid Sans", sans-serif;
--vscode-font-weight: normal;
--vscode-font-size: 13px;