From 196843da35f017beed2294ed6f5a68cde96749c4 Mon Sep 17 00:00:00 2001 From: Musiker15 Date: Thu, 2 Jul 2026 08:20:15 +0200 Subject: [PATCH] feat(branding): beginner-friendly custom CSS (live preview + starter snippets) Custom CSS was a bare textarea with no feedback. Make it approachable: - Live preview: a sample public form rendered in a sandboxed iframe with the guild's accent color and custom CSS applied instantly (no save needed). The iframe is fully isolated (sandbox="", CSS sanitized the same way as on save), so rules can't leak into the dashboard or run scripts, and it mirrors the live page's token system (.msk-form + CSS variables, light/dark aware). - Quick styles: one-click starter snippets (rounded corners, accent background, larger text, colored heading) inserted into the editor. Each targets .msk-form or a CSS variable, so it works identically in preview and live. i18n for the new labels, snippet names and sample content in all 7 locales. No schema/env change. --- .../src/components/branding/branding-form.tsx | 77 +++++++++++++--- .../src/components/branding/css-preview.tsx | 87 +++++++++++++++++++ apps/web/src/i18n/dictionaries.ts | 28 ++++++ 3 files changed, 181 insertions(+), 11 deletions(-) create mode 100644 apps/web/src/components/branding/css-preview.tsx diff --git a/apps/web/src/components/branding/branding-form.tsx b/apps/web/src/components/branding/branding-form.tsx index e22a2d2..aefddf6 100644 --- a/apps/web/src/components/branding/branding-form.tsx +++ b/apps/web/src/components/branding/branding-form.tsx @@ -5,6 +5,7 @@ import { Button, Card, Field, Input, Textarea } from "@msk-forms/ui"; import { useRouter } from "next/navigation"; import { useState } from "react"; +import { CssPreview } from "@/components/branding/css-preview"; import type { Dictionary } from "@/i18n"; type BrandingDict = Dictionary["branding"]; @@ -12,6 +13,21 @@ type BrandingDict = Dictionary["branding"]; const DEFAULT_ACCENT = "#5eb131"; const HEX = /^#[0-9a-fA-F]{6}$/; +/** + * Beginner-friendly starter rules. Each targets `.msk-form` or a CSS variable + * so it behaves identically in the preview and on the live page (the two + * documented, stable hooks). `key` maps to a localized label. + */ +const CSS_SNIPPETS: { key: keyof BrandingDict["snip"]; css: string }[] = [ + { key: "rounded", css: ".msk-form {\n --radius: 1.25rem;\n}" }, + { + key: "tint", + css: ".msk-form {\n background: hsl(var(--primary) / 0.05);\n padding: 1.5rem;\n border-radius: 1rem;\n}", + }, + { key: "text", css: ".msk-form {\n font-size: 1.05rem;\n}" }, + { key: "heading", css: ".msk-form h1 {\n color: hsl(var(--primary));\n font-weight: 800;\n}" }, +]; + export function BrandingForm({ guildId, initial, @@ -58,6 +74,11 @@ export function BrandingForm({ } } + function addSnippet(snippet: string) { + setCss((prev) => (prev.trim() ? `${prev.trimEnd()}\n\n${snippet}` : snippet)); + setSaved(false); + } + const swatch = HEX.test(color.trim()) ? color.trim() : DEFAULT_ACCENT; return ( @@ -101,17 +122,51 @@ export function BrandingForm({ {canCss ? ( - `; + }, [css, accentColor, dark, labels]); + + return ( +