@leonardofirme/deploy-nextjs16 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +99 -0
  2. package/bin/cli.js +36 -0
  3. package/eslint.config.mjs +18 -0
  4. package/next.config.ts +8 -0
  5. package/package.json +40 -0
  6. package/postcss.config.mjs +7 -0
  7. package/public/favicon.png +0 -0
  8. package/src/app/favicon.ico +0 -0
  9. package/src/app/globals.css +79 -0
  10. package/src/app/layout.tsx +39 -0
  11. package/src/app/page.tsx +96 -0
  12. package/src/components/ui/Alert.tsx +35 -0
  13. package/src/components/ui/Badge.tsx +25 -0
  14. package/src/components/ui/Breadcrumb.tsx +24 -0
  15. package/src/components/ui/Button.tsx +33 -0
  16. package/src/components/ui/Card.tsx +43 -0
  17. package/src/components/ui/Checkbox.tsx +34 -0
  18. package/src/components/ui/Dropdown.tsx +72 -0
  19. package/src/components/ui/FireworksBackground.tsx +202 -0
  20. package/src/components/ui/Index.tsx +56 -0
  21. package/src/components/ui/Input.tsx +34 -0
  22. package/src/components/ui/Modal.tsx +65 -0
  23. package/src/components/ui/Progress.tsx +27 -0
  24. package/src/components/ui/Provider.tsx +16 -0
  25. package/src/components/ui/Select.tsx +41 -0
  26. package/src/components/ui/Skeleton.tsx +10 -0
  27. package/src/components/ui/StarfieldBackground.tsx +161 -0
  28. package/src/components/ui/Table.tsx +53 -0
  29. package/src/components/ui/Textarea.tsx +34 -0
  30. package/src/components/ui/Toaster.tsx +24 -0
  31. package/src/components/ui/Toggle.tsx +36 -0
  32. package/src/components/ui/ToggleDarkmode.tsx +74 -0
  33. package/src/core/animations.ts +38 -0
  34. package/src/core/config.ts +21 -0
  35. package/src/core/constants.ts +38 -0
  36. package/src/core/legal.ts +11 -0
  37. package/src/core/providers/node-resolver.tsx +21 -0
  38. package/src/hooks/use-theme.tsx +27 -0
  39. package/src/layouts/default-layout.tsx +28 -0
  40. package/src/proxy.ts +26 -0
  41. package/src/types/common.ts +10 -0
  42. package/src/types/index.ts +22 -0
  43. package/src/utils/cn.ts +10 -0
  44. package/tsconfig.json +34 -0
