Skip to content

Commit 0ca7e13

Browse files
fix: editor theme and touch targets match design spec (#56) (#57)
- Replace arbitrary oklch color values in code highlighting with named CSS variable tokens (--code-keyword, --code-string, --code-constant, --code-type, --code-builtin) - Remove leading-relaxed override from paragraph theme - Change blockquote border opacity from 0.12 to 0.06 - Increase floating toolbar button touch targets to 44px on mobile - Increase link editor button touch targets to 44px on mobile - Add regression tests for theme color tokens and line height - Document syntax highlighting tokens in design spec Co-authored-by: Ona <no-reply@ona.com>
1 parent 22a8348 commit 0ca7e13

6 files changed

Lines changed: 124 additions & 31 deletions

File tree

.agents/design.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,21 @@ Dark mode only. Use oklch values via CSS variables — same hue family as softwa
3434
| `--accent` | `oklch(0.60 0.06 248)` | Links, selected items, focus rings |
3535
| `--destructive` | `oklch(0.55 0.2 25)` | Delete actions, error states |
3636

37+
### Syntax Highlighting Tokens
38+
39+
Used in the Lexical editor code block theme (`src/components/editor/theme.ts`).
40+
41+
| Token | Value | Usage |
42+
|---|---|---|
43+
| `--code-keyword` | `oklch(0.65 0.12 300)` | Keywords, operators (purple) |
44+
| `--code-string` | `oklch(0.70 0.12 150)` | Strings, chars, selectors (green) |
45+
| `--code-constant` | `oklch(0.65 0.15 25)` | Booleans, numbers, constants, tags (red-orange) |
46+
| `--code-type` | `oklch(0.75 0.08 80)` | Class names, type annotations (yellow) |
47+
| `--code-builtin` | `oklch(0.70 0.08 200)` | Built-in functions and types (cyan) |
48+
49+
Tokens that already map to existing colors: `text-primary` for functions/properties/attrs,
50+
`text-muted-foreground` for comments/punctuation, `text-destructive` for deleted text.
51+
3752
Rules:
3853
- No color outside this token set without updating this file first.
3954
- Accent color is used sparingly — selected sidebar item, focused input, links. Not decorative.

src/app/globals.css

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@
3939
--color-card: var(--card);
4040
--color-foreground: var(--foreground);
4141
--color-background: var(--background);
42+
--color-code-keyword: var(--code-keyword);
43+
--color-code-string: var(--code-string);
44+
--color-code-constant: var(--code-constant);
45+
--color-code-type: var(--code-type);
46+
--color-code-builtin: var(--code-builtin);
4247
--radius-sm: calc(var(--radius) * 0.6);
4348
--radius-md: calc(var(--radius) * 0.8);
4449
--radius-lg: var(--radius);
@@ -82,6 +87,11 @@
8287
--sidebar-accent-foreground: oklch(0.87 0.01 255);
8388
--sidebar-border: oklch(0.22 0.012 255);
8489
--sidebar-ring: oklch(0.60 0.06 248);
90+
--code-keyword: oklch(0.65 0.12 300);
91+
--code-string: oklch(0.70 0.12 150);
92+
--code-constant: oklch(0.65 0.15 25);
93+
--code-type: oklch(0.75 0.08 80);
94+
--code-builtin: oklch(0.70 0.08 200);
8595
}
8696

8797
/* Duplicate for .dark class to satisfy shadcn components that check for it */
@@ -117,6 +127,11 @@
117127
--sidebar-accent-foreground: oklch(0.87 0.01 255);
118128
--sidebar-border: oklch(0.22 0.012 255);
119129
--sidebar-ring: oklch(0.60 0.06 248);
130+
--code-keyword: oklch(0.65 0.12 300);
131+
--code-string: oklch(0.70 0.12 150);
132+
--code-constant: oklch(0.65 0.15 25);
133+
--code-type: oklch(0.75 0.08 80);
134+
--code-builtin: oklch(0.70 0.08 200);
120135
}
121136

