Skip to content

Commit d91b5c3

Browse files
committed
Merge dev — editor theme/font picker
2 parents 15100df + 1b88c1c commit d91b5c3

9 files changed

Lines changed: 274 additions & 12 deletions

File tree

apps/web/app/layout.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Metadata } from "next"
2-
import { Geist_Mono, Inter } from "next/font/google"
2+
import { Geist_Mono, Inter, JetBrains_Mono, Fira_Code, IBM_Plex_Mono } from "next/font/google"
33
import { ClerkProvider } from "@clerk/nextjs"
44

55
import "./globals.css"
@@ -15,6 +15,9 @@ export const metadata: Metadata = {
1515
const geistMonoHeading = Geist_Mono({ subsets: ["latin"], variable: "--font-heading" })
1616
const inter = Inter({ subsets: ["latin"], variable: "--font-sans" })
1717
const fontMono = Geist_Mono({ subsets: ["latin"], variable: "--font-mono" })
18+
const jetbrainsMono = JetBrains_Mono({ subsets: ["latin"], variable: "--font-jetbrains", weight: ["400", "500"] })
19+
const firaCode = Fira_Code({ subsets: ["latin"], variable: "--font-fira", weight: ["400", "500"] })
20+
const ibmPlexMono = IBM_Plex_Mono({ subsets: ["latin"], variable: "--font-ibm-plex", weight: ["400", "500"] })
1821

1922
export default function RootLayout({
2023
children,
@@ -26,7 +29,15 @@ export default function RootLayout({
2629
<html
2730
lang="en"
2831
suppressHydrationWarning
29-
className={cn("antialiased", fontMono.variable, "font-sans", inter.variable, geistMonoHeading.variable)}
32+
className={cn(
33+
"antialiased font-sans",
34+
fontMono.variable,
35+
inter.variable,
36+
geistMonoHeading.variable,
37+
jetbrainsMono.variable,
38+
firaCode.variable,
39+
ibmPlexMono.variable,
40+
)}
3041
>
3142
<body>
3243
<Providers>
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
"use client"
2+
3+
import { HugeiconsIcon } from "@hugeicons/react"
4+
import { ColorsIcon, TextFontIcon } from "@hugeicons/core-free-icons"
5+
import {
6+
Select,
7+
SelectContent,
8+
SelectGroup,
9+
SelectItem,
10+
SelectTrigger,
11+
SelectValue,
12+
} from "@/components/ui/select"
13+
import { EDITOR_THEMES, EDITOR_FONTS, type EditorTheme, type EditorFont } from "@/lib/editor-prefs"
14+
15+
interface EditorToolbarProps {
16+
theme: EditorTheme
17+
font: EditorFont
18+
onThemeChange: (theme: EditorTheme) => void
19+
onFontChange: (font: EditorFont) => void
20+
}
21+
22+
export function EditorToolbar({ theme, font, onThemeChange, onFontChange }: EditorToolbarProps) {
23+
return (
24+
<div className="flex items-center gap-0.5 border-b border-border/50 bg-muted/20 px-2.5 py-1.5">
25+
{/* Theme picker */}
26+
<div className="flex items-center gap-1">
27+
<HugeiconsIcon
28+
icon={ColorsIcon}
29+
size={12}
30+
strokeWidth={2}
31+
className="shrink-0 text-muted-foreground/50"
32+
/>
33+
<Select value={theme} onValueChange={(v) => onThemeChange(v as EditorTheme)}>
34+
<SelectTrigger className="h-6 gap-1 rounded-lg border-0 bg-transparent px-1.5 text-[11px] font-medium text-muted-foreground shadow-none ring-0 hover:bg-muted/60 hover:text-foreground data-[size=sm]:h-6">
35+
<SelectValue />
36+
</SelectTrigger>
37+
<SelectContent>
38+
<SelectGroup>
39+
{EDITOR_THEMES.map((t) => (
40+
<SelectItem key={t.value} value={t.value} className="text-xs">
41+
{t.label}
42+
</SelectItem>
43+
))}
44+
</SelectGroup>
45+
</SelectContent>
46+
</Select>
47+
</div>
48+
49+
{/* Divider */}
50+
<div className="mx-1 h-3 w-px shrink-0 bg-border" />
51+
52+
{/* Font picker */}
53+
<div className="flex items-center gap-1">
54+
<HugeiconsIcon
55+
icon={TextFontIcon}
56+
size={12}
57+
strokeWidth={2}
58+
className="shrink-0 text-muted-foreground/50"
59+
/>
60+
<Select value={font} onValueChange={(v) => onFontChange(v as EditorFont)}>
61+
<SelectTrigger className="h-6 gap-1 rounded-lg border-0 bg-transparent px-1.5 text-[11px] font-medium text-muted-foreground shadow-none ring-0 hover:bg-muted/60 hover:text-foreground data-[size=sm]:h-6">
62+
<SelectValue />
63+
</SelectTrigger>
64+
<SelectContent>
65+
<SelectGroup>
66+
{EDITOR_FONTS.map((f) => (
67+
<SelectItem key={f.value} value={f.value} className="text-xs">
68+
{f.label}
69+
</SelectItem>
70+
))}
71+
</SelectGroup>
72+
</SelectContent>
73+
</Select>
74+
</div>
75+
</div>
76+
)
77+
}

apps/web/components/review-card.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
import { useState } from "react"
44
import CodeMirror from "@uiw/react-codemirror"
5-
import { githubDark } from "@uiw/codemirror-theme-github"
5+
import { getThemeExtension, getFontCss, cleanGutter } from "@/lib/editor-prefs"
6+
import { useEditorPrefs } from "@/hooks/use-editor-prefs"
67
import { cn } from "@/lib/utils"
78
import { getLanguageExtension, type Language } from "@/lib/languages"
89
import { Button } from "@/components/ui/button"
@@ -19,6 +20,7 @@ interface ReviewCardProps {
1920
export function ReviewCard({ snippet, index, total, onRate, submittingRating }: ReviewCardProps) {
2021
const isSubmitting = submittingRating !== null
2122
const [revealed, setReveal] = useState(false)
23+
const { prefs } = useEditorPrefs()
2224

2325
return (
2426
<div className="flex flex-col gap-4">
@@ -88,11 +90,12 @@ export function ReviewCard({ snippet, index, total, onRate, submittingRating }:
8890
<div className={cn("min-h-48 transition-[filter] duration-300", !revealed && "blur-sm select-none pointer-events-none")}>
8991
<CodeMirror
9092
value={snippet.code}
91-
theme={githubDark}
93+
theme={getThemeExtension(prefs.theme)}
9294
extensions={[
9395
...(getLanguageExtension(snippet.language as Language)
9496
? [getLanguageExtension(snippet.language as Language)!]
9597
: []),
98+
cleanGutter,
9699
]}
97100
editable={false}
98101
basicSetup={{
@@ -102,7 +105,7 @@ export function ReviewCard({ snippet, index, total, onRate, submittingRating }:
102105
autocompletion: false,
103106
}}
104107
className="text-sm"
105-
style={{ fontFamily: "var(--font-mono, ui-monospace, monospace)" }}
108+
style={{ fontFamily: getFontCss(prefs.font) }}
106109
/>
107110
</div>
108111
</div>

apps/web/components/snippet-detail-client.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import { useRouter } from "next/navigation"
55
import { useAuth } from "@clerk/nextjs"
66
import { useQueryClient } from "@tanstack/react-query"
77
import CodeMirror from "@uiw/react-codemirror"
8-
import { githubDark } from "@uiw/codemirror-theme-github"
8+
import { getThemeExtension, getFontCss, cleanGutter } from "@/lib/editor-prefs"
9+
import { useEditorPrefs } from "@/hooks/use-editor-prefs"
10+
import { EditorToolbar } from "@/components/editor-toolbar"
911
import { HugeiconsIcon } from "@hugeicons/react"
1012
import { Edit01Icon, Delete01Icon } from "@hugeicons/core-free-icons"
1113
import { Button } from "@/components/ui/button"
@@ -128,6 +130,7 @@ function EditDialog({
128130
onOpenChange: (open: boolean) => void
129131
onSaved: () => void
130132
}) {
133+
const { prefs, updatePrefs } = useEditorPrefs()
131134
const [title, setTitle] = useState(snippet.title)
132135
const [description, setDescription] = useState(snippet.description ?? "")
133136
const [language, setLanguage] = useState<Language>(snippet.language as Language)
@@ -237,12 +240,19 @@ function EditDialog({
237240
<div className="flex flex-col gap-1.5">
238241
<Label className="text-xs text-muted-foreground">Code</Label>
239242
<div className="overflow-hidden rounded-2xl border border-border">
243+
<EditorToolbar
244+
theme={prefs.theme}
245+
font={prefs.font}
246+
onThemeChange={(theme) => updatePrefs({ theme })}
247+
onFontChange={(font) => updatePrefs({ font })}
248+
/>
240249
<CodeMirror
241250
value={code}
242251
onChange={setCode}
243-
theme={githubDark}
252+
theme={getThemeExtension(prefs.theme)}
244253
extensions={[
245254
...(getLanguageExtension(language) ? [getLanguageExtension(language)!] : []),
255+
cleanGutter,
246256
]}
247257
basicSetup={{
248258
lineNumbers: true,
@@ -252,7 +262,7 @@ function EditDialog({
252262
}}
253263
className="text-sm"
254264
style={{
255-
fontFamily: "var(--font-mono, ui-monospace, monospace)",
265+
fontFamily: getFontCss(prefs.font),
256266
maxHeight: "280px",
257267
overflowY: "auto",
258268
}}

apps/web/components/snippet-editor.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import { useState } from "react"
44
import { useRouter } from "next/navigation"
55
import { useForm } from "@tanstack/react-form"
66
import CodeMirror from "@uiw/react-codemirror"
7-
import { githubDark } from "@uiw/codemirror-theme-github"
7+
import { getThemeExtension, getFontCss, cleanGutter } from "@/lib/editor-prefs"
8+
import { useEditorPrefs } from "@/hooks/use-editor-prefs"
9+
import { EditorToolbar } from "@/components/editor-toolbar"
810
import { cn } from "@/lib/utils"
911
import { LANGUAGES, getLanguageExtension, type Language } from "@/lib/languages"
1012
import { TagInput } from "@/components/tag-input"
@@ -39,6 +41,7 @@ export function SnippetEditor() {
3941
const router = useRouter()
4042
const createSnippet = useCreateSnippet()
4143
const { data: existingSnippets = [] } = useSnippets()
44+
const { prefs, updatePrefs } = useEditorPrefs()
4245

4346
const [duplicates, setDuplicates] = useState<DuplicateMatch[]>([])
4447
const [ignoreDuplicate, setIgnoreDuplicate] = useState(false)
@@ -227,17 +230,24 @@ export function SnippetEditor() {
227230
{(field) => (
228231
<form.Subscribe selector={(s) => s.submissionAttempts}>
229232
{(attempts) => (
230-
<div className={cn(attempts > 0 && field.state.meta.errors.length > 0 && "ring-1 ring-inset ring-destructive/40")}>
233+
<div className={cn("overflow-hidden", attempts > 0 && field.state.meta.errors.length > 0 && "ring-1 ring-inset ring-destructive/40")}>
234+
<EditorToolbar
235+
theme={prefs.theme}
236+
font={prefs.font}
237+
onThemeChange={(theme) => updatePrefs({ theme })}
238+
onFontChange={(font) => updatePrefs({ font })}
239+
/>
231240
<form.Subscribe selector={(s) => s.values.language}>
232241
{(language) => (
233242
<CodeMirror
234243
value={field.state.value}
235244
onChange={(val) => field.handleChange(val)}
236-
theme={githubDark}
245+
theme={getThemeExtension(prefs.theme)}
237246
extensions={[
238247
...(getLanguageExtension(language as Language)
239248
? [getLanguageExtension(language as Language)!]
240249
: []),
250+
cleanGutter,
241251
]}
242252
minHeight="240px"
243253
basicSetup={{
@@ -247,7 +257,7 @@ export function SnippetEditor() {
247257
autocompletion: true,
248258
}}
249259
className="text-sm"
250-
style={{ fontFamily: "var(--font-mono, ui-monospace, monospace)" }}
260+
style={{ fontFamily: getFontCss(prefs.font) }}
251261
/>
252262
)}
253263
</form.Subscribe>

apps/web/hooks/use-editor-prefs.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"use client"
2+
3+
import { useState } from "react"
4+
import { DEFAULT_PREFS, type EditorPrefs, type EditorFont, type EditorTheme } from "@/lib/editor-prefs"
5+
6+
const STORAGE_KEY = "codepulse:editor-prefs"
7+
8+
function readStored(): EditorPrefs {
9+
if (typeof window === "undefined") return DEFAULT_PREFS
10+
try {
11+
const raw = localStorage.getItem(STORAGE_KEY)
12+
if (!raw) return DEFAULT_PREFS
13+
return { ...DEFAULT_PREFS, ...JSON.parse(raw) }
14+
} catch {
15+
return DEFAULT_PREFS
16+
}
17+
}
18+
19+
export function useEditorPrefs() {
20+
const [prefs, setPrefs] = useState<EditorPrefs>(readStored)
21+
22+
const updatePrefs = (updates: Partial<{ theme: EditorTheme; font: EditorFont }>) => {
23+
setPrefs((prev) => {
24+
const next: EditorPrefs = { ...prev, ...updates }
25+
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(next)) } catch {}
26+
return next
27+
})
28+
}
29+
30+
return { prefs, updatePrefs }
31+
}

apps/web/lib/editor-prefs.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { EditorView } from "@codemirror/view"
2+
import { githubDark, githubLight } from "@uiw/codemirror-theme-github"
3+
import { dracula } from "@uiw/codemirror-theme-dracula"
4+
import { tokyoNight } from "@uiw/codemirror-theme-tokyo-night"
5+
import { nord } from "@uiw/codemirror-theme-nord"
6+
import { oneDark } from "@codemirror/theme-one-dark"
7+
import type { Extension } from "@codemirror/state"
8+
9+
export const EDITOR_THEMES = [
10+
{ value: "github-dark", label: "GitHub Dark" },
11+
{ value: "github-light", label: "GitHub Light" },
12+
{ value: "dracula", label: "Dracula" },
13+
{ value: "tokyo-night", label: "Tokyo Night" },
14+
{ value: "nord", label: "Nord" },
15+
{ value: "one-dark", label: "One Dark" },
16+
] as const
17+
18+
export const EDITOR_FONTS = [
19+
{ value: "geist-mono", label: "Geist Mono", css: "var(--font-mono, ui-monospace, monospace)" },
20+
{ value: "jetbrains", label: "JetBrains Mono", css: "var(--font-jetbrains, 'JetBrains Mono', monospace)" },
21+
{ value: "fira-code", label: "Fira Code", css: "var(--font-fira, 'Fira Code', monospace)" },
22+
{ value: "ibm-plex", label: "IBM Plex Mono", css: "var(--font-ibm-plex, 'IBM Plex Mono', monospace)" },
23+
] as const
24+
25+
export type EditorTheme = (typeof EDITOR_THEMES)[number]["value"]
26+
export type EditorFont = (typeof EDITOR_FONTS)[number]["value"]
27+
28+
export interface EditorPrefs {
29+
theme: EditorTheme
30+
font: EditorFont
31+
}
32+
33+
export const DEFAULT_PREFS: EditorPrefs = { theme: "github-dark", font: "geist-mono" }
34+
35+
export function getThemeExtension(theme: EditorTheme): Extension {
36+
switch (theme) {
37+
case "github-dark": return githubDark
38+
case "github-light": return githubLight
39+
case "dracula": return dracula
40+
case "tokyo-night": return tokyoNight
41+
case "nord": return nord
42+
case "one-dark": return oneDark
43+
}
44+
}
45+
46+
export function getFontCss(font: EditorFont): string {
47+
return EDITOR_FONTS.find((f) => f.value === font)?.css ?? EDITOR_FONTS[0].css
48+
}
49+
50+
// Cleans up the line-number gutter across all themes:
51+
// transparent background, subtle separator, tabular-nums, muted color.
52+
export const cleanGutter = EditorView.theme({
53+
".cm-gutters": {
54+
backgroundColor: "transparent !important",
55+
borderRight: "1px solid rgba(128,128,128,0.10) !important",
56+
paddingRight: "0",
57+
},
58+
".cm-lineNumbers .cm-gutterElement": {
59+
fontVariantNumeric: "tabular-nums",
60+
paddingLeft: "12px",
61+
paddingRight: "14px",
62+
fontSize: "11.5px",
63+
color: "rgba(128,128,128,0.40)",
64+
minWidth: "2.75rem",
65+
userSelect: "none",
66+
},
67+
".cm-activeLineGutter": {
68+
backgroundColor: "transparent !important",
69+
color: "rgba(128,128,128,0.70) !important",
70+
},
71+
})

apps/web/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,17 @@
2121
"@codemirror/lang-json": "^6.0.2",
2222
"@codemirror/lang-markdown": "^6.5.0",
2323
"@codemirror/lang-python": "^6.2.1",
24+
"@codemirror/theme-one-dark": "^6.1.3",
2425
"@hugeicons/core-free-icons": "^4.1.1",
2526
"@hugeicons/react": "^1.1.6",
2627
"@supabase/ssr": "^0.10.2",
2728
"@supabase/supabase-js": "^2.105.1",
2829
"@tanstack/react-form": "^1.29.1",
2930
"@tanstack/react-query": "^5.100.5",
31+
"@uiw/codemirror-theme-dracula": "^4.25.9",
3032
"@uiw/codemirror-theme-github": "^4.25.9",
33+
"@uiw/codemirror-theme-nord": "^4.25.9",
34+
"@uiw/codemirror-theme-tokyo-night": "^4.25.9",
3135
"@uiw/codemirror-theme-vscode": "^4.25.9",
3236
"@uiw/react-codemirror": "^4.25.9",
3337
"class-variance-authority": "^0.7.1",

0 commit comments

Comments
 (0)