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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user