fix(web): restyle the custom CSS editor in the app's inks and show its toolbar icons

This commit is contained in:
Amruth Pillai
2026-09-29 08:17:12 +02:00
parent 49422e98f2
commit 5dc67c3bd4
8 changed files with 36 additions and 17 deletions
+1
View File
@@ -34,6 +34,7 @@
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@fontsource-variable/manrope": "^5.3.0",
"@lezer/highlight": "^1.2.4",
"@lingui/core": "^6.8.0",
"@lingui/react": "^6.8.0",
"@orpc/client": "^1.15.4",
@@ -6,7 +6,7 @@ import type { SemanticCssColorToken } from "./color-tokens";
import type { SemanticCssEditorMetadata } from "./protocol";
import { defaultKeymap, indentWithTab } from "@codemirror/commands";
import { css } from "@codemirror/lang-css";
import { defaultHighlightStyle, syntaxHighlighting } from "@codemirror/language";
import { HighlightStyle, syntaxHighlighting } from "@codemirror/language";
import { Annotation, Compartment, EditorState, Prec, Transaction } from "@codemirror/state";
import {
drawSelection,
@@ -16,6 +16,7 @@ import {
keymap,
lineNumbers,
} from "@codemirror/view";
import { tags } from "@lezer/highlight";
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
@@ -54,6 +55,18 @@ type EditorCompartments = {
intelligence: Compartment;
};
/** CSS in the app's own inks, so it reads the same in light and dark. */
const highlightStyle = HighlightStyle.define([
{ tag: [tags.comment, tags.meta], color: "var(--ink-3)", fontStyle: "italic" },
{ tag: [tags.tagName, tags.className, tags.labelName], color: "var(--accent-text)" },
{ tag: [tags.propertyName, tags.attributeName], color: "var(--info-text)" },
{ tag: [tags.string, tags.number, tags.unit, tags.color, tags.atom], color: "var(--warn-text)" },
{ tag: [tags.keyword, tags.modifier, tags.definitionKeyword, tags.controlKeyword], color: "var(--danger-text)" },
{ tag: [tags.variableName, tags.function(tags.variableName)], color: "var(--ink)", fontWeight: "500" },
{ tag: [tags.punctuation, tags.operator, tags.bracket], color: "var(--ink-2)" },
{ tag: tags.invalid, color: "var(--danger-text)", textDecoration: "underline wavy" },
]);
const editorTheme = (dark: boolean): Extension =>
EditorView.theme(
{
@@ -65,7 +78,7 @@ const editorTheme = (dark: boolean): Extension =>
},
".cm-scroller": {
overflow: "auto",
fontFamily: '"JetBrains Mono Variable", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
fontFamily: "var(--font-mono)",
lineHeight: "1.5",
},
".cm-content": { minHeight: "100%", padding: "0.75rem 0" },
@@ -162,7 +175,7 @@ export function StylesheetCodeEditor({
drawSelection(),
highlightActiveLine(),
css(),
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
syntaxHighlighting(highlightStyle),
EditorView.editorAttributes.of({ dir: "ltr" }),
EditorView.contentAttributes.of({ "aria-label": initial.label, dir: "ltr", spellcheck: "false" }),
Prec.high(
@@ -21,7 +21,7 @@ export function LegacyStylesheetBanner({ disabled, onActivate }: LegacyStyleshee
</p>
<Button type="button" size="sm" disabled={disabled} onClick={onActivate}>
<Trans>Activate Semantic CSS</Trans>
<Icon name="arrow_forward" size={16} data-icon="inline-end" />
<Icon name="arrow_forward" size={16} />
</Button>
</AlertDescription>
</Alert>
@@ -23,19 +23,19 @@ export function StylesheetStatus({ mode, status, diagnostics }: StylesheetStatus
<div className="space-y-2" aria-live="polite">
{hasFatalErrors ? (
<Badge variant="danger">
<Icon name="error" size={16} data-icon="inline-start" />
<Icon name="error" size={16} />
<Trans>Fatal error</Trans>
</Badge>
) : isPending ? (
<Badge variant="outline">{mode === "legacy" ? <Trans>Checking draft</Trans> : <Trans>Checking</Trans>}</Badge>
) : hasRecoverableErrors ? (
<Badge variant="warn">
<Icon name="error" size={16} data-icon="inline-start" />
<Icon name="error" size={16} />
{mode === "legacy" ? <Trans>Ready to activate with errors</Trans> : <Trans>Valid with errors</Trans>}
</Badge>
) : warnings.length > 0 ? (
<Badge variant="warn">
<Icon name="warning" size={16} data-icon="inline-start" />
<Icon name="warning" size={16} />
{mode === "legacy" ? <Trans>Ready to activate with warnings</Trans> : <Trans>Valid with warnings</Trans>}
</Badge>
) : (
@@ -52,23 +52,19 @@ export function StylesheetToolbar({
return (
<div className="flex flex-wrap items-center gap-1" role="toolbar" aria-label={t`Stylesheet editor`}>
<ToolbarButton label={t`Undo stylesheet edit`} disabled={disabled || !canUndo} onClick={onUndo}>
<Icon name="undo" size={16} data-icon="inline-start" />
<Icon name="undo" size={16} />
</ToolbarButton>
<ToolbarButton label={t`Redo stylesheet edit`} disabled={disabled || !canRedo} onClick={onRedo}>
<Icon name="redo" size={16} data-icon="inline-start" />
<Icon name="redo" size={16} />
</ToolbarButton>
<ToolbarButton label={t`Copy stylesheet`} onClick={() => void copySourceToClipboard(source)}>
<Icon name="content_copy" size={16} data-icon="inline-start" />
<Icon name="content_copy" size={16} />
</ToolbarButton>
<ToolbarButton label={t`Format stylesheet`} disabled={disabled} onClick={onFormat}>
<Icon name="auto_fix_high" size={16} data-icon="inline-start" />
<Icon name="auto_fix_high" size={16} />
</ToolbarButton>
<ToolbarButton label={focused ? t`Exit focus mode` : t`Open focus mode`} onClick={onFocusToggle}>
{focused ? (
<Icon name="close_fullscreen" size={16} data-icon="inline-start" />
) : (
<Icon name="open_in_full" size={16} data-icon="inline-start" />
)}
{focused ? <Icon name="close_fullscreen" size={16} /> : <Icon name="open_in_full" size={16} />}
</ToolbarButton>
</div>
);
+5
View File
@@ -19,6 +19,11 @@ describe("Icon", () => {
expect(container.querySelector("button")).toHaveTextContent(/^Search$/);
});
it("keeps its glyph when a caller passes its own data-icon", () => {
const { container } = render(<Icon name="undo" {...{ "data-icon": "inline-start" }} />);
expect(container.querySelector('[data-slot="icon"]')).toHaveAttribute("data-icon", "undo");
});
it("uses the filled glyph only when asked", () => {
const { container } = render(<Icon name="description" filled />);
expect(container.querySelector('[data-slot="icon"]')?.getAttribute("style")).toContain('"FILL" 1');
+2 -1
View File
@@ -29,6 +29,8 @@ function Icon({ name, filled = false, size = 20, className, style, ...props }: I
<span
aria-hidden="true"
translate="no"
{...props}
// After the props: `data-icon` is the glyph, so a stray one never blanks it.
data-slot="icon"
data-icon={name}
className={cn("material-symbol", DIRECTIONAL_ICONS.has(name) && "rtl:-scale-x-100", className)}
@@ -37,7 +39,6 @@ function Icon({ name, filled = false, size = 20, className, style, ...props }: I
fontVariationSettings: `"FILL" ${filled ? 1 : 0}, "wght" 300, "GRAD" 0, "opsz" ${size >= 24 ? 24 : 20}`,
...style,
}}
{...props}
/>
);
}
+3
View File
@@ -549,6 +549,9 @@ importers:
'@fontsource-variable/manrope':
specifier: ^5.3.0
version: 5.3.0
'@lezer/highlight':
specifier: ^1.2.4
version: 1.2.4
'@lingui/core':
specifier: ^6.8.0
version: 6.8.0(@babel/core@8.0.6)(babel-plugin-macros@3.1.0)