@@ -0,0 +1,72 @@
1
+ // src/components/ui/Dropdown.tsx
2
+ /**
3
+ * @file Dropdown.tsx
4
+ * @description Componente de menu suspenso com detecção de clique externo.
5
+ * Marcado como 'use client' para suportar Hooks de estado e efeitos no NextJS 16.
6
+ */
7
+ "use client";
8
+
9
+ import React, { useState, useRef, useEffect } from 'react';
10
+ import { cn } from '@/utils/cn';
11
+
12
+ interface DropdownProps {
13
+ label: React.ReactNode;
14
+ children: React.ReactNode;
15
+ className?: string;
16
+ }
17
+
18
+ export const Dropdown = ({ label, children, className }: DropdownProps): React.JSX.Element => {
19
+ const [isOpen, setIsOpen] = useState(false);
20
+ const containerRef = useRef<HTMLDivElement>(null);
21
+
22
+ useEffect(() => {
23
+ const handleClickOutside = (event: MouseEvent) => {
24
+ if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
25
+ setIsOpen(false);
26
+ }
27
+ };
28
+ document.addEventListener('mousedown', handleClickOutside);
29
+ return () => document.removeEventListener('mousedown', handleClickOutside);
30
+ }, []);
31
+
32
+ return (
33
+ <div className="relative inline-block text-left" ref={containerRef}>
34
+ <div
35
+ className="cursor-pointer"
36
+ onClick={() => setIsOpen(!isOpen)}
37
+ >
38
+ {label}
39
+ </div>
40
+
41
+ {isOpen && (
42
+ <div className={cn(
43
+ "absolute right-0 z-50 mt-2 w-56 origin-top-right rounded-xl border border-gray-200 bg-white shadow-xl outline-none",
44
+ "dark:border-gray-800 dark:bg-gray-950",
45
+ className
46
+ )}>
47
+ <div className="py-2 px-1">{children}</div>
48
+ </div>
49
+ )}
50
+ </div>
51
+ );
52
+ };
53
+
54
+ interface DropdownItemProps {
55
+ children: React.ReactNode;
56
+ onClick?: () => void;
57
+ className?: string;
58
+ }
59
+
60
+ export const DropdownItem = ({ children, onClick, className }: DropdownItemProps): React.JSX.Element => (
61
+ <button
62
+ onClick={onClick}
63
+ className={cn(
64
+ "block w-full px-4 py-2 text-left text-sm transition-colors rounded-lg font-sans",
65
+ "text-gray-500 hover:bg-gray-100 hover:text-gray-800",
66
+ "dark:text-gray-200 dark:hover:bg-gray-900 dark:hover:text-gray-50",
67
+ className
68
+ )}
69
+ >
70
+ {children}
71
+ </button>
72
+ );
@@ -0,0 +1,202 @@
1
+ // src/components/ui/FireworksBackground.tsx
2
+ /**
3
+ * @file FireworksBackground.tsx
4
+ * @description Background de alta performance com Canvas 2D.
5
+ * Implementação limpa sem dependência de lib externa.
6
+ * @author Leonardo Firme
7
+ */
8
+ "use client";
9
+
10
+ import React, { useCallback, useEffect, useRef } from "react";
11
+
12
+ // Implementação interna do cn para evitar erro de módulo não encontrado em pastas separadas
13
+ const cn = (...classes: (string | undefined | boolean)[]) => classes.filter(Boolean).join(" ");
14
+
15
+ interface Particle {
16
+ x: number;
17
+ y: number;
18
+ vx: number;
19
+ vy: number;
20
+ alpha: number;
21
+ decay: number;
22
+ color: string;
23
+ size: number;
24
+ }
25
+
26
+ interface Firework {
27
+ x: number;
28
+ y: number;
29
+ targetY: number;
30
+ vx: number;
31
+ vy: number;
32
+ color: string;
33
+ trail: { x: number; y: number; alpha: number }[];
34
+ }
35
+
36
+ export interface FireworksProps {
37
+ className?: string;
38
+ children?: React.ReactNode;
39
+ autoLaunchInterval?: number;
40
+ particleCount?: number;
41
+ }
42
+
43
+ export function FireworksBackground({
44
+ className,
45
+ children,
46
+ autoLaunchInterval = 800,
47
+ particleCount = 60,
48
+ }: FireworksProps) {
49
+ const canvasRef = useRef<HTMLCanvasElement>(null);
50
+ const containerRef = useRef<HTMLDivElement>(null);
51
+ const particlesRef = useRef<Particle[]>([]);
52
+ const fireworksRef = useRef<Firework[]>([]);
53
+ const animationRef = useRef<number>(0);
54
+ const scaleRef = useRef(1);
55
+
56
+ const colors = [
57
+ "#ff595e",
58
+ "#ffca3a",
59
+ "#8ac926",
60
+ "#1982c4",
61
+ "#6a4c93",
62
+ "#f72585",
63
+ "#4cc9f0",
64
+ "#80ed99",
65
+ "#ffd166",
66
+ "#ef476f",
67
+ ];
68
+
69
+ const rand = (min: number, max: number) => Math.random() * (max - min) + min;
70
+
71
+ const createExplosion = useCallback((x: number, y: number, color: string) => {
72
+ const scale = scaleRef.current;
73
+ const count = Math.floor(particleCount * Math.max(1, scale * 0.8));
74
+ for (let i = 0; i < count; i++) {
75
+ const angle = rand(0, Math.PI * 2);
76
+ const speed = rand(1, 6) * scale;
77
+ particlesRef.current.push({
78
+ x,
79
+ y,
80
+ vx: Math.cos(angle) * speed,
81
+ vy: Math.sin(angle) * speed,
82
+ alpha: 1,
83
+ decay: rand(0.01, 0.02),
84
+ color,
85
+ size: rand(1.5, 3.5),
86
+ });
87
+ }
88
+ }, [particleCount]);
89
+
90
+ const launchFirework = useCallback((targetX?: number, targetY?: number) => {
91
+ if (!containerRef.current) return;
92
+ const { offsetWidth: width, offsetHeight: height } = containerRef.current;
93
+ const scale = scaleRef.current;
94
+ const x = targetX ?? rand(width * 0.2, width * 0.8);
95
+ const startY = height;
96
+ const endY = targetY ?? rand(height * 0.2, height * 0.5);
97
+ const color = colors[Math.floor(Math.random() * colors.length)];
98
+ const speed = rand(10, 15) * scale;
99
+
100
+ fireworksRef.current.push({
101
+ x,
102
+ y: startY,
103
+ targetY: endY,
104
+ vx: rand(-1, 1) * scale,
105
+ vy: -speed,
106
+ color,
107
+ trail: [],
108
+ });
109
+ }, []);
110
+
111
+ useEffect(() => {
112
+ const canvas = canvasRef.current;
113
+ const container = containerRef.current;
114
+ if (!canvas || !container) return;
115
+ const ctx = canvas.getContext("2d");
116
+ if (!ctx) return;
117
+
118
+ const updateSize = () => {
119
+ const rect = container.getBoundingClientRect();
120
+ canvas.width = rect.width;
121
+ canvas.height = rect.height;
122
+ scaleRef.current = Math.min(rect.width, rect.height) / 1000;
123
+ };
124
+
125
+ updateSize();
126
+ const ro = new ResizeObserver(updateSize);
127
+ ro.observe(container);
128
+
129
+ const animate = () => {
130
+ const isDark = document.documentElement.classList.contains("dark");
131
+ ctx.fillStyle = isDark ? "rgba(3, 7, 18, 0.2)" : "rgba(255, 255, 255, 0.2)";
132
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
133
+
134
+ const scale = scaleRef.current;
135
+
136
+ for (let i = fireworksRef.current.length - 1; i >= 0; i--) {
137
+ const fw = fireworksRef.current[i];
138
+ fw.trail.push({ x: fw.x, y: fw.y, alpha: 1 });
139
+ if (fw.trail.length > 12) fw.trail.shift();
140
+ fw.x += fw.vx;
141
+ fw.y += fw.vy;
142
+ fw.vy += 0.2 * scale;
143
+
144
+ fw.trail.forEach((p, idx) => {
145
+ ctx.beginPath();
146
+ ctx.arc(p.x, p.y, 2 * scale * (idx / fw.trail.length), 0, Math.PI * 2);
147
+ ctx.fillStyle = fw.color;
148
+ ctx.globalAlpha = (idx / fw.trail.length) * 0.5;
149
+ ctx.fill();
150
+ });
151
+
152
+ if (fw.vy >= 0 || fw.y <= fw.targetY) {
153
+ createExplosion(fw.x, fw.y, fw.color);
154
+ fireworksRef.current.splice(i, 1);
155
+ }
156
+ }
157
+
158
+ for (let i = particlesRef.current.length - 1; i >= 0; i--) {
159
+ const p = particlesRef.current[i];
160
+ p.vy += 0.08 * scale;
161
+ p.x += p.vx;
162
+ p.y += p.vy;
163
+ p.alpha -= p.decay;
164
+
165
+ if (p.alpha <= 0) {
166
+ particlesRef.current.splice(i, 1);
167
+ continue;
168
+ }
169
+
170
+ ctx.save();
171
+ ctx.globalAlpha = p.alpha;
172
+ ctx.beginPath();
173
+ ctx.arc(p.x, p.y, p.size * scale, 0, Math.PI * 2);
174
+ ctx.fillStyle = p.color;
175
+ if (isDark) {
176
+ ctx.shadowBlur = 10 * scale;
177
+ ctx.shadowColor = p.color;
178
+ }
179
+ ctx.fill();
180
+ ctx.restore();
181
+ }
182
+
183
+ animationRef.current = requestAnimationFrame(animate);
184
+ };
185
+
186
+ animationRef.current = requestAnimationFrame(animate);
187
+ const launcher = setInterval(() => autoLaunchInterval > 0 && launchFirework(), autoLaunchInterval);
188
+
189
+ return () => {
190
+ cancelAnimationFrame(animationRef.current);
191
+ clearInterval(launcher);
192
+ ro.disconnect();
193
+ };
194
+ }, [createExplosion, launchFirework, autoLaunchInterval]);
195
+
196
+ return (
197
+ <div ref={containerRef} className={cn("fixed inset-0 overflow-hidden bg-white dark:bg-gray-950", className)}>
198
+ <canvas ref={canvasRef} className="absolute inset-0 h-full w-full" />
199
+ <div className="relative z-10 h-full w-full">{children}</div>
200
+ </div>
201
+ );
202
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * @file Index.ts
3
+ * @description Ponto central de exportação dos componentes atômicos (UI Kit).
4
+ * Esta estrutura segue os padrões de Clean Architecture, permitindo que os componentes
5
+ * sejam importados de forma modular e otimizada via Tree Shaking.
6
+ * * @author Leonardo Firme | v0 Digital
7
+ * @version 1.1.0
8
+ */
9
+
10
+ // Exportação do componente de botão com suporte a variantes (primary, outline, ghost)
11
+ export * from './Button';
12
+
13
+ // Exportação de campos de entrada de texto com tipagem estrita e modo dark nativo
14
+ export * from './Input';
15
+
16
+ // Exportação de containers de conteúdo com suporte a títulos e subtítulos
17
+ export * from './Card';
18
+
19
+ // Exportação de indicadores de status e tags informativas
20
+ export * from './Badge';
21
+
22
+ // Exportação de campo de texto multilinhas para descrições e observações
23
+ export * from './Textarea';
24
+
25
+ // Exportação de placeholders animados para estados de carregamento (Loading)
26
+ export * from './Skeleton';
27
+
28
+ // Exportação de interruptores (Switch) para controle de estados binários (on/off)
29
+ export * from './Toggle';
30
+
31
+ // Exportação do ecossistema de tabelas (Table, Header, Row, Cell) para exibição de dados
32
+ export * from './Table';
33
+
34
+ // Exportação de janelas sobrepostas animadas via Framer Motion
35
+ export * from './Modal';
36
+
37
+ // Exportação de menus de seleção única com mapeamento de opções
38
+ export * from './Select';
39
+
40
+ // Exportação de componentes para feedbacks visuais, mensagens de erro ou avisos
41
+ export * from './Alert';
42
+
43
+ // Exportação de sistemas de navegação estruturada (Trilhas)
44
+ export * from './Breadcrumb';
45
+
46
+ // Exportação de seletores de múltipla escolha (Check)
47
+ export * from './Checkbox';
48
+
49
+ // Exportação de menus de contexto e ações suspensas
50
+ export * from './Dropdown';
51
+
52
+ // Exportação de barras de progresso e indicadores de carregamento linear
53
+ export * from './Progress';
54
+
55
+ // Exportação do provedor de notificações (Toast) padronizado v0 Digital
56
+ export * from './Toaster';
@@ -0,0 +1,34 @@
1
+ // src/components/ui/Input.tsx
2
+ /**
3
+ * @file Input.tsx
4
+ * @description Campo de entrada de texto padronizado.
5
+ * Sem uso de uppercase para preservar a integridade de dados sensíveis.
6
+ */
7
+ import React from 'react';
8
+ import { cn } from '@/utils/cn';
9
+
10
+ export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
11
+ label?: string;
12
+ }
13
+
14
+ export const Input = ({ label, className, ...props }: InputProps): React.JSX.Element => {
15
+ return (
16
+ <div className="w-full space-y-1.5">
17
+ {label && (
18
+ <label className="text-sm font-medium text-gray-500 dark:text-gray-100 font-sans">
19
+ {label}
20
+ </label>
21
+ )}
22
+ <input
23
+ {...props}
24
+ className={cn(
25
+ "flex h-10 w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-800 transition-all font-sans",
26
+ "placeholder:text-gray-400 focus-visible:outline-hidden focus:border-v0-600",
27
+ "disabled:cursor-not-allowed disabled:opacity-50",
28
+ "dark:border-gray-800 dark:bg-gray-950 dark:text-gray-50 dark:placeholder:text-gray-400",
29
+ className
30
+ )}
31
+ />
32
+ </div>
33
+ );
34
+ };
@@ -0,0 +1,65 @@
1
+ // src/components/ui/Modal.tsx
2
+ /**
3
+ * @file Modal.tsx
4
+ * @description Janela sobreposta animada com Framer Motion.
5
+ * Marcado como 'use client' para gerenciar estados de animação e eventos de clique.
6
+ */
7
+ "use client";
8
+
9
+ import React from 'react';
10
+ import { motion, AnimatePresence } from 'framer-motion';
11
+ import { cn } from '@/utils/cn';
12
+
13
+ interface ModalProps {
14
+ isOpen: boolean;
15
+ onClose: () => void;
16
+ title: string;
17
+ children: React.ReactNode;
18
+ className?: string;
19
+ }
20
+
21
+ export const Modal = ({ isOpen, onClose, title, children, className }: ModalProps): React.JSX.Element => {
22
+ return (
23
+ <AnimatePresence>
24
+ {isOpen && (
25
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-4">
26
+ {/* Overlay com desfoque minimalista */}
27
+ <motion.div
28
+ initial={{ opacity: 0 }}
29
+ animate={{ opacity: 1 }}
30
+ exit={{ opacity: 0 }}
31
+ onClick={onClose}
32
+ className="absolute inset-0 bg-black/40 backdrop-blur-sm cursor-pointer"
33
+ />
34
+
35
+ {/* Conteúdo do Modal */}
36
+ <motion.div
37
+ initial={{ scale: 0.95, opacity: 0 }}
38
+ animate={{ scale: 1, opacity: 1 }}
39
+ exit={{ scale: 0.95, opacity: 0 }}
40
+ className={cn(
41
+ "relative w-full max-w-lg rounded-xl border border-gray-200 bg-white p-6 shadow-xl",
42
+ "dark:border-gray-800 dark:bg-gray-950 font-sans",
43
+ className
44
+ )}
45
+ >
46
+ <div className="mb-4 flex items-center justify-between">
47
+ <h2 className="text-xl font-bold tracking-tight text-gray-800 dark:text-gray-50 font-sans">
48
+ {title}
49
+ </h2>
50
+ <button
51
+ onClick={onClose}
52
+ className="text-gray-400 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-50 cursor-pointer transition-colors"
53
+ >
54
+
55
+ </button>
56
+ </div>
57
+ <div className="text-gray-400 dark:text-gray-200 leading-relaxed">
58
+ {children}
59
+ </div>
60
+ </motion.div>
61
+ </div>
62
+ )}
63
+ </AnimatePresence>
64
+ );
65
+ };
@@ -0,0 +1,27 @@
1
+ // src/components/ui/Progress.tsx
2
+ /**
3
+ * @file Progress.tsx
4
+ * @description Barra de progresso linear para feedbacks de carregamento ou métricas.
5
+ * Design minimalista com transições suaves integradas.
6
+ */
7
+ import React from 'react';
8
+ import { cn } from '@/utils/cn';
9
+
10
+ interface ProgressProps {
11
+ value?: number;
12
+ className?: string;
13
+ }
14
+
15
+ export const Progress = ({ value = 0, className }: ProgressProps): React.JSX.Element => {
16
+ return (
17
+ <div className={cn(
18
+ "h-2 w-full overflow-hidden rounded-full bg-gray-100 dark:bg-gray-900",
19
+ className
20
+ )}>
21
+ <div
22
+ className="h-full bg-gray-800 transition-all duration-500 ease-in-out dark:bg-gray-50"
23
+ style={{ width: `${value}%` }}
24
+ />
25
+ </div>
26
+ );
27
+ };
@@ -0,0 +1,16 @@
1
+ // src/components/ui/Provider.tsx
2
+ "use client";
3
+ import { useEffect } from "react";
4
+ import { LEGAL } from "@/core/legal";
5
+
6
+ export const IntegrityProvider = ({ children }: { children: React.ReactNode }) => {
7
+ useEffect(() => {
8
+ // Log estilizado que aparece no console do navegador do usuário
9
+ console.log(
10
+ `%c ${LEGAL.notice} `,
11
+ "background: #111; color: #fff; border-left: 4px solid #v0-600; padding: 10px; font-weight: bold;"
12
+ );
13
+ }, []);
14
+
15
+ return <>{children}</>;
16
+ };
@@ -0,0 +1,41 @@
1
+ // src/components/ui/Select.tsx
2
+ /**
3
+ * @file Select.tsx
4
+ * @description Seletor de opções padronizado para formulários ERP.
5
+ * Segue o layout minimalista de bordas e tipografia do ecossistema v0 Digital.
6
+ */
7
+ import React from 'react';
8
+ import { cn } from '@/utils/cn';
9
+
10
+ export interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {
11
+ label?: string;
12
+ options: { label: string; value: string | number }[];
13
+ }
14
+
15
+ export const Select = ({ label, options, className, ...props }: SelectProps): React.JSX.Element => {
16
+ return (
17
+ <div className="w-full space-y-1.5">
18
+ {label && (
19
+ <label className="text-sm font-medium text-gray-500 dark:text-gray-100 font-sans">
20
+ {label}
21
+ </label>
22
+ )}
23
+ <select
24
+ {...props}
25
+ className={cn(
26
+ "flex h-10 w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-800 font-sans transition-all",
27
+ "focus-visible:outline-hidden focus:border-v0-600 cursor-pointer",
28
+ "disabled:cursor-not-allowed disabled:opacity-50",
29
+ "dark:border-gray-800 dark:bg-gray-950 dark:text-gray-50",
30
+ className
31
+ )}
32
+ >
33
+ {options.map((opt) => (
34
+ <option key={opt.value} value={opt.value}>
35
+ {opt.label}
36
+ </option>
37
+ ))}
38
+ </select>
39
+ </div>
40
+ );
41
+ };
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+
3
+ export const Skeleton = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>): React.JSX.Element => {
4
+ return (
5
+ <div
6
+ className={`animate-pulse rounded-md bg-gray-200 dark:bg-gray-800 ${className}`}
7
+ {...props}
8
+ />
9
+ );
10
+ };
@@ -0,0 +1,161 @@
1
+ // src/components/ui/StarfieldBackground.tsx
2
+ /**
3
+ * @file StarfieldBackground.tsx
4
+ * @description Background espacial com efeito de profundidade (warp speed).
5
+ * Otimizado para alta performance via Canvas 2D sem dependências.
6
+ * @author Leonardo Firme
7
+ */
8
+ "use client";
9
+
10
+ import React, { useEffect, useRef } from "react";
11
+
12
+ // Implementação interna do cn para garantir portabilidade entre pastas
13
+ const cn = (...classes: (string | undefined | boolean)[]) => classes.filter(Boolean).join(" ");
14
+
15
+ export interface StarfieldBackgroundProps {
16
+ className?: string;
17
+ children?: React.ReactNode;
18
+ count?: number;
19
+ speed?: number;
20
+ starColor?: string;
21
+ twinkle?: boolean;
22
+ }
23
+
24
+ interface Star {
25
+ x: number;
26
+ y: number;
27
+ z: number;
28
+ twinkleSpeed: number;
29
+ twinkleOffset: number;
30
+ }
31
+
32
+ export function StarfieldBackground({
33
+ className,
34
+ children,
35
+ count = 400,
36
+ speed = 0.5,
37
+ starColor = "#ffffff",
38
+ twinkle = true,
39
+ }: StarfieldBackgroundProps) {
40
+ const canvasRef = useRef<HTMLCanvasElement>(null);
41
+ const containerRef = useRef<HTMLDivElement>(null);
42
+
43
+ useEffect(() => {
44
+ const canvas = canvasRef.current;
45
+ const container = containerRef.current;
46
+ if (!canvas || !container) return;
47
+
48
+ const ctx = canvas.getContext("2d");
49
+ if (!ctx) return;
50
+
51
+ let width = 0;
52
+ let height = 0;
53
+ const maxDepth = 1500;
54
+
55
+ const updateSize = () => {
56
+ const rect = container.getBoundingClientRect();
57
+ width = rect.width;
58
+ height = rect.height;
59
+ canvas.width = width;
60
+ canvas.height = height;
61
+ };
62
+
63
+ const createStar = (initialZ?: number): Star => ({
64
+ x: (Math.random() - 0.5) * width * 2,
65
+ y: (Math.random() - 0.5) * height * 2,
66
+ z: initialZ ?? Math.random() * maxDepth,
67
+ twinkleSpeed: Math.random() * 0.02 + 0.01,
68
+ twinkleOffset: Math.random() * Math.PI * 2,
69
+ });
70
+
71
+ updateSize();
72
+ let stars: Star[] = Array.from({ length: count }, () => createStar());
73
+ let animationId: number;
74
+ let tick = 0;
75
+
76
+ const ro = new ResizeObserver(updateSize);
77
+ ro.observe(container);
78
+
79
+ const animate = () => {
80
+ tick++;
81
+ const isDark = document.documentElement.classList.contains("dark");
82
+
83
+ // Cor de fundo dinâmica baseada no tema do sistema
84
+ ctx.fillStyle = isDark ? "rgba(3, 7, 18, 0.2)" : "rgba(255, 255, 255, 0.2)";
85
+ ctx.fillRect(0, 0, width, height);
86
+
87
+ const cx = width / 2;
88
+ const cy = height / 2;
89
+ const currentStarColor = isDark ? starColor : "#1f2937"; // Gray-800 no light mode
90
+
91
+ for (const star of stars) {
92
+ star.z -= speed * 2;
93
+
94
+ if (star.z <= 0) {
95
+ star.x = (Math.random() - 0.5) * width * 2;
96
+ star.y = (Math.random() - 0.5) * height * 2;
97
+ star.z = maxDepth;
98
+ }
99
+
100
+ const scale = 400 / star.z;
101
+ const x = cx + star.x * scale;
102
+ const y = cy + star.y * scale;
103
+
104
+ if (x < -10 || x > width + 10 || y < -10 || y > height + 10) continue;
105
+
106
+ const size = Math.max(0.5, (1 - star.z / maxDepth) * 3);
107
+ let opacity = (1 - star.z / maxDepth) * 0.9 + 0.1;
108
+
109
+ if (twinkle && star.twinkleSpeed > 0.015) {
110
+ opacity *= 0.7 + 0.3 * Math.sin(tick * star.twinkleSpeed + star.twinkleOffset);
111
+ }
112
+
113
+ ctx.beginPath();
114
+ ctx.arc(x, y, size, 0, Math.PI * 2);
115
+ ctx.fillStyle = currentStarColor;
116
+ ctx.globalAlpha = opacity;
117
+ ctx.fill();
118
+
119
+ // Streak effect (rastro) para estrelas próximas
120
+ if (star.z < maxDepth * 0.3 && speed > 0.3) {
121
+ const streakLength = (1 - star.z / maxDepth) * speed * 8;
122
+ const angle = Math.atan2(star.y, star.x);
123
+ ctx.beginPath();
124
+ ctx.moveTo(x, y);
125
+ ctx.lineTo(x - Math.cos(angle) * streakLength, y - Math.sin(angle) * streakLength);
126
+ ctx.strokeStyle = currentStarColor;
127
+ ctx.globalAlpha = opacity * 0.3;
128
+ ctx.lineWidth = size * 0.5;
129
+ ctx.stroke();
130
+ }
131
+ }
132
+
133
+ ctx.globalAlpha = 1;
134
+ animationId = requestAnimationFrame(animate);
135
+ };
136
+
137
+ animationId = requestAnimationFrame(animate);
138
+
139
+ return () => {
140
+ cancelAnimationFrame(animationId);
141
+ ro.disconnect();
142
+ };
143
+ }, [count, speed, starColor, twinkle]);
144
+
145
+ return (
146
+ <div ref={containerRef} className={cn("fixed inset-0 overflow-hidden bg-white dark:bg-gray-950", className)}>
147
+ <canvas ref={canvasRef} className="absolute inset-0 h-full w-full" />
148
+
149
+ {/* Nebula sutil usando as cores v0 */}
150
+ <div
151
+ className="pointer-events-none absolute inset-0 opacity-20 dark:opacity-30"
152
+ style={{
153
+ background:
154
+ "radial-gradient(ellipse at 30% 40%, rgba(79, 70, 229, 0.1) 0%, transparent 50%), radial-gradient(ellipse at 70% 60%, rgba(99, 102, 241, 0.08) 0%, transparent 50%)",
155
+ }}
156
+ />
157
+
158
+ {children && <div className="relative z-10 h-full w-full">{children}</div>}
159
+ </div>
160
+ );
161
+ }