Files
lms-sb/src/app/(student)/tools/theme/ThemeForm.tsx
T
admins b6d555ab3b Add Obsidian Toolbox — 4 syntax generators under /tools
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>
2026-06-23 15:45:52 +05:00

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>
);
}