@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.
- package/README.md +99 -0
- package/bin/cli.js +36 -0
- package/eslint.config.mjs +18 -0
- package/next.config.ts +8 -0
- package/package.json +40 -0
- package/postcss.config.mjs +7 -0
- package/public/favicon.png +0 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +79 -0
- package/src/app/layout.tsx +39 -0
- package/src/app/page.tsx +96 -0
- package/src/components/ui/Alert.tsx +35 -0
- package/src/components/ui/Badge.tsx +25 -0
- package/src/components/ui/Breadcrumb.tsx +24 -0
- package/src/components/ui/Button.tsx +33 -0
- package/src/components/ui/Card.tsx +43 -0
- package/src/components/ui/Checkbox.tsx +34 -0
- package/src/components/ui/Dropdown.tsx +72 -0
- package/src/components/ui/FireworksBackground.tsx +202 -0
- package/src/components/ui/Index.tsx +56 -0
- package/src/components/ui/Input.tsx +34 -0
- package/src/components/ui/Modal.tsx +65 -0
- package/src/components/ui/Progress.tsx +27 -0
- package/src/components/ui/Provider.tsx +16 -0
- package/src/components/ui/Select.tsx +41 -0
- package/src/components/ui/Skeleton.tsx +10 -0
- package/src/components/ui/StarfieldBackground.tsx +161 -0
- package/src/components/ui/Table.tsx +53 -0
- package/src/components/ui/Textarea.tsx +34 -0
- package/src/components/ui/Toaster.tsx +24 -0
- package/src/components/ui/Toggle.tsx +36 -0
- package/src/components/ui/ToggleDarkmode.tsx +74 -0
- package/src/core/animations.ts +38 -0
- package/src/core/config.ts +21 -0
- package/src/core/constants.ts +38 -0
- package/src/core/legal.ts +11 -0
- package/src/core/providers/node-resolver.tsx +21 -0
- package/src/hooks/use-theme.tsx +27 -0
- package/src/layouts/default-layout.tsx +28 -0
- package/src/proxy.ts +26 -0
- package/src/types/common.ts +10 -0
- package/src/types/index.ts +22 -0
- package/src/utils/cn.ts +10 -0
- 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
|
+
}
|