feat(registratura): atomic numbering, reserved slots, audit trail, API endpoints + theme toggle animation

Registratura module:
- Atomic sequence numbering (BTG-2026-IN-00125 format) via PostgreSQL upsert
- Reserved monthly slots (2/company/month) for late registrations
- Append-only audit trail with diff tracking
- REST API: /api/registratura (CRUD), /api/registratura/reserved, /api/registratura/audit
- Auth: NextAuth session + Bearer API key support
- New "intern" direction type with UI support (form, filters, table, detail panel)
- Prisma models: RegistrySequence, RegistryAudit

Theme toggle:
- SVG mask-based sun/moon morph with 360° spin animation
- Inverted logic (sun in dark mode, moon in light mode)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
AI Assistant
2026-03-10 07:54:32 +02:00
parent f94529c380
commit a0dd35a066
15 changed files with 1354 additions and 124 deletions
+103 -79
View File
@@ -1,108 +1,132 @@
"use client";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
import { Sun, Moon } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { cn } from "@/shared/lib/utils";
/**
* Sun↔Moon morphing toggle.
*
* One single SVG. On click it immediately spins 360°, and mid-spin
* the sun circle morphs into a crescent (via sliding SVG mask) while
* the rays retract with staggered timing. Everything is one fluid motion.
*
* Inverted logic: dark → shows sun, light → shows moon.
*/
export function ThemeToggle() {
const { resolvedTheme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
const svgRef = useRef<SVGSVGElement>(null);
useEffect(() => setMounted(true), []);
if (!mounted) {
return <div className="h-8 w-16 rounded-full bg-muted" />;
return <div className="h-9 w-9 rounded-xl bg-muted" />;
}
const isDark = resolvedTheme === "dark";
// Inverted: dark → sun (action: go light), light → moon (action: go dark)
const showSun = isDark;
const handleClick = () => {
// Trigger spin via Web Animations API — instant, no state re-render needed
svgRef.current?.animate(
[
{ transform: "rotate(0deg) scale(1)" },
{ transform: "rotate(180deg) scale(1.12)", offset: 0.5 },
{ transform: "rotate(360deg) scale(1)" },
],
{ duration: 550, easing: "cubic-bezier(0.4, 0, 0.2, 1)" },
);
setTheme(isDark ? "light" : "dark");
};
// Ray endpoints — 8 rays at 45° intervals
const rays = [0, 45, 90, 135, 180, 225, 270, 315].map((deg) => {
const r = (deg * Math.PI) / 180;
return {
x1: 12 + 6.5 * Math.cos(r),
y1: 12 + 6.5 * Math.sin(r),
x2: 12 + 9 * Math.cos(r),
y2: 12 + 9 * Math.sin(r),
};
});
const ease = "cubic-bezier(0.4, 0, 0.2, 1)";
const dur = "500ms";
return (
<button
type="button"
role="switch"
aria-checked={isDark}
aria-label="Schimbă tema"
onClick={() => setTheme(isDark ? "light" : "dark")}
aria-label={isDark ? "Treci la modul luminos" : "Treci la modul intunecat"}
onClick={handleClick}
className={cn(
"relative inline-flex h-8 w-16 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-all duration-500 ease-in-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
isDark
? "bg-gradient-to-r from-indigo-950 via-indigo-900 to-slate-800"
: "bg-gradient-to-r from-sky-400 via-sky-300 to-amber-200",
"relative flex h-9 w-9 items-center justify-center rounded-xl",
"transition-colors duration-200 hover:bg-accent/60",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
)}
>
{/* Stars — visible in dark mode */}
<span
className={cn(
"pointer-events-none absolute left-2 top-1.5 flex gap-1 transition-all duration-500",
isDark ? "opacity-100 scale-100" : "opacity-0 scale-50",
)}
<svg
ref={svgRef}
viewBox="0 0 24 24"
fill="none"
className="h-[22px] w-[22px]"
>
<span className="h-[3px] w-[3px] rounded-full bg-white/90 animate-[pulse_2s_ease-in-out_infinite]" />
<span
className="mt-1.5 h-[2px] w-[2px] rounded-full bg-white/60 animate-[pulse_3s_ease-in-out_infinite]"
style={{ animationDelay: "0.7s" }}
/>
<span
className="-mt-0.5 h-[2px] w-[2px] rounded-full bg-white/50 animate-[pulse_2.5s_ease-in-out_infinite]"
style={{ animationDelay: "1.2s" }}
/>
</span>
<defs>
<mask id="theme-mask">
<rect width="24" height="24" fill="white" />
{/* Slides in to carve the crescent; slides out to reveal full circle */}
<circle
r="6.5"
fill="black"
cx={showSun ? 28 : 16}
cy={showSun ? 2 : 6}
style={{ transition: `cx ${dur} ${ease}, cy ${dur} ${ease}` }}
/>
</mask>
</defs>
{/* Clouds — visible in light mode */}
<span
className={cn(
"pointer-events-none absolute right-2 bottom-1 transition-all duration-500",
isDark
? "translate-y-3 opacity-0 scale-75"
: "translate-y-0 opacity-100 scale-100",
)}
>
<span className="inline-block h-2 w-4 rounded-full bg-white/70" />
<span className="absolute -right-1 -top-0.5 h-1.5 w-2.5 rounded-full bg-white/50" />
</span>
{/* Sliding knob with Sun/Moon icon */}
<span
className={cn(
"pointer-events-none relative z-10 inline-flex h-6 w-6 items-center justify-center rounded-full shadow-lg transition-all duration-500 ease-in-out",
isDark ? "translate-x-[33px]" : "translate-x-[3px]",
isDark
? "bg-indigo-100 shadow-indigo-300/40"
: "bg-amber-50 shadow-amber-400/50",
)}
>
{/* Sun icon */}
<Sun
className={cn(
"absolute h-4 w-4 text-amber-500 transition-all duration-500",
isDark
? "rotate-90 scale-0 opacity-0"
: "rotate-0 scale-100 opacity-100",
)}
strokeWidth={2.5}
/>
{/* Moon icon */}
<Moon
className={cn(
"absolute h-3.5 w-3.5 text-indigo-600 transition-all duration-500",
isDark
? "rotate-0 scale-100 opacity-100"
: "-rotate-90 scale-0 opacity-0",
)}
strokeWidth={2.5}
{/* Main body — full sun circle that morphs into crescent moon */}
<circle
cx="12"
cy="12"
r={showSun ? 4.5 : 5.5}
fill="currentColor"
mask="url(#theme-mask)"
className={showSun ? "text-amber-400" : "text-slate-400 dark:text-indigo-300"}
style={{ transition: `r ${dur} ${ease}, color ${dur}` }}
/>
{/* Glow ring */}
<span
className={cn(
"absolute inset-0 rounded-full transition-all duration-500",
isDark
? "shadow-[0_0_8px_2px_rgba(129,140,248,0.3)]"
: "shadow-[0_0_8px_2px_rgba(251,191,36,0.3)] animate-[pulse_2s_ease-in-out_infinite]",
)}
/>
</span>
{/* Sun rays — retract into center when moon */}
<g
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
className={showSun ? "text-amber-400" : "text-slate-400 dark:text-indigo-300"}
style={{
transition: `opacity 300ms ${ease}, color ${dur}`,
opacity: showSun ? 1 : 0,
}}
>
{rays.map((ray, i) => (
<line
key={i}
x1={showSun ? ray.x1 : 12}
y1={showSun ? ray.y1 : 12}
x2={showSun ? ray.x2 : 12}
y2={showSun ? ray.y2 : 12}
style={{
transition: [
`x1 ${dur} ${ease} ${i * 20}ms`,
`y1 ${dur} ${ease} ${i * 20}ms`,
`x2 ${dur} ${ease} ${i * 20}ms`,
`y2 ${dur} ${ease} ${i * 20}ms`,
].join(", "),
}}
/>
))}
</g>
</svg>
</button>
);
}