Files
Oleg Isonen d4b1994a6a fix: Improve editor selection and project tag contrast (#6302)
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
2026-09-01 17:05:14 +02:00

129 lines
3.5 KiB
TypeScript

import {
AccessibleIcon,
css,
cssVar,
Grid,
LinkButton,
Text,
theme,
webstudioBrand,
} from "@webstudio-is/design-system";
import { WebstudioIcon } from "@webstudio-is/icons";
const pageStyle = css({
position: "fixed",
justifyItems: "center",
alignContent: "start",
inset: 0,
background: webstudioBrand.backgroundGradient,
paddingTop: "10vh",
// prevent global root styles override error color
color: `light-dark(
${cssVar("--foreground-primary", "#11181c")},
${cssVar("--foreground-on-inverse", "#fff")}
)`,
});
const standaloneButtonBackground = cssVar("--background-accent", "#096cff");
const standaloneButtonForeground = cssVar("--foreground-on-accent", "#fff");
const standaloneButtonHoverOverlay = `oklch(from ${standaloneButtonForeground} 0 0 h / 6.2745%)`;
const standaloneButtonPressedOverlay = `oklch(from ${standaloneButtonForeground} 0 0 h / 10.9804%)`;
const standaloneButtonStyle = {
background: standaloneButtonBackground,
color: standaloneButtonForeground,
"&&[data-state=auto]:hover, &&[data-state=hover]": {
background: `linear-gradient(${standaloneButtonHoverOverlay}, ${standaloneButtonHoverOverlay}), ${standaloneButtonBackground}`,
color: standaloneButtonForeground,
},
"&&[data-state=auto]:focus-visible, &&[data-state=focus]": {
color: standaloneButtonForeground,
outline: `1px solid ${cssVar("--border-focus", "#297aff")}`,
outlineOffset: "1px",
},
"&&[data-state=auto]:active, &&[data-state=pressed]": {
background: `linear-gradient(${standaloneButtonPressedOverlay}, ${standaloneButtonPressedOverlay}), ${standaloneButtonBackground}`,
color: standaloneButtonForeground,
},
};
export const ErrorMessage = ({
error,
}: {
error: {
status: number;
statusText?: string;
message: string;
description?: string;
};
}) => {
return (
<Grid className={pageStyle()} justify={"center"} gap={6}>
<AccessibleIcon label="Logo">
<WebstudioIcon size="60" />
</AccessibleIcon>
<div />
<div />
<Text
css={{
fontSize: theme.spacing[21],
lineHeight: 1,
}}
variant={"bigTitle"}
>
{error.status}
</Text>
<Grid
css={{
justifyItems: "center",
marginInline: theme.spacing[10],
maxWidth: 600,
}}
gap={5}
>
<Grid
css={{
background: `light-dark(
${cssVar("--background-primary", "#fff")},
${cssVar("--background-inverse", "#11181c")}
)`,
padding: theme.spacing[7],
borderRadius: theme.spacing[5],
minWidth: theme.spacing[34],
}}
gap="3"
>
<Text
css={{
display: "-webkit-box",
"-webkit-line-clamp": 4,
"-webkit-box-orient": "vertical",
overflow: "hidden",
textOverflow: "ellipsis",
wordBreak: "break-word",
}}
variant="brandSectionTitle"
userSelect="text"
>
{error.message ?? error.statusText}
</Text>
{error.description && (
<Text
css={{ wordBreak: "break-word", whiteSpace: "pre-line" }}
userSelect="text"
variant="brandRegular"
>
{error.description}
</Text>
)}
</Grid>
<LinkButton color="primary" href="/" css={standaloneButtonStyle}>
Go home
</LinkButton>
</Grid>
</Grid>
);
};