b6d555ab3b
Inside-LMS toolbox for registered students: callout-CSS, YAML-frontmatter, theme CSS-variables, Style Settings generators. Auth inherited from (student) route group; no public surface, no email gate. - Pure generators in src/lib/tools/* with Vitest (33 tests) - Shared YAML-safe scalar quoter (_shared/yaml.ts) used by frontmatter + style-settings: unquoted user input was silently breaking YAML (tags '#x' -> null, color default '#7C3AED' -> null, 'a: b' -> parse error) - hex validation in callout (no NaN), date input constrained, clipboard + analytics calls guarded - Prisma ToolUsage model + migration; logToolUsage Server Action (auth-first, 10-min dedup window per user/tool) - Tool index /tools + ToolCard (explicit lucide map, no import *) + header link Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
59 lines
2.4 KiB
TypeScript
59 lines
2.4 KiB
TypeScript
"use client";
|
|
import { useMemo, useState } from "react";
|
|
import { buildThemeCss, type ThemeInput } from "@/lib/tools/theme";
|
|
import { CodeOutput } from "@/components/tools/CodeOutput";
|
|
|
|
const FIELDS: { key: keyof ThemeInput; label: string }[] = [
|
|
{ key: "background", label: "Фон" },
|
|
{ key: "text", label: "Текст" },
|
|
{ key: "accent", label: "Акцент" },
|
|
{ key: "link", label: "Ссылки" },
|
|
{ key: "border", label: "Рамка" },
|
|
];
|
|
|
|
const fieldStyle = { border: "1px solid var(--border)", borderRadius: "2px", backgroundColor: "var(--background)", color: "var(--foreground)" } as const;
|
|
|
|
export function ThemeForm() {
|
|
const [theme, setTheme] = useState<ThemeInput>({
|
|
background: "#0F0F0F", text: "#EAEAEA", accent: "#7C3AED", link: "#4EA1FF", border: "#333333",
|
|
});
|
|
|
|
const css = useMemo(() => buildThemeCss(theme), [theme]);
|
|
|
|
function update(key: keyof ThemeInput, value: string) {
|
|
setTheme((prev) => ({ ...prev, [key]: value }));
|
|
}
|
|
|
|
return (
|
|
<div className="grid gap-6 md:grid-cols-2">
|
|
<div className="flex flex-col gap-3">
|
|
{FIELDS.map((f) => (
|
|
<label key={f.key} className="flex items-center justify-between gap-3 text-sm">
|
|
<span>{f.label}</span>
|
|
<span className="flex items-center gap-2">
|
|
<input
|
|
type="color" value={theme[f.key]} onChange={(e) => update(f.key, e.target.value)}
|
|
aria-label={f.label} style={{ width: 36, height: 28, border: "1px solid var(--border)", borderRadius: 2, padding: 0, background: "none" }}
|
|
/>
|
|
<input
|
|
className="w-28 p-2 text-sm" style={fieldStyle}
|
|
value={theme[f.key]} onChange={(e) => update(f.key, e.target.value)}
|
|
/>
|
|
</span>
|
|
</label>
|
|
))}
|
|
<div
|
|
className="mt-2 p-4 text-sm"
|
|
style={{ backgroundColor: theme.background, color: theme.text, border: `2px solid ${theme.border}`, borderRadius: 2 }}
|
|
>
|
|
Превью темы. <a href="#" onClick={(e) => e.preventDefault()} style={{ color: theme.link }}>ссылка</a>,{" "}
|
|
<span style={{ color: theme.accent, fontWeight: 700 }}>акцент</span>.
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-col gap-4">
|
|
<CodeOutput code={css} tool="theme" label="CSS-переменные темы" />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|