feat: custom CSS editor with CodeMirror, live preview, and /docs/css reference

This commit is contained in:
2026-05-24 03:26:34 +02:00
parent fccc4064cf
commit f4932af2ba
7 changed files with 493 additions and 5 deletions

View File

@@ -0,0 +1,100 @@
"use client";
import { useEffect, useRef, useState } from "react";
import CodeMirror from "@uiw/react-codemirror";
import { css } from "@codemirror/lang-css";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ExternalLink } from "lucide-react";
interface CssEditorProps {
value?: string;
onChange?: (value: string) => void;
username: string;
}
export function CssEditor({ value = "", onChange, username }: CssEditorProps) {
const iframeRef = useRef<HTMLIFrameElement>(null);
const [iframeReady, setIframeReady] = useState(false);
useEffect(() => {
if (!iframeReady) return;
const iframe = iframeRef.current;
if (!iframe) return;
const timer = setTimeout(() => {
iframe.contentWindow?.postMessage(
{ type: "css-preview", css: value },
window.location.origin
);
}, 150);
return () => clearTimeout(timer);
}, [value, iframeReady]);
return (
<div className="rounded-lg border bg-card overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b">
<span className="text-sm font-medium">Custom CSS</span>
<a
href="/docs/css"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
>
<ExternalLink className="h-3 w-3" />
CSS Reference
</a>
</div>
{/* Tabs */}
<Tabs defaultValue="edit">
<TabsList className="w-full justify-start rounded-none border-b h-auto p-0 bg-transparent">
<TabsTrigger
value="edit"
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent px-4 py-2 text-sm"
>
Edit
</TabsTrigger>
<TabsTrigger
value="preview"
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent px-4 py-2 text-sm"
>
Preview
</TabsTrigger>
</TabsList>
<TabsContent value="edit" className="mt-0">
<CodeMirror
value={value}
height="280px"
extensions={[css()]}
theme="dark"
onChange={(val) => onChange?.(val)}
basicSetup={{
lineNumbers: true,
foldGutter: false,
dropCursor: false,
allowMultipleSelections: false,
indentOnInput: true,
}}
/>
</TabsContent>
<TabsContent value="preview" className="mt-0 data-[state=inactive]:hidden" forceMount>
<div className="relative">
<iframe
ref={iframeRef}
src={`/users/${username}`}
onLoad={() => setIframeReady(true)}
className="w-full border-0"
style={{ height: "560px" }}
title="Profile preview"
/>
<div className="absolute top-2 right-2 text-xs text-muted-foreground bg-background/80 px-2 py-1 rounded">
Live preview
</div>
</div>
</TabsContent>
</Tabs>
</div>
);
}

View File

@@ -0,0 +1,27 @@
"use client";
import { useEffect } from "react";
export function CssPreviewListener() {
useEffect(() => {
if (window.self === window.top) return;
function handler(e: MessageEvent) {
if (e.origin !== window.location.origin) return;
if (!e.data || e.data.type !== "css-preview") return;
let el = document.getElementById("css-preview-style") as HTMLStyleElement | null;
if (!el) {
el = document.createElement("style");
el.id = "css-preview-style";
document.head.appendChild(el);
}
el.textContent = typeof e.data.css === "string" ? e.data.css : "";
}
window.addEventListener("message", handler);
return () => window.removeEventListener("message", handler);
}, []);
return null;
}

View File

@@ -21,6 +21,7 @@ import {
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { UserAvatar } from "@/components/user-avatar";
import { CssEditor } from "@/components/css-editor";
import { Camera, ImagePlus, Loader2 } from "lucide-react";
interface EditProfileFormProps {
@@ -199,12 +200,11 @@ export function EditProfileForm({ currentUser, token }: EditProfileFormProps) {
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Custom CSS</FormLabel>
<FormControl>
<Textarea
placeholder="body { font-family: 'Comic Sans MS'; }"
rows={5}
{...field}
<CssEditor
value={field.value ?? ""}
onChange={field.onChange}
username={currentUser.username}
/>
</FormControl>
<FormMessage />