122137
@layer base {

src/components/editor/floating-link-editor-plugin.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -203,15 +203,15 @@ export function FloatingLinkEditorPlugin({
203203
/>
204204
<button
205205
type="button"
206-
className="flex h-6 w-6 items-center justify-center text-muted-foreground hover:text-foreground"
206+
className="flex h-11 w-11 sm:h-6 sm:w-6 items-center justify-center text-muted-foreground hover:text-foreground"
207207
onClick={handleSave}
208208
aria-label="Save link"
209209
>
210210
<Check className="h-3.5 w-3.5" />
211211
</button>
212212
<button
213213
type="button"
214-
className="flex h-6 w-6 items-center justify-center text-muted-foreground hover:text-foreground"
214+
className="flex h-11 w-11 sm:h-6 sm:w-6 items-center justify-center text-muted-foreground hover:text-foreground"
215215
onClick={() => {
216216
setIsEditing(false);
217217
setEditedUrl(linkUrl);
@@ -233,23 +233,23 @@ export function FloatingLinkEditorPlugin({
233233
</a>
234234
<button
235235
type="button"
236-
className="flex h-6 w-6 items-center justify-center text-muted-foreground hover:text-foreground"
236+
className="flex h-11 w-11 sm:h-6 sm:w-6 items-center justify-center text-muted-foreground hover:text-foreground"
237237
onClick={() => window.open(linkUrl, "_blank", "noopener")}
238238
aria-label="Open link"
239239
>
240240
<ExternalLink className="h-3.5 w-3.5" />
241241
</button>
242242
<button
243243
type="button"
244-
className="flex h-6 w-6 items-center justify-center text-muted-foreground hover:text-foreground"
244+
className="flex h-11 w-11 sm:h-6 sm:w-6 items-center justify-center text-muted-foreground hover:text-foreground"
245245
onClick={() => setIsEditing(true)}
246246
aria-label="Edit link"
247247
>
248248
<Pencil className="h-3.5 w-3.5" />
249249
</button>
250250
<button
251251
type="button"
252-
className="flex h-6 w-6 items-center justify-center text-destructive hover:text-destructive/80"
252+
className="flex h-11 w-11 sm:h-6 sm:w-6 items-center justify-center text-destructive hover:text-destructive/80"
253253
onClick={handleRemove}
254254
aria-label="Remove link"
255255
>

src/components/editor/floating-toolbar-plugin.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ function ToolbarButton({
224224
return (
225225
<button
226226
type="button"
227-
className={`flex h-7 w-7 items-center justify-center text-sm ${
227+
className={`flex h-11 w-11 sm:h-7 sm:w-7 items-center justify-center text-sm ${
228228
active
229229
? "bg-white/[0.08] text-foreground"
230230
: "text-muted-foreground hover:bg-white/[0.04] hover:text-foreground"
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { describe, it, expect } from "vitest";
2+
import { editorTheme } from "./theme";
3+
4+
/**
5+
* Regression test for issue #56: arbitrary color values in the editor theme
6+
* violate the design spec. All colors must use named design tokens
7+
* (e.g. text-primary, text-muted-foreground) not arbitrary oklch/hex/rgb values.
8+
*/
9+
describe("editorTheme", () => {
10+
const arbitraryColorPattern =
11+
/text-\[(?:oklch|rgb|hsl|#)[^\]]*\]|bg-\[(?:oklch|rgb|hsl|#)[^\]]*\]/;
12+
13+
function collectClassStrings(
14+
obj: Record<string, unknown>,
15+
path = ""
16+
): Array<{ path: string; value: string }> {
17+
const results: Array<{ path: string; value: string }> = [];
18+
for (const [key, value] of Object.entries(obj)) {
19+
const currentPath = path ? `${path}.${key}` : key;
20+
if (typeof value === "string") {
21+
results.push({ path: currentPath, value });
22+
} else if (typeof value === "object" && value !== null) {
23+
if (Array.isArray(value)) {
24+
value.forEach((item, i) => {
25+
if (typeof item === "string") {
26+
results.push({ path: `${currentPath}[${i}]`, value: item });
27+
}
28+
});
29+
} else {
30+
results.push(
31+
...collectClassStrings(
32+
value as Record<string, unknown>,
33+
currentPath
34+
)
35+
);
36+
}
37+
}
38+
}
39+
return results;
40+
}
41+
42+
it("does not contain arbitrary color values", () => {
43+
const allClasses = collectClassStrings(
44+
editorTheme as unknown as Record<string, unknown>
45+
);
46+
const violations = allClasses.filter(({ value }) =>
47+
arbitraryColorPattern.test(value)
48+
);
49+
50+
expect(violations).toEqual([]);
51+
});
52+
53+
it("paragraph does not override line height", () => {
54+
expect(editorTheme.paragraph).toBeDefined();
55+
expect(editorTheme.paragraph).not.toMatch(/leading-/);
56+
});
57+
58+
it("blockquote border uses design-spec opacity", () => {
59+
const quote = editorTheme.quote as string;
60+
expect(quote).toContain("border-white/[0.06]");
61+
expect(quote).not.toContain("border-white/[0.12]");
62+
});
63+
});

src/components/editor/theme.ts

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import type { EditorThemeClasses } from "lexical";
22

33
export const editorTheme: EditorThemeClasses = {
4-
paragraph: "mt-0.5 text-sm text-foreground leading-relaxed",
4+
paragraph: "mt-0.5 text-sm text-foreground",
55
heading: {
66
h1: "mt-6 text-2xl font-semibold text-foreground",
77
h2: "mt-5 text-xl font-semibold text-foreground",
88
h3: "mt-4 text-lg font-medium text-foreground",
99
},
1010
quote:
11-
"mt-3 border-l-2 border-white/[0.12] pl-4 text-sm text-muted-foreground italic",
11+
"mt-3 border-l-2 border-white/[0.06] pl-4 text-sm text-muted-foreground italic",
1212
list: {
1313
ul: "mt-0.5 list-disc pl-6 text-sm",
1414
ol: "mt-0.5 list-decimal pl-6 text-sm",
@@ -23,36 +23,36 @@ export const editorTheme: EditorThemeClasses = {
2323
},
2424
code: "mt-3 block bg-muted rounded-sm p-4 font-mono text-sm overflow-x-auto",
2525
codeHighlight: {
26-
atrule: "text-[oklch(0.74_0.032_248)]",
27-
attr: "text-[oklch(0.74_0.032_248)]",
28-
boolean: "text-[oklch(0.65_0.15_25)]",
29-
builtin: "text-[oklch(0.70_0.08_200)]",
26+
atrule: "text-primary",
27+
attr: "text-primary",
28+
boolean: "text-code-constant",
29+
builtin: "text-code-builtin",
3030
cdata: "text-muted-foreground",
31-
char: "text-[oklch(0.70_0.12_150)]",
32-
class: "text-[oklch(0.75_0.08_80)]",
33-
"class-name": "text-[oklch(0.75_0.08_80)]",
31+
char: "text-code-string",
32+
class: "text-code-type",
33+
"class-name": "text-code-type",
3434
comment: "text-muted-foreground",
35-
constant: "text-[oklch(0.65_0.15_25)]",
35+
constant: "text-code-constant",
3636
deleted: "text-destructive",
3737
doctype: "text-muted-foreground",
38-
entity: "text-[oklch(0.65_0.15_25)]",
39-
function: "text-[oklch(0.74_0.032_248)]",
40-
important: "text-[oklch(0.65_0.15_25)]",
41-
inserted: "text-[oklch(0.70_0.12_150)]",
42-
keyword: "text-[oklch(0.65_0.12_300)]",
38+
entity: "text-code-constant",
39+
function: "text-primary",
40+
important: "text-code-constant",
41+
inserted: "text-code-string",
42+
keyword: "text-code-keyword",
4343
namespace: "text-muted-foreground",
44-
number: "text-[oklch(0.65_0.15_25)]",
45-
operator: "text-[oklch(0.65_0.12_300)]",
44+
number: "text-code-constant",
45+
operator: "text-code-keyword",
4646
prolog: "text-muted-foreground",
47-
property: "text-[oklch(0.74_0.032_248)]",
47+
property: "text-primary",
4848
punctuation: "text-muted-foreground",
49-
regex: "text-[oklch(0.65_0.15_25)]",
50-
selector: "text-[oklch(0.70_0.12_150)]",
51-
string: "text-[oklch(0.70_0.12_150)]",
52-
symbol: "text-[oklch(0.65_0.15_25)]",
53-
tag: "text-[oklch(0.65_0.15_25)]",
54-
url: "text-[oklch(0.74_0.032_248)]",
55-
variable: "text-[oklch(0.65_0.15_25)]",
49+
regex: "text-code-constant",
50+
selector: "text-code-string",
51+
string: "text-code-string",
52+
symbol: "text-code-constant",
53+
tag: "text-code-constant",
54+
url: "text-primary",
55+
variable: "text-code-constant",
5656
},
5757
text: {
5858
bold: "font-bold",

0 commit comments

Comments
 (0)