mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
feat(studio): the code editor follows studio's light and dark theme (#4862)
* feat(studio): the code editor follows studio's light and dark theme Replaces One Dark with a Studio editor theme whose colours are theme.css variables: eight syntax role tokens (One Light and One Dark hues held at 4.5:1), editor surfaces from Studio's roles, all measured by the contrast ratchet. * test(studio): the code editor theme takes every colour from a theme.css token * fix(studio): code stays at 4.5:1 when a selection or search match sits on the active line Syntax tokens are tuned for the selection over the active line, which the ratchet now measures; matches are outlined rather than stacking another wash; the search panel's pressed button and special characters follow the theme; unused tooltip rules go. * fix(studio): code matches carry no background, and types read apart from constants Search and selection matches are outlined only, so CodeMirror's lime match wash and stacked washes are gone; type and constant tokens split by hue in both themes. * fix(studio): the current search match reads apart by width, not colour alone A 2px accent outline on the current match and a muted 1px outline on other copies of the selected word.
This commit is contained in:
@@ -329,7 +329,6 @@
|
||||
"@codemirror/language": "^6.12.2",
|
||||
"@codemirror/search": "^6.6.0",
|
||||
"@codemirror/state": "^6.6.0",
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@codemirror/view": "6.40.0",
|
||||
"@hyperframes/core": "workspace:*",
|
||||
"@hyperframes/parsers": "workspace:*",
|
||||
@@ -337,6 +336,7 @@
|
||||
"@hyperframes/sdk": "workspace:*",
|
||||
"@hyperframes/shader-transitions": "workspace:*",
|
||||
"@hyperframes/studio-server": "workspace:*",
|
||||
"@lezer/highlight": "^1.2.3",
|
||||
"@mcp-b/global": "^5.0.1",
|
||||
"@phosphor-icons/react": "^2.1.10",
|
||||
"@tanstack/react-virtual": "^3.14.13",
|
||||
@@ -552,8 +552,6 @@
|
||||
|
||||
"@codemirror/state": ["@codemirror/state@6.6.0", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ=="],
|
||||
|
||||
"@codemirror/theme-one-dark": ["@codemirror/theme-one-dark@6.1.3", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/highlight": "^1.0.0" } }, "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA=="],
|
||||
|
||||
"@codemirror/view": ["@codemirror/view@6.40.0", "", { "dependencies": { "@codemirror/state": "^6.6.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-WA0zdU7xfF10+5I3HhUUq3kqOx3KjqmtQ9lqZjfK7jtYk4G72YW9rezcSywpaUMCWOMlq+6E0pO1IWg1TNIhtg=="],
|
||||
|
||||
"@commitlint/cli": ["@commitlint/cli@20.5.3", "", { "dependencies": { "@commitlint/format": "^20.5.0", "@commitlint/lint": "^20.5.3", "@commitlint/load": "^20.5.3", "@commitlint/read": "^20.5.0", "@commitlint/types": "^20.5.0", "tinyexec": "^1.0.0", "yargs": "^17.0.0" }, "bin": { "commitlint": "./cli.js" } }, "sha512-OJdL0EXWD5y9LPa0nr/geOwzaS8BsdaybKkcloB0JgsguGxNv2R+hC2FTPqrAcprg35zF33KOQerY0x8W1aesA=="],
|
||||
|
||||
@@ -93,7 +93,6 @@
|
||||
"@codemirror/language": "^6.12.2",
|
||||
"@codemirror/search": "^6.6.0",
|
||||
"@codemirror/state": "^6.6.0",
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@codemirror/view": "6.40.0",
|
||||
"@hyperframes/core": "workspace:*",
|
||||
"@hyperframes/parsers": "workspace:*",
|
||||
@@ -101,6 +100,7 @@
|
||||
"@hyperframes/sdk": "workspace:*",
|
||||
"@hyperframes/shader-transitions": "workspace:*",
|
||||
"@hyperframes/studio-server": "workspace:*",
|
||||
"@lezer/highlight": "^1.2.3",
|
||||
"@mcp-b/global": "^5.0.1",
|
||||
"@phosphor-icons/react": "^2.1.10",
|
||||
"@tanstack/react-virtual": "^3.14.13",
|
||||
|
||||
@@ -11,12 +11,12 @@ import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
|
||||
import { bracketMatching, foldGutter, indentOnInput } from "@codemirror/language";
|
||||
import { closeBrackets, closeBracketsKeymap } from "@codemirror/autocomplete";
|
||||
import { highlightSelectionMatches, searchKeymap } from "@codemirror/search";
|
||||
import { oneDark } from "@codemirror/theme-one-dark";
|
||||
import type { Extension } from "@codemirror/state";
|
||||
import { html } from "@codemirror/lang-html";
|
||||
import { css } from "@codemirror/lang-css";
|
||||
import { javascript } from "@codemirror/lang-javascript";
|
||||
import { markdown } from "@codemirror/lang-markdown";
|
||||
import { studioCodeTheme } from "./studioCodeTheme";
|
||||
|
||||
// Marks a programmatic doc sync (external content push — e.g. a manual-edit
|
||||
// commit writing the source) so the update listener doesn't mistake it for a
|
||||
@@ -114,13 +114,9 @@ export const SourceEditor = memo(function SourceEditor({
|
||||
highlightSelectionMatches(),
|
||||
keymap.of([...closeBracketsKeymap, ...defaultKeymap, ...searchKeymap, ...historyKeymap]),
|
||||
getLanguageExtension(lang),
|
||||
oneDark,
|
||||
studioCodeTheme,
|
||||
updateListener,
|
||||
EditorState.readOnly.of(readOnly),
|
||||
EditorView.theme({
|
||||
"&": { height: "100%" },
|
||||
".cm-scroller": { overflow: "auto" },
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const read = (file: string) => readFileSync(new URL(file, import.meta.url), "utf8");
|
||||
const source = read("./studioCodeTheme.ts");
|
||||
const themeCss = read("../../styles/theme.css");
|
||||
|
||||
describe("the code editor theme", () => {
|
||||
it("takes every colour from a token theme.css declares", () => {
|
||||
const used = [...source.matchAll(/var\((--color-[\w-]+)\)/g)].map((match) => match[1]!);
|
||||
expect(used.length).toBeGreaterThan(20);
|
||||
for (const token of new Set(used)) expect(themeCss, token).toContain(`${token}:`);
|
||||
});
|
||||
|
||||
it("paints no literal colour of its own", () => {
|
||||
expect(source).not.toMatch(
|
||||
/#[\da-f]{3,8}\b|\b(rgba?|hsla?|hwb|lab|lch|oklab|oklch|color|color-mix)\(|"(white|black|red|orange|yellow|green|blue|purple|pink|gray|grey|silver|lime|cyan|magenta)"/i,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
import { HighlightStyle, syntaxHighlighting } from "@codemirror/language";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { tags as t } from "@lezer/highlight";
|
||||
|
||||
// One Dark's layout on Studio's tokens: every colour is a theme.css variable, so the editor
|
||||
// follows the light or dark theme the page paints with no editor reconfiguration.
|
||||
const editorTheme = EditorView.theme({
|
||||
"&": { height: "100%", color: "var(--color-text-0)", backgroundColor: "var(--color-surface)" },
|
||||
".cm-scroller": { overflow: "auto" },
|
||||
".cm-content": { caretColor: "var(--color-text-0)" },
|
||||
".cm-cursor, .cm-dropCursor": { borderLeftColor: "var(--color-text-0)" },
|
||||
"&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection":
|
||||
{ backgroundColor: "var(--color-on)" },
|
||||
".cm-activeLine": { backgroundColor: "var(--color-hover)" },
|
||||
".cm-selectionMatch": {
|
||||
backgroundColor: "transparent",
|
||||
outline: "1px solid var(--color-text-muted)",
|
||||
},
|
||||
".cm-searchMatch": {
|
||||
backgroundColor: "transparent",
|
||||
outline: "1px solid var(--color-text-muted)",
|
||||
},
|
||||
".cm-searchMatch.cm-searchMatch-selected": {
|
||||
backgroundColor: "transparent",
|
||||
outline: "2px solid var(--color-accent-ink)",
|
||||
},
|
||||
"&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket": {
|
||||
backgroundColor: "var(--color-on)",
|
||||
},
|
||||
".cm-gutters": {
|
||||
backgroundColor: "var(--color-surface)",
|
||||
color: "var(--color-text-muted)",
|
||||
border: "none",
|
||||
},
|
||||
".cm-activeLineGutter": { backgroundColor: "var(--color-hover)", color: "var(--color-text-2)" },
|
||||
".cm-foldPlaceholder": {
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
color: "var(--color-text-2)",
|
||||
},
|
||||
".cm-panels": { backgroundColor: "var(--color-raised)", color: "var(--color-text-0)" },
|
||||
".cm-panels.cm-panels-top": { borderBottom: "1px solid var(--color-border)" },
|
||||
".cm-panels.cm-panels-bottom": { borderTop: "1px solid var(--color-border)" },
|
||||
".cm-textfield": {
|
||||
backgroundColor: "var(--color-input)",
|
||||
color: "var(--color-text-0)",
|
||||
border: "1px solid var(--color-border-input)",
|
||||
},
|
||||
".cm-button": {
|
||||
backgroundImage: "none",
|
||||
backgroundColor: "var(--color-hover)",
|
||||
color: "var(--color-text-0)",
|
||||
border: "1px solid var(--color-border)",
|
||||
},
|
||||
"&.cm-editor .cm-button:active": {
|
||||
backgroundImage: "none",
|
||||
backgroundColor: "var(--color-press)",
|
||||
},
|
||||
".cm-specialChar": { color: "var(--color-danger-ink)" },
|
||||
});
|
||||
|
||||
const highlightStyle = HighlightStyle.define([
|
||||
{ tag: t.keyword, color: "var(--color-code-keyword)" },
|
||||
{
|
||||
tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName],
|
||||
color: "var(--color-code-name)",
|
||||
},
|
||||
{ tag: [t.function(t.variableName), t.labelName], color: "var(--color-code-function)" },
|
||||
{ tag: [t.color, t.constant(t.name), t.standard(t.name)], color: "var(--color-code-constant)" },
|
||||
{ tag: [t.definition(t.name), t.separator], color: "var(--color-text-0)" },
|
||||
{
|
||||
tag: [
|
||||
t.typeName,
|
||||
t.className,
|
||||
t.number,
|
||||
t.changed,
|
||||
t.annotation,
|
||||
t.modifier,
|
||||
t.self,
|
||||
t.namespace,
|
||||
],
|
||||
color: "var(--color-code-type)",
|
||||
},
|
||||
{
|
||||
tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.special(t.string)],
|
||||
color: "var(--color-code-operator)",
|
||||
},
|
||||
{ tag: [t.meta, t.comment], color: "var(--color-code-comment)" },
|
||||
{ tag: t.strong, fontWeight: "bold" },
|
||||
{ tag: t.emphasis, fontStyle: "italic" },
|
||||
{ tag: t.strikethrough, textDecoration: "line-through" },
|
||||
{ tag: t.link, color: "var(--color-code-comment)", textDecoration: "underline" },
|
||||
{ tag: t.heading, fontWeight: "bold", color: "var(--color-code-name)" },
|
||||
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: "var(--color-code-constant)" },
|
||||
{ tag: [t.processingInstruction, t.string, t.inserted], color: "var(--color-code-string)" },
|
||||
{ tag: t.invalid, color: "var(--color-danger-ink)" },
|
||||
]);
|
||||
|
||||
export const studioCodeTheme = [editorTheme, syntaxHighlighting(highlightStyle)];
|
||||
@@ -77,6 +77,16 @@
|
||||
--color-media: light-dark(oklch(0.5 0.1 215), #00e3ff);
|
||||
--color-container: light-dark(oklch(0.55 0.13 70), #f5a623);
|
||||
|
||||
/* Code editor syntax: One Light and One Dark hues, held at 4.5:1 on a line, the active line and a selection on it. */
|
||||
--color-code-keyword: light-dark(oklch(0.509 0.211 329), oklch(0.829 0.164 318));
|
||||
--color-code-name: light-dark(oklch(0.504 0.179 28), oklch(0.856 0.145 17));
|
||||
--color-code-function: light-dark(oklch(0.492 0.193 263), oklch(0.805 0.121 245));
|
||||
--color-code-constant: light-dark(oklch(0.495 0.13 50), oklch(0.815 0.11 50));
|
||||
--color-code-type: light-dark(oklch(0.48 0.12 92), oklch(0.805 0.1 92));
|
||||
--color-code-operator: light-dark(oklch(0.471 0.127 237), oklch(0.793 0.092 206));
|
||||
--color-code-comment: light-dark(oklch(0.48 0.009 279), oklch(0.806 0.03 262));
|
||||
--color-code-string: light-dark(oklch(0.468 0.142 143), oklch(0.793 0.11 133));
|
||||
|
||||
/* The two stops of the HeyGen brand gradient, worn by the header logo. Fixed brand values
|
||||
* rather than roles, so they are not reachable as a background or text colour by mistake. */
|
||||
--color-brand-gradient-from: #06e3fa;
|
||||
|
||||
@@ -23,11 +23,11 @@ export default defineConfig({
|
||||
"@codemirror/language",
|
||||
"@codemirror/search",
|
||||
"@codemirror/state",
|
||||
"@codemirror/theme-one-dark",
|
||||
"@codemirror/view",
|
||||
"@hyperframes/core",
|
||||
"@hyperframes/player",
|
||||
"@hyperframes/sdk",
|
||||
"@lezer/highlight",
|
||||
"@phosphor-icons/react",
|
||||
"bpm-detective",
|
||||
"mediabunny",
|
||||
|
||||
@@ -435,6 +435,70 @@
|
||||
"role": "text",
|
||||
"minimum": 4.5,
|
||||
"sources": ["packages/studio/src/styles/theme.css"]
|
||||
},
|
||||
{
|
||||
"id": "code-keyword",
|
||||
"foreground": "--color-code-keyword",
|
||||
"role": "text",
|
||||
"minimum": 4.5,
|
||||
"sources": ["packages/studio/src/components/editor/studioCodeTheme.ts"],
|
||||
"on": ["panel", "panel-hover", "panel-on", "panel-hover-on"]
|
||||
},
|
||||
{
|
||||
"id": "code-name",
|
||||
"foreground": "--color-code-name",
|
||||
"role": "text",
|
||||
"minimum": 4.5,
|
||||
"sources": ["packages/studio/src/components/editor/studioCodeTheme.ts"],
|
||||
"on": ["panel", "panel-hover", "panel-on", "panel-hover-on"]
|
||||
},
|
||||
{
|
||||
"id": "code-function",
|
||||
"foreground": "--color-code-function",
|
||||
"role": "text",
|
||||
"minimum": 4.5,
|
||||
"sources": ["packages/studio/src/components/editor/studioCodeTheme.ts"],
|
||||
"on": ["panel", "panel-hover", "panel-on", "panel-hover-on"]
|
||||
},
|
||||
{
|
||||
"id": "code-constant",
|
||||
"foreground": "--color-code-constant",
|
||||
"role": "text",
|
||||
"minimum": 4.5,
|
||||
"sources": ["packages/studio/src/components/editor/studioCodeTheme.ts"],
|
||||
"on": ["panel", "panel-hover", "panel-on", "panel-hover-on"]
|
||||
},
|
||||
{
|
||||
"id": "code-type",
|
||||
"foreground": "--color-code-type",
|
||||
"role": "text",
|
||||
"minimum": 4.5,
|
||||
"sources": ["packages/studio/src/components/editor/studioCodeTheme.ts"],
|
||||
"on": ["panel", "panel-hover", "panel-on", "panel-hover-on"]
|
||||
},
|
||||
{
|
||||
"id": "code-operator",
|
||||
"foreground": "--color-code-operator",
|
||||
"role": "text",
|
||||
"minimum": 4.5,
|
||||
"sources": ["packages/studio/src/components/editor/studioCodeTheme.ts"],
|
||||
"on": ["panel", "panel-hover", "panel-on", "panel-hover-on"]
|
||||
},
|
||||
{
|
||||
"id": "code-comment",
|
||||
"foreground": "--color-code-comment",
|
||||
"role": "text",
|
||||
"minimum": 4.5,
|
||||
"sources": ["packages/studio/src/components/editor/studioCodeTheme.ts"],
|
||||
"on": ["panel", "panel-hover", "panel-on", "panel-hover-on"]
|
||||
},
|
||||
{
|
||||
"id": "code-string",
|
||||
"foreground": "--color-code-string",
|
||||
"role": "text",
|
||||
"minimum": 4.5,
|
||||
"sources": ["packages/studio/src/components/editor/studioCodeTheme.ts"],
|
||||
"on": ["panel", "panel-hover", "panel-on", "panel-hover-on"]
|
||||
}
|
||||
],
|
||||
"surfaces": {
|
||||
@@ -447,7 +511,8 @@
|
||||
"panel-pressed": ["--color-surface", "--color-press"],
|
||||
"panel-on": ["--color-surface", "--color-on"],
|
||||
"panel-on-hover": ["--color-surface", "--color-on-hover"],
|
||||
"raised-on-hover": ["--color-raised", "--color-on-hover"]
|
||||
"raised-on-hover": ["--color-raised", "--color-on-hover"],
|
||||
"panel-hover-on": ["--color-surface", "--color-hover", "--color-on"]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user