mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-10-30 00:27:43 +09:00
Closes #6301 ## Summary - Restore Dashboard project tags to the pre-color-refactor treatment: fixed white text over a 30% black scrim in both application themes. - Use one high-contrast text-selection treatment across native inputs, textareas, contenteditable fields, and every shared CodeMirror editor. - Preserve the existing subtle selection treatment for Navigator, Project Settings, command menus, Dashboard navigation, and other selected controls. - Add direct Storybook coverage for the CSS fragment editor. - Simplify chromatic color derivation from 571 to 329 lines without changing computed colors. ## Technical choices - Keep project tags independent from application-theme polarity because they overlay arbitrary thumbnail artwork. - Keep the existing `selectionBackground` control-state export unchanged at its original 12% light / 20% dark accent blend. - Define separate `--background-text-selection` and `--foreground-on-text-selection` tokens exclusively for selected text. - Use the bounded accent in light mode and a luminance-lifted accent in dark mode so selected text remains distinct even with extreme custom theme colors. - Style CodeMirror through its existing selection layer and `::selection` CSS hooks; no selection tracking or editor-state logic is added. - Leave canvas rich-text selection independent: authored canvas documents do not load the Builder UI theme stylesheet. - Replace five duplicated XYZ clamp formulas with the exact upper-luminance cap they reduce to. The removed lower-bound and white-mixing branches were unreachable because the minimum was fixed at zero. ## Todo - [x] Restore the original project-tag scrim and foreground in both themes. - [x] Inventory native, CodeMirror, and canvas rich-text selection paths. - [x] Unify native and shared CodeMirror text-selection colors. - [x] Keep text selection independent from selected control and navigation states. - [x] Add the missing CSS fragment editor story. - [x] Add light- and dark-mode browser regressions and confirm they fail before the fix. - [x] Enforce 4.5:1 selected-text contrast and 3:1 highlight-to-surface contrast across supported theme inputs. - [x] Remove duplicated and unreachable chromatic color derivation while preserving outputs. - [x] Review documentation impact; the design-system color-token reference is updated, with no user-facing product documentation change required. - [x] Verify fixture impact; no fixture inputs or generated outputs are affected. ## Verification - `pnpm --filter @webstudio-is/design-system test` — 227 tests passed, including the control/text separation regression, Navigator tree interaction, default-palette comparisons, and the full supported-theme contrast sweep - `pnpm --filter @webstudio-is/design-system typecheck` - `pnpm --filter @webstudio-is/builder exec vitest run app/shared/code-editor-base.browser.test.tsx --project app-browser-shared` - `pnpm --filter @webstudio-is/builder exec vitest run app/dashboard/projects/project-card.browser.test.tsx --project app-browser-canvas-dashboard` - `pnpm --filter @webstudio-is/builder typecheck` - Targeted `oxlint`, formatting, and `git diff --check` - Live dark-mode Dashboard verification of the restored subtle navigation selection - Storybook visual checks for the full color system and input/CodeMirror selection in light and dark modes
140 lines
4.1 KiB
TypeScript
140 lines
4.1 KiB
TypeScript
import { act } from "react-dom/test-utils";
|
|
import { createRoot, type Root } from "react-dom/client";
|
|
import { EditorSelection } from "@codemirror/state";
|
|
import { EditorView } from "@codemirror/view";
|
|
import { afterEach, beforeEach, expect, test, vi } from "vitest";
|
|
import { cssVar } from "@webstudio-is/design-system";
|
|
import "@webstudio-is/design-system/global.css";
|
|
import { EditorContent } from "./code-editor-base";
|
|
|
|
(
|
|
globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
).IS_REACT_ACT_ENVIRONMENT = true;
|
|
|
|
let container: HTMLDivElement;
|
|
let root: Root;
|
|
const rangeGetClientRects = Object.getOwnPropertyDescriptor(
|
|
Range.prototype,
|
|
"getClientRects"
|
|
);
|
|
|
|
beforeEach(() => {
|
|
vi.stubGlobal(
|
|
"ResizeObserver",
|
|
class {
|
|
observe() {}
|
|
unobserve() {}
|
|
disconnect() {}
|
|
}
|
|
);
|
|
Object.defineProperty(Range.prototype, "getClientRects", {
|
|
configurable: true,
|
|
value: () => [new DOMRect(0, 0, 20, 16)],
|
|
});
|
|
container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
root = createRoot(container);
|
|
});
|
|
|
|
afterEach(() => {
|
|
act(() => root.unmount());
|
|
container.remove();
|
|
document.documentElement.removeAttribute("data-color-scheme");
|
|
vi.unstubAllGlobals();
|
|
if (rangeGetClientRects === undefined) {
|
|
delete (Range.prototype as { getClientRects?: unknown }).getClientRects;
|
|
} else {
|
|
Object.defineProperty(
|
|
Range.prototype,
|
|
"getClientRects",
|
|
rangeGetClientRects
|
|
);
|
|
}
|
|
});
|
|
|
|
test.each(["light", "dark"] as const)(
|
|
"uses the shared selection color in %s mode",
|
|
async (mode) => {
|
|
document.documentElement.dataset.colorScheme = mode;
|
|
act(() => {
|
|
root.render(
|
|
<EditorContent
|
|
value="selected text"
|
|
onChange={() => {}}
|
|
onChangeComplete={() => {}}
|
|
/>
|
|
);
|
|
});
|
|
|
|
const editor = container.querySelector<HTMLElement>('[role="textbox"]');
|
|
if (editor === null) {
|
|
throw new Error("Expected the code editor");
|
|
}
|
|
const view = EditorView.findFromDOM(editor);
|
|
if (view === null) {
|
|
throw new Error("Expected the CodeMirror view");
|
|
}
|
|
act(() => {
|
|
view.dispatch({
|
|
selection: EditorSelection.range(0, view.state.doc.length),
|
|
});
|
|
view.focus();
|
|
});
|
|
await act(
|
|
() =>
|
|
new Promise<void>((resolve) =>
|
|
requestAnimationFrame(() => requestAnimationFrame(() => resolve()))
|
|
)
|
|
);
|
|
|
|
const selection = container.querySelector<HTMLElement>(
|
|
".cm-selectionBackground"
|
|
);
|
|
if (selection === null) {
|
|
throw new Error("Expected a rendered selection");
|
|
}
|
|
const reference = document.createElement("span");
|
|
reference.style.background = cssVar("--background-text-selection");
|
|
reference.style.color = "var(--foreground-on-text-selection)";
|
|
document.body.appendChild(reference);
|
|
|
|
const selectionColor = getComputedStyle(selection).backgroundColor;
|
|
const referenceColor = getComputedStyle(reference).backgroundColor;
|
|
const content = container.querySelector<HTMLElement>(".cm-content");
|
|
if (content === null) {
|
|
throw new Error("Expected editor content");
|
|
}
|
|
const selectedTextColor = getComputedStyle(content, "::selection").color;
|
|
const referenceTextColor = getComputedStyle(reference).color;
|
|
reference.remove();
|
|
expect(selectionColor).toBe(referenceColor);
|
|
expect(selectedTextColor).toBe(referenceTextColor);
|
|
}
|
|
);
|
|
|
|
test("scopes disabled styles to the Webstudio code editor", () => {
|
|
act(() => {
|
|
root.render(
|
|
<fieldset disabled>
|
|
<EditorContent
|
|
value="disabled editor"
|
|
onChange={() => {}}
|
|
onChangeComplete={() => {}}
|
|
/>
|
|
<div className="cm-editor" data-foreign-editor />
|
|
</fieldset>
|
|
);
|
|
});
|
|
|
|
const editors = container.querySelectorAll<HTMLElement>(".cm-editor");
|
|
const foreignEditor = container.querySelector<HTMLElement>(
|
|
"[data-foreign-editor]"
|
|
);
|
|
if (editors[0] === undefined || foreignEditor === null) {
|
|
throw new Error("Expected both code editors");
|
|
}
|
|
|
|
expect(getComputedStyle(editors[0]).opacity).toBe("0.3");
|
|
expect(getComputedStyle(foreignEditor).opacity).toBe("1");
|
|
});
|