918 lines
43 KiB
JavaScript
918 lines
43 KiB
JavaScript
import React, { useState, useMemo, useEffect, lazy, Suspense } from 'react';
|
|
import {
|
|
Library,
|
|
Box,
|
|
Layers,
|
|
Code2,
|
|
ChevronRight,
|
|
Monitor,
|
|
Smartphone,
|
|
Tablet,
|
|
RotateCcw,
|
|
ExternalLink,
|
|
Info,
|
|
Plus,
|
|
FlaskConical,
|
|
Rocket,
|
|
Search,
|
|
Loader2
|
|
} from 'lucide-react';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
// UI Components
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card } from '@/components/ui/card';
|
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
import { Separator } from '@/components/ui/separator';
|
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { TableRow, TableCell } from '@/components/ui/table';
|
|
|
|
// Lazy loading components to showcase
|
|
const ExcelTable = lazy(() => import('@/features/prafrot/components/ExcelTable'));
|
|
const ItemDetailPanel = lazy(() => import('@/components/shared/ItemDetailPanel').then(m => ({ default: m.ItemDetailPanel })));
|
|
const ItemDetailPanelDebug = lazy(() => import('@/components/shared/ItemDetailPanel/ItemDetailPanel.debug').then(m => ({ default: m.ItemDetailPanelDebug })));
|
|
const StatusBadge = lazy(() => import('../components/sample/StatusBadge').then(m => ({ default: m.StatusBadge })));
|
|
const StatusBadgeDebug = lazy(() => import('../components/sample/StatusBadge/StatusBadge.debug').then(m => ({ default: m.default })));
|
|
const FinesCard = lazy(() => import('@/features/prafrot/components/FinesCard').then(m => ({ default: m.FinesCard })));
|
|
const FinesCardDebug = lazy(() => import('@/features/prafrot/components/FinesCard/FinesCard.debug').then(m => ({ default: m.default })));
|
|
const StatsGrid = lazy(() => import('@/components/shared/StatsGrid').then(m => ({ default: m.StatsGrid })));
|
|
const DataTable = lazy(() => import('@/components/shared/DataTable').then(m => ({ default: m.DataTable })));
|
|
const AutoFillInput = lazy(() => import('@/components/shared/AutoFillInput').then(m => ({ default: m.AutoFillInput })));
|
|
const DashboardKPICard = lazy(() => import('@/components/shared/DashboardKPICard').then(m => ({ default: m.DashboardKPICard })));
|
|
const DashboardKPICardDebug = lazy(() => import('@/components/shared/DashboardKPICard/DashboardKPICard.debug').then(m => ({ default: m.DashboardKPICardDebug })));
|
|
const KanbanBoard = lazy(() => import('@/components/shared/KanbanBoard').then(m => ({ default: m.KanbanBoard })));
|
|
const KanbanBoardDebug = lazy(() => import('@/components/shared/KanbanBoard/KanbanBoard.debug').then(m => ({ default: m.KanbanBoardDebug })));
|
|
const Sidebar = lazy(() => import('@/features/layout/components/Sidebar').then(m => ({ default: m.Sidebar })));
|
|
const SidebarDebug = lazy(() => import('@/features/layout/components/Sidebar/Sidebar.debug').then(m => ({ default: m.SidebarDebug })));
|
|
|
|
/**
|
|
* Loading Placeholder for Lazy Components
|
|
*/
|
|
const ComponentLoader = () => (
|
|
<div className="flex flex-col items-center justify-center p-12 gap-4 h-full w-full bg-slate-500/5 animate-pulse rounded-xl border border-dashed border-slate-500/20">
|
|
<Loader2 className="w-8 h-8 text-emerald-500 animate-spin" />
|
|
<span className="text-[10px] font-bold uppercase tracking-widest text-slate-500">Iniciando Laboratório...</span>
|
|
</div>
|
|
);
|
|
|
|
// Sidebar Navigation Component
|
|
const SidebarNav = React.memo(({
|
|
activeCategory,
|
|
setActiveCategory,
|
|
setSelectedComponent,
|
|
theme,
|
|
setTheme,
|
|
categories,
|
|
searchQuery,
|
|
setSearchQuery,
|
|
filteredComponents,
|
|
selectedComponent
|
|
}) => (
|
|
<aside className={cn(
|
|
"w-80 flex flex-col border-r h-full",
|
|
theme === 'dark' ? "bg-[#111] border-white/5" : "bg-white border-slate-200"
|
|
)}>
|
|
<div className="p-6">
|
|
<div className="flex items-center gap-3 mb-8">
|
|
<div className="w-10 h-10 bg-emerald-500 rounded-xl flex items-center justify-center shadow-lg shadow-emerald-500/20">
|
|
<Box className="text-white" size={20} />
|
|
</div>
|
|
<div>
|
|
<h1 className="font-black tracking-tight text-lg leading-tight uppercase italic">Component</h1>
|
|
<span className="text-[10px] font-bold text-emerald-500 tracking-[0.3em] uppercase opacity-80">Laboratory</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
{categories.map((cat) => (
|
|
<button
|
|
key={cat.id}
|
|
onClick={() => {
|
|
setActiveCategory(cat.id);
|
|
setSelectedComponent(null);
|
|
}}
|
|
className={cn(
|
|
"w-full flex items-center gap-3 px-4 py-2.5 rounded-lg text-sm font-medium transition-all duration-200",
|
|
activeCategory === cat.id
|
|
? (theme === 'dark' ? "bg-emerald-500/10 text-emerald-400" : "bg-emerald-50 text-emerald-600")
|
|
: (theme === 'dark' ? "text-slate-500 hover:text-slate-300 hover:bg-white/5" : "text-slate-600 hover:bg-slate-100")
|
|
)}
|
|
>
|
|
<cat.icon size={18} />
|
|
{cat.label}
|
|
{activeCategory === cat.id && (
|
|
<div className="ml-auto w-1.5 h-1.5 rounded-full bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.5)]" />
|
|
)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<Separator className={theme === 'dark' ? "bg-white/5" : "bg-slate-100"} />
|
|
|
|
<ScrollArea className="flex-1">
|
|
<div className="p-4 space-y-4">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" size={14} />
|
|
<input
|
|
type="text"
|
|
placeholder="Filtrar componentes..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
className={cn(
|
|
"w-full pl-9 pr-4 py-2 rounded-md text-xs border focus:outline-none transition-all",
|
|
theme === 'dark' ? "bg-white/5 border-white/10 focus:border-emerald-500/50 text-white" : "bg-slate-100 border-slate-200 focus:border-emerald-500/50"
|
|
)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<span className="px-2 text-[10px] font-bold text-slate-500 uppercase tracking-widest">Lista de Items</span>
|
|
{filteredComponents.map((comp) => (
|
|
<button
|
|
key={comp.name}
|
|
onClick={() => setSelectedComponent(comp)}
|
|
className={cn(
|
|
"w-full flex items-center justify-between px-3 py-2.5 rounded-md text-sm transition-all group",
|
|
selectedComponent?.name === comp.name
|
|
? (theme === 'dark' ? "bg-white/5 text-white" : "bg-white shadow-sm ring-1 ring-slate-200 text-slate-900")
|
|
: (theme === 'dark' ? "text-slate-400 hover:text-slate-200 hover:bg-white/5" : "text-slate-600 hover:bg-slate-100")
|
|
)}
|
|
>
|
|
<span className="truncate">{comp.name}</span>
|
|
<div className="flex items-center gap-2">
|
|
{comp.debugRender && (
|
|
<FlaskConical size={12} className="text-purple-400" title="Possui versão Debug" />
|
|
)}
|
|
<ChevronRight size={14} className={cn(
|
|
"transition-transform shrink-0",
|
|
selectedComponent?.name === comp.name ? "rotate-90 text-emerald-500" : "opacity-0 group-hover:opacity-100"
|
|
)} />
|
|
</div>
|
|
</button>
|
|
))}
|
|
{filteredComponents.length === 0 && (
|
|
<div className="p-4 text-center text-xs text-slate-500 italic">Nenhum componente encontrado.</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</ScrollArea>
|
|
|
|
{/* Theme Toggler - Restored to Sidebar */}
|
|
<div className="p-4 border-t border-white/5">
|
|
<div className="flex gap-2 p-1 bg-black/20 rounded-lg">
|
|
<button
|
|
onClick={() => setTheme('dark')}
|
|
className={cn(
|
|
"flex-1 py-1.5 rounded text-[10px] font-bold uppercase tracking-wider transition-all",
|
|
theme === 'dark' ? "bg-emerald-500 text-white shadow-lg shadow-emerald-500/20" : "text-slate-500 hover:text-slate-300"
|
|
)}
|
|
>
|
|
Dark Mode
|
|
</button>
|
|
<button
|
|
onClick={() => setTheme('light')}
|
|
className={cn(
|
|
"flex-1 py-1.5 rounded text-[10px] font-bold uppercase tracking-wider transition-all",
|
|
theme === 'light' ? "bg-white text-emerald-600 shadow-sm" : "text-slate-500 hover:text-slate-300"
|
|
)}
|
|
>
|
|
Light Mode
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
));
|
|
|
|
// Header/Toolbar Component
|
|
const PlaygroundHeader = React.memo(({
|
|
selectedComponent,
|
|
activeCategory,
|
|
theme,
|
|
versionMode,
|
|
setVersionMode,
|
|
viewMode,
|
|
setViewMode
|
|
}) => (
|
|
<header className={cn(
|
|
"h-16 flex items-center justify-between px-8 border-b transition-colors",
|
|
theme === 'dark' ? "bg-[#0d0d0d] border-white/5" : "bg-white border-slate-200"
|
|
)}>
|
|
<div className="flex items-center gap-4">
|
|
{selectedComponent ? (
|
|
<>
|
|
<Badge variant="outline" className="text-emerald-500 border-emerald-500/20 px-2 py-0 text-[10px] uppercase font-bold tracking-tighter">{activeCategory}</Badge>
|
|
<div className="h-4 w-[1px] bg-white/10" />
|
|
<h2 className="text-lg font-bold tracking-tight">{selectedComponent.name}</h2>
|
|
</>
|
|
) : (
|
|
<h2 className="text-lg font-bold tracking-tight text-slate-500 italic">Pronto para criar sua próxima obra-prima?</h2>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center gap-6">
|
|
<div className={cn(
|
|
"flex p-1 rounded-lg border",
|
|
theme === 'dark' ? "bg-black/20 border-white/5" : "bg-slate-100 border-slate-200"
|
|
)}>
|
|
<button
|
|
onClick={() => setVersionMode('production')}
|
|
className={cn(
|
|
"flex items-center gap-2 px-3 py-1.5 rounded transition-all text-[10px] font-bold uppercase tracking-wider",
|
|
versionMode === 'production'
|
|
? (theme === 'dark' ? "bg-emerald-500 text-white shadow-lg shadow-emerald-500/20" : "bg-emerald-500 text-white")
|
|
: "text-slate-500 hover:text-slate-300"
|
|
)}
|
|
>
|
|
<Rocket size={12} />
|
|
Production
|
|
</button>
|
|
<button
|
|
onClick={() => setVersionMode('debug')}
|
|
className={cn(
|
|
"flex items-center gap-2 px-3 py-1.5 rounded transition-all text-[10px] font-bold uppercase tracking-wider",
|
|
versionMode === 'debug'
|
|
? (theme === 'dark' ? "bg-purple-500 text-white shadow-lg shadow-purple-500/20" : "bg-purple-500 text-white")
|
|
: "text-slate-500 hover:text-slate-300"
|
|
)}
|
|
>
|
|
<FlaskConical size={12} />
|
|
Debug
|
|
</button>
|
|
</div>
|
|
|
|
<Separator orientation="vertical" className="h-6 bg-white/10 mx-1" />
|
|
|
|
<div className={cn(
|
|
"flex p-1 rounded-lg border",
|
|
theme === 'dark' ? "bg-black/20 border-white/5" : "bg-slate-100 border-slate-200"
|
|
)}>
|
|
<button
|
|
onClick={() => setViewMode('mobile')}
|
|
className={cn("p-1.5 rounded transition-all", viewMode === 'mobile' ? "bg-emerald-500 text-white" : "text-slate-400")}
|
|
title="Mobile View"
|
|
>
|
|
<Smartphone size={16} />
|
|
</button>
|
|
<button
|
|
onClick={() => setViewMode('tablet')}
|
|
className={cn("p-1.5 rounded transition-all", viewMode === 'tablet' ? "bg-emerald-500 text-white" : "text-slate-400")}
|
|
title="Tablet View"
|
|
>
|
|
<Tablet size={16} />
|
|
</button>
|
|
<button
|
|
onClick={() => setViewMode('desktop')}
|
|
className={cn("p-1.5 rounded transition-all", viewMode === 'desktop' ? "bg-emerald-500 text-white" : "text-slate-400")}
|
|
title="Desktop View"
|
|
>
|
|
<Monitor size={16} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
));
|
|
|
|
/**
|
|
* Component Laboratory - Main View
|
|
* A premium environment to test and document components.
|
|
*/
|
|
const PlaygroundView = () => {
|
|
const [activeCategory, setActiveCategory] = useState('shared');
|
|
const [selectedComponent, setSelectedComponent] = useState(null);
|
|
const [viewMode, setViewMode] = useState('desktop');
|
|
const [versionMode, setVersionMode] = useState('production');
|
|
const [theme, setTheme] = useState('dark');
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [debouncedSearch, setDebouncedSearch] = useState('');
|
|
|
|
// Debounce search query
|
|
useEffect(() => {
|
|
const timer = setTimeout(() => {
|
|
setDebouncedSearch(searchQuery);
|
|
}, 300);
|
|
return () => clearTimeout(timer);
|
|
}, [searchQuery]);
|
|
|
|
const categories = useMemo(() => [
|
|
{ id: 'shared', label: 'Componentes Shared', icon: Layers },
|
|
{ id: 'ui', label: 'Base UI (Shadcn)', icon: Box },
|
|
{ id: 'icons', label: 'Ícones & Assets', icon: Library },
|
|
{ id: 'layout', label: 'Componentes de Layout', icon: Layers },
|
|
{ id: 'sample', label: 'Exemplos Versionados', icon: FlaskConical },
|
|
], []);
|
|
|
|
const componentsList = useMemo(() => ({
|
|
shared: [
|
|
{
|
|
name: 'ExcelTable (Pixel Perfect)',
|
|
description: 'Tabela de alta performance estilo Excel com filtros avançados e colunas fixas.',
|
|
props: {
|
|
data: [
|
|
{ id: 1, modelo: 'TEO0G02', placa: 'TEO0G02', status: 'Em uso', motorista: 'Larissa Souza', combustivel: 'Flex', tipo: 'Passeio', marca: 'Fiat', ano: '2020', cidade: 'São Paulo' },
|
|
{ id: 2, modelo: 'HFB3A70', placa: 'HFB3A70', status: 'Manutenção', motorista: 'Carlos Silva', combustivel: 'Diesel', tipo: 'Utilitário', marca: 'VW', ano: '2021', cidade: 'Rio' },
|
|
{ id: 3, modelo: 'LUN1968', placa: 'LUN1968', status: 'Em uso', motorista: 'Roberto Alves', combustivel: 'Gasolina', tipo: 'Passeio', marca: 'Ford', ano: '2022', cidade: 'Curitiba' },
|
|
],
|
|
columns: [
|
|
{ header: 'MODELO', field: 'modelo', width: '120px' },
|
|
{ header: 'PLACA', field: 'placa', width: '120px', className: 'text-amber-500 font-bold' },
|
|
{ header: 'STATUS', field: 'status', width: '150px' },
|
|
{ header: 'MOTORISTA', field: 'motorista', width: '200px' },
|
|
{ header: 'COMBUSTÍVEL', field: 'combustivel', width: '120px' },
|
|
],
|
|
filterDefs: [
|
|
{ field: 'status', label: 'Status', type: 'select' },
|
|
{ field: 'combustivel', label: 'Combustível', type: 'select' }
|
|
]
|
|
},
|
|
render: (props) => <Suspense fallback={<ComponentLoader />}><div className="h-[500px] w-full"><ExcelTable {...props} /></div></Suspense>
|
|
},
|
|
{
|
|
name: 'ItemDetailPanel',
|
|
description: 'Painel deslizante modular para detalhes de registros (Clientes, Veículos, etc).',
|
|
props: {
|
|
title: 'Título do Panel',
|
|
subtitle: 'Subtítulo',
|
|
status: 'Status'
|
|
},
|
|
render: (props) => (
|
|
<Suspense fallback={<ComponentLoader />}>
|
|
<div className="relative w-full h-[600px] bg-muted/10 border rounded-xl overflow-hidden flex items-center justify-center">
|
|
<ItemDetailPanel
|
|
title="Item Exemplo"
|
|
subtitle="#123456"
|
|
status="Ativo"
|
|
actions={[
|
|
{ label: 'Editar', onClick: () => {} },
|
|
{ label: 'Deletar', isDestructive: true, onClick: () => {} }
|
|
]}
|
|
tabs={[
|
|
{ id: '1', label: 'Geral', content: <div className="p-4">Conteúdo Geral</div> }
|
|
]}
|
|
{...props}
|
|
/>
|
|
</div>
|
|
</Suspense>
|
|
),
|
|
debugRender: () => <Suspense fallback={<ComponentLoader />}><ItemDetailPanelDebug /></Suspense>
|
|
},
|
|
{
|
|
name: 'StatsGrid',
|
|
description: 'Grade de KPIs com tendências e ícones.',
|
|
props: {
|
|
stats: [
|
|
{ label: 'Frota Total', value: '142', change: '+5', trend: 'up' },
|
|
{ label: 'Em Manutenção', value: '8', change: '-2', trend: 'down' },
|
|
{ label: 'Sinistros Mês', value: '3', change: '+1', trend: 'up' },
|
|
{ label: 'Disponibilidade', value: '94%', change: '+2%', trend: 'up' },
|
|
]
|
|
},
|
|
render: (props) => <Suspense fallback={<ComponentLoader />}><StatsGrid {...props} /></Suspense>
|
|
},
|
|
{
|
|
name: 'DashboardKPICard (Premium)',
|
|
description: 'Card de KPI de alta fidelidade visual com suporte a tendências, ícones customizados e temas.',
|
|
props: {
|
|
label: 'Veículos em uso',
|
|
value: 'R$ 159,2M',
|
|
subtitle: '1033 de 1364 veículos - Total R$ 221,2M',
|
|
trend: '72.0%',
|
|
trendDirection: 'down',
|
|
color: 'blue'
|
|
},
|
|
render: (props) => (
|
|
<Suspense fallback={<ComponentLoader />}>
|
|
<div className="flex items-center justify-center h-full">
|
|
<DashboardKPICard {...props} icon={() => <Box size={24} />} className="w-80" />
|
|
</div>
|
|
</Suspense>
|
|
),
|
|
debugRender: (props) => (
|
|
<Suspense fallback={<ComponentLoader />}>
|
|
<DashboardKPICardDebug {...props} />
|
|
</Suspense>
|
|
)
|
|
},
|
|
{
|
|
name: 'DataTable',
|
|
description: 'Tabela otimizada com busca e paginação.',
|
|
props: {
|
|
data: [
|
|
{ id: 1, name: 'João Silva', role: 'Dev', status: 'Ativo' },
|
|
{ id: 2, name: 'Maria Santos', role: 'Designer', status: 'Inativo' },
|
|
],
|
|
columns: ['ID', 'Nome', 'Cargo', 'Status'],
|
|
searchKey: 'name',
|
|
renderRow: (item) => (
|
|
<TableRow key={item.id}>
|
|
<TableCell className="font-medium">{item.id}</TableCell>
|
|
<TableCell>{item.name}</TableCell>
|
|
<TableCell>{item.role}</TableCell>
|
|
<TableCell>
|
|
<Badge variant={item.status === 'Ativo' ? 'default' : 'secondary'}>
|
|
{item.status}
|
|
</Badge>
|
|
</TableCell>
|
|
</TableRow>
|
|
)
|
|
},
|
|
render: (props) => <Suspense fallback={<ComponentLoader />}><DataTable {...props} /></Suspense>
|
|
},
|
|
{
|
|
name: 'AutoFillInput (Smart Search)',
|
|
description: 'Input inteligente com consulta em tempo real, debounce e autopreenchimento de formulários.',
|
|
props: {
|
|
label: 'Pesquisar Colaborador',
|
|
placeholder: 'Digite nome, cargo ou CPF...',
|
|
filterField: 'name',
|
|
displayField: 'name',
|
|
data: [
|
|
{ id: '101', name: 'Larissa Ribeiro de Souza', role: 'Motorista Categoria D', phone: '(11) 98888-7777', document: '123.456.789-00', registration: 'PR-2024-001' },
|
|
{ id: '102', name: 'Carlos Silva Pereira', role: 'Operador de Empilhadeira', phone: '(11) 97777-6666', document: '987.654.321-11', registration: 'PR-2024-002' },
|
|
{ id: '103', name: 'Roberto Alves da Costa', role: 'Supervisor de Logística', phone: '(11) 96666-5555', document: '456.123.789-22', registration: 'PR-2024-003' },
|
|
{ id: '104', name: 'Ana Beatriz Mendes', role: 'Auxiliar Administrativo', phone: '(11) 95555-4444', document: '321.654.987-33', registration: 'PR-2024-004' },
|
|
{ id: '105', name: 'Marcos Vinícius Jota', role: 'Motorista Carreteiro', phone: '(11) 94444-3333', document: '159.357.258-44', registration: 'PR-2024-005' },
|
|
]
|
|
},
|
|
render: (props) => {
|
|
const TestForm = () => {
|
|
const [formData, setFormData] = React.useState({
|
|
name: '',
|
|
role: '',
|
|
phone: '',
|
|
document: '',
|
|
registration: ''
|
|
});
|
|
|
|
const handleSelect = (item) => {
|
|
setFormData({
|
|
name: item.name,
|
|
role: item.role,
|
|
phone: item.phone,
|
|
document: item.document,
|
|
registration: item.registration
|
|
});
|
|
};
|
|
|
|
const handleAddNew = (term) => {
|
|
const confirmCreate = window.confirm(`Deseja iniciar o cadastro para "${term}"?`);
|
|
if (confirmCreate) {
|
|
setFormData({
|
|
name: term,
|
|
role: 'NOVO CADASTRADO',
|
|
phone: '',
|
|
document: '',
|
|
registration: 'AGUARDANDO...'
|
|
});
|
|
}
|
|
};
|
|
|
|
const resetForm = () => {
|
|
setFormData({ name: '', role: '', phone: '', document: '', registration: '' });
|
|
};
|
|
|
|
return (
|
|
<div className="max-w-2xl mx-auto space-y-8 py-4">
|
|
<div className="bg-emerald-500/5 border border-emerald-500/10 p-6 rounded-2xl">
|
|
<Suspense fallback={<ComponentLoader />}>
|
|
<AutoFillInput
|
|
{...props}
|
|
onSelect={handleSelect}
|
|
onAddNew={handleAddNew}
|
|
/>
|
|
</Suspense>
|
|
<div className="mt-3 flex items-center justify-between">
|
|
<p className="text-[10px] text-slate-500 font-bold uppercase tracking-widest leading-relaxed">
|
|
Teste: Digite "Larissa" ou "Analista"
|
|
</p>
|
|
<Button variant="ghost" size="sm" onClick={resetForm} className="h-6 text-[10px] uppercase font-black italic text-rose-500 hover:bg-rose-500/10">
|
|
Limpar Formulário
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4 animate-in fade-in slide-in-from-top-4 duration-500">
|
|
<div className="space-y-1.5">
|
|
<label className="text-[10px] font-black uppercase text-slate-500 tracking-widest ml-1">Cargo / Função</label>
|
|
<input readOnly value={formData.role} className="w-full bg-slate-500/5 border border-white/5 rounded-lg px-4 py-2.5 text-sm text-slate-400 outline-none" placeholder="Preenchido automaticamente..." />
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<label className="text-[10px] font-black uppercase text-slate-500 tracking-widest ml-1">Telefone</label>
|
|
<input readOnly value={formData.phone} className="w-full bg-slate-500/5 border border-white/5 rounded-lg px-4 py-2.5 text-sm text-slate-400 outline-none" placeholder="(00) 00000-0000" />
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<label className="text-[10px] font-black uppercase text-slate-500 tracking-widest ml-1">CPF / Documento</label>
|
|
<input readOnly value={formData.document} className="w-full bg-slate-500/5 border border-white/5 rounded-lg px-4 py-2.5 text-sm text-slate-400 outline-none" placeholder="000.000.000-00" />
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<label className="text-[10px] font-black uppercase text-slate-500 tracking-widest ml-1">Matrícula</label>
|
|
<input readOnly value={formData.registration} className="w-full bg-slate-500/5 border border-white/5 rounded-lg px-4 py-2.5 text-sm text-slate-400 outline-none" placeholder="REG-000000" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
return <TestForm />;
|
|
}
|
|
},
|
|
{
|
|
name: 'KanbanBoard (Premium)',
|
|
description: 'Quadro Kanban de alta fidelidade para gestão de processos e status.',
|
|
props: {
|
|
columns: [
|
|
{ id: '1', title: 'A FAZER', color: 'blue', cards: [{ id: 1, title: 'TESTE', date: '12/01/2026' }] }
|
|
]
|
|
},
|
|
render: (props) => (
|
|
<Suspense fallback={<ComponentLoader />}>
|
|
<div className="h-[600px] w-full">
|
|
<KanbanBoard {...props} />
|
|
</div>
|
|
</Suspense>
|
|
),
|
|
debugRender: () => (
|
|
<Suspense fallback={<ComponentLoader />}>
|
|
<div className="h-[700px] w-full">
|
|
<KanbanBoardDebug />
|
|
</div>
|
|
</Suspense>
|
|
)
|
|
}
|
|
],
|
|
layout: [
|
|
{
|
|
name: 'Sidebar (Reproduced)',
|
|
description: 'Menu lateral completo reproduzido do Angular. Possui estados colapsáveis, busca e submenus.',
|
|
props: {
|
|
isCollapsed: false
|
|
},
|
|
render: (props) => (
|
|
<Suspense fallback={<ComponentLoader />}>
|
|
<div className="h-[700px] w-full relative bg-slate-900 rounded-xl overflow-hidden p-8">
|
|
<div className="relative h-full w-[260px] translate-x-2 translate-y-2">
|
|
<Sidebar {...props} />
|
|
</div>
|
|
</div>
|
|
</Suspense>
|
|
),
|
|
debugRender: () => (
|
|
<Suspense fallback={<ComponentLoader />}>
|
|
<SidebarDebug />
|
|
</Suspense>
|
|
)
|
|
}
|
|
],
|
|
ui: [
|
|
{
|
|
name: 'Button',
|
|
description: 'Botão padrão do sistema.',
|
|
render: () => (
|
|
<div className="flex flex-wrap gap-4 items-center justify-center h-full">
|
|
<Button variant="default">Primary Button</Button>
|
|
<Button variant="outline">Outline Button</Button>
|
|
<Button variant="destructive">Destructive</Button>
|
|
<Button variant="secondary">Secondary</Button>
|
|
<Button variant="ghost">Ghost Button</Button>
|
|
<Button variant="link">Link Button</Button>
|
|
</div>
|
|
)
|
|
},
|
|
{
|
|
name: 'Badge',
|
|
description: 'Indicadores de status e tags.',
|
|
render: () => (
|
|
<div className="flex gap-4 items-center justify-center h-full">
|
|
<Badge>Default</Badge>
|
|
<Badge variant="secondary">Secondary</Badge>
|
|
<Badge variant="outline">Outline</Badge>
|
|
<Badge variant="destructive">Destructive</Badge>
|
|
<Badge className="bg-emerald-500 hover:bg-emerald-600">Emerald</Badge>
|
|
</div>
|
|
)
|
|
},
|
|
{
|
|
name: 'Card',
|
|
description: 'Container base para conteúdo.',
|
|
render: () => (
|
|
<div className="p-8 flex items-center justify-center h-full">
|
|
<Card className="p-6 w-80 shadow-xl border-white/5 bg-white/5 backdrop-blur-md">
|
|
<h3 className="font-bold mb-2">Premium Card</h3>
|
|
<p className="text-sm text-slate-400">Este é um exemplo de card com efeito de glassmorphism aplicado.</p>
|
|
<Button className="w-full mt-4 bg-emerald-500 hover:bg-emerald-600">Ação</Button>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|
|
],
|
|
icons: [
|
|
{
|
|
name: 'Lucide Icons Set',
|
|
description: 'Conjunto de ícones vetoriais padrão.',
|
|
render: () => (
|
|
<div className="grid grid-cols-6 gap-8 p-8 place-items-center h-full">
|
|
<div className="flex flex-col items-center gap-2"><Plus className="text-emerald-500" /><span className="text-[10px] text-slate-500">Plus</span></div>
|
|
<div className="flex flex-col items-center gap-2"><Search className="text-emerald-500" /><span className="text-[10px] text-slate-500">Search</span></div>
|
|
<div className="flex flex-col items-center gap-2"><Monitor className="text-emerald-500" /><span className="text-[10px] text-slate-500">Monitor</span></div>
|
|
<div className="flex flex-col items-center gap-2"><Smartphone className="text-emerald-500" /><span className="text-[10px] text-slate-500">Mobile</span></div>
|
|
<div className="flex flex-col items-center gap-2"><RotateCcw className="text-emerald-500" /><span className="text-[10px] text-slate-500">Reload</span></div>
|
|
<div className="flex flex-col items-center gap-2"><ExternalLink className="text-emerald-500" /><span className="text-[10px] text-slate-500">Link</span></div>
|
|
</div>
|
|
)
|
|
}
|
|
],
|
|
sample: [
|
|
{
|
|
name: 'StatusBadge',
|
|
description: 'Badge de status versionado. Possui uma versão de produção simplificada e uma de debug com controles extras.',
|
|
props: {
|
|
status: 'active',
|
|
label: 'Ativo'
|
|
},
|
|
render: (props) => (
|
|
<Suspense fallback={<ComponentLoader />}>
|
|
<div className="flex items-center justify-center h-full">
|
|
<StatusBadge {...props} />
|
|
</div>
|
|
</Suspense>
|
|
),
|
|
debugRender: (props) => (
|
|
<Suspense fallback={<ComponentLoader />}>
|
|
<div className="flex items-center justify-center h-full w-full">
|
|
<StatusBadgeDebug {...props} />
|
|
</div>
|
|
</Suspense>
|
|
)
|
|
},
|
|
{
|
|
name: 'FinesCard',
|
|
description: 'Card de KPIs de Multas com comparação mensal e suporte a gráfico de composição.',
|
|
props: {
|
|
currentValue: 850,
|
|
currentCount: 3,
|
|
previousValue: 1100,
|
|
previousCount: 6,
|
|
isLoading: false,
|
|
monthlyData: [
|
|
{ name: 'Jul', value: 1200 },
|
|
{ name: 'Ago', value: 950 },
|
|
{ name: 'Set', value: 1100 },
|
|
{ name: 'Out', value: 800 },
|
|
{ name: 'Nov', value: 1350 },
|
|
{ name: 'Dez', value: 850 },
|
|
],
|
|
data: [
|
|
{ name: 'Excesso de Velocidade', value: 450.00, color: '#ef4444' },
|
|
{ name: 'Estacionamento Irregular', value: 250.00, color: '#f59e0b' },
|
|
{ name: 'Documentação', value: 150.00, color: '#3b82f6' }
|
|
]
|
|
},
|
|
render: (props) => (
|
|
<Suspense fallback={<ComponentLoader />}>
|
|
<div className="flex items-center justify-center h-full">
|
|
<div className="w-[350px] h-[450px]">
|
|
<FinesCard {...props} />
|
|
</div>
|
|
</div>
|
|
</Suspense>
|
|
),
|
|
debugRender: (props) => (
|
|
<Suspense fallback={<ComponentLoader />}>
|
|
<div className="flex items-center justify-center h-full w-full">
|
|
<FinesCardDebug {...props} />
|
|
</div>
|
|
</Suspense>
|
|
)
|
|
}
|
|
]
|
|
}), []);
|
|
|
|
const filteredComponents = useMemo(() =>
|
|
(componentsList[activeCategory] || []).filter(comp =>
|
|
comp.name.toLowerCase().includes(debouncedSearch.toLowerCase())
|
|
),
|
|
[activeCategory, componentsList, debouncedSearch]
|
|
);
|
|
|
|
return (
|
|
<div className={cn(
|
|
"flex h-screen w-full overflow-hidden font-sans transition-colors duration-300",
|
|
theme,
|
|
theme === 'dark' ? "bg-[#0a0a0a] text-slate-200" : "bg-slate-50 text-slate-900"
|
|
)}>
|
|
<SidebarNav
|
|
activeCategory={activeCategory}
|
|
setActiveCategory={setActiveCategory}
|
|
setSelectedComponent={setSelectedComponent}
|
|
theme={theme}
|
|
setTheme={setTheme}
|
|
categories={categories}
|
|
searchQuery={searchQuery}
|
|
setSearchQuery={setSearchQuery}
|
|
filteredComponents={filteredComponents}
|
|
selectedComponent={selectedComponent}
|
|
/>
|
|
|
|
<main className="flex-1 flex flex-col min-w-0">
|
|
<PlaygroundHeader
|
|
selectedComponent={selectedComponent}
|
|
activeCategory={activeCategory}
|
|
theme={theme}
|
|
versionMode={versionMode}
|
|
setVersionMode={setVersionMode}
|
|
viewMode={viewMode}
|
|
setViewMode={setViewMode}
|
|
/>
|
|
|
|
<div className={cn(
|
|
"flex-1 overflow-auto p-12 custom-scrollbar transition-all duration-500",
|
|
theme === 'dark'
|
|
? "bg-[radial-gradient(#1a1a1a_1px,transparent_1px)] [background-size:20px_20px]"
|
|
: "bg-[radial-gradient(#e2e8f0_1px,transparent_1px)] [background-size:20px_20px]",
|
|
versionMode === 'debug' && (theme === 'dark' ? "bg-[radial-gradient(#2e1065_1px,transparent_1px)]" : "bg-[radial-gradient(#e9d5ff_1px,transparent_1px)]")
|
|
)}>
|
|
{selectedComponent ? (
|
|
<div className="mx-auto h-full flex flex-col gap-10">
|
|
<div className={cn(
|
|
"mx-auto rounded-2xl shadow-2xl transition-all duration-500 overflow-hidden relative border",
|
|
viewMode === 'desktop' && "w-full max-w-6xl h-auto min-h-[500px]",
|
|
viewMode === 'tablet' && "w-[768px] h-[1024px] scale-[0.6] origin-top",
|
|
viewMode === 'mobile' && "w-[375px] h-[667px] scale-[0.8] origin-top",
|
|
theme === 'dark' ? "bg-[#111] border-white/5 shadow-black/50" : "bg-white border-slate-200 shadow-slate-200",
|
|
versionMode === 'debug' && "border-purple-500/20 shadow-purple-500/10"
|
|
)}>
|
|
<div className={cn(
|
|
"h-8 border-b flex items-center px-4 gap-1.5",
|
|
theme === 'dark' ? "bg-white/5 border-white/5" : "bg-slate-50 border-slate-200"
|
|
)}>
|
|
<div className="w-2.5 h-2.5 rounded-full bg-red-500/40" />
|
|
<div className="w-2.5 h-2.5 rounded-full bg-yellow-500/40" />
|
|
<div className="w-2.5 h-2.5 rounded-full bg-green-500/40" />
|
|
<div className="ml-4 flex items-center gap-2 text-[10px] text-slate-400 font-mono tracking-tighter truncate opacity-60">
|
|
<span>preview://components/{selectedComponent.name.toLowerCase().replace(/ /g, '-')}</span>
|
|
{versionMode === 'debug' && <span className="text-purple-400 font-bold px-1 bg-purple-500/10 rounded">@DEBUG-BUILD</span>}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-8 h-full overflow-auto relative">
|
|
{versionMode === 'debug' && (
|
|
<div className="absolute inset-0 pointer-events-none opacity-20"
|
|
style={{ backgroundImage: `linear-gradient(#a855f7 1px, transparent 1px), linear-gradient(90deg, #a855f7 1px, transparent 1px)`, backgroundSize: '20px 20px' }}
|
|
/>
|
|
)}
|
|
|
|
{versionMode === 'debug' && selectedComponent.debugRender ? (
|
|
selectedComponent.debugRender(selectedComponent.props || {})
|
|
) : (
|
|
versionMode === 'debug' ? (
|
|
<div className="h-full flex flex-col items-center justify-center">
|
|
<div className="absolute top-4 right-4 bg-orange-500/10 text-orange-500 px-3 py-1 rounded text-[10px] font-bold uppercase border border-orange-500/20">
|
|
Fallback: Production Version
|
|
</div>
|
|
{selectedComponent.render(selectedComponent.props || {})}
|
|
</div>
|
|
) : (
|
|
selectedComponent.render(selectedComponent.props || {})
|
|
)
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="w-full max-w-6xl mx-auto space-y-6 pb-20">
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
<Card className={cn(
|
|
"p-8 lg:col-span-2 border-white/5 shadow-xl",
|
|
theme === 'dark' ? "bg-[#111]" : "bg-white"
|
|
)}>
|
|
<div className="flex items-center gap-3 mb-6">
|
|
<div className="p-2 bg-emerald-500/10 rounded-lg">
|
|
<Info size={18} className="text-emerald-500" />
|
|
</div>
|
|
<h3 className="text-sm font-black uppercase tracking-[0.2em]">{versionMode === 'debug' ? 'Debug Spec & Logs' : 'Blueprint & Contrato'}</h3>
|
|
</div>
|
|
|
|
<p className="text-slate-400 text-sm leading-relaxed mb-8 font-medium">
|
|
{selectedComponent.description}
|
|
</p>
|
|
|
|
<Tabs defaultValue="props" className="w-full">
|
|
<TabsList className={cn(
|
|
"p-1 border h-auto",
|
|
theme === 'dark' ? "bg-black/40 border-white/5" : "bg-slate-100 border-slate-200"
|
|
)}>
|
|
<TabsTrigger value="props" className="px-6 py-2 text-xs font-bold uppercase tracking-wider data-[state=active]:bg-emerald-500 data-[state=active]:text-white">API Props</TabsTrigger>
|
|
<TabsTrigger value="code" className="px-6 py-2 text-xs font-bold uppercase tracking-wider data-[state=active]:bg-emerald-500 data-[state=active]:text-white">
|
|
<Code2 size={14} className="mr-2" /> Source Code
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="props" className="mt-6 animate-in fade-in slide-in-from-bottom-2 duration-300">
|
|
<div className={cn(
|
|
"border rounded-xl overflow-hidden",
|
|
theme === 'dark' ? "border-white/5" : "border-slate-200"
|
|
)}>
|
|
<table className="w-full text-xs text-left border-collapse">
|
|
<thead>
|
|
<tr className={cn(
|
|
"border-b transition-colors",
|
|
theme === 'dark' ? "bg-white/5 text-slate-500 border-white/5" : "bg-slate-50 text-slate-500 border-slate-200"
|
|
)}>
|
|
<th className="px-6 py-4 font-black uppercase tracking-widest text-[10px]">Propriedade</th>
|
|
<th className="px-6 py-4 font-black uppercase tracking-widest text-[10px]">Tipo esperado</th>
|
|
<th className="px-6 py-4 font-black uppercase tracking-widest text-[10px]">Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className={cn(
|
|
"divide-y",
|
|
"divide-white/5"
|
|
)}>
|
|
{Object.keys(selectedComponent.props || {}).map(key => (
|
|
<tr key={key} className="hover:bg-white/5 transition-colors group">
|
|
<td className="px-6 py-4 font-mono text-emerald-500 font-bold">{key}</td>
|
|
<td className="px-6 py-4 text-slate-400 italic">{typeof selectedComponent.props[key]}</td>
|
|
<td className="px-6 py-4">
|
|
<Badge variant="outline" className="text-[9px] border-emerald-500/30 text-emerald-500">DEFINIDO</Badge>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
{(!selectedComponent.props || Object.keys(selectedComponent.props).length === 0) && (
|
|
<tr>
|
|
<td colSpan={3} className="px-6 py-10 text-slate-500 italic text-center">Componente puro ou sem propriedades de teste configuradas.</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="code" className="mt-6 animate-in fade-in slide-in-from-bottom-2 duration-300">
|
|
<div className="bg-[#050505] p-8 rounded-xl border border-white/5 font-mono text-[13px] leading-relaxed overflow-x-auto shadow-2xl">
|
|
<div className="text-emerald-500/40 mb-4 select-none">/* Auto-generated snippet */</div>
|
|
<span className="text-blue-400">const</span> <span className="text-emerald-400">MyComponent</span> <span className="text-white">=</span> <span className="text-white">()</span> <span className="text-blue-400">=></span> <span className="text-white">{`{`}</span>
|
|
<pre className="text-emerald-500/90 mt-2">{` return (
|
|
<${selectedComponent.name.split(' (')[0]}
|
|
${Object.keys(selectedComponent.props || {}).map(k => {
|
|
const val = selectedComponent.props[k];
|
|
let valStr = '';
|
|
try {
|
|
valStr = typeof val === 'function' ? '() => { ... }' : JSON.stringify(val);
|
|
if (!valStr) valStr = 'undefined';
|
|
} catch (e) {
|
|
valStr = '"Could not serialize"';
|
|
}
|
|
return `${k}={${valStr.slice(0, 20)}${valStr.length > 20 ? '...' : ''}}`;
|
|
}).join('\n ')}
|
|
/>
|
|
)`}</pre>
|
|
<span className="text-white">{`}`}</span>
|
|
</div>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</Card>
|
|
|
|
<div className="space-y-6">
|
|
<Card className={cn(
|
|
"p-6 border-white/5 shadow-xl",
|
|
theme === 'dark' ? "bg-[#111]" : "bg-white"
|
|
)}>
|
|
<h4 className="text-[10px] font-black uppercase tracking-[0.3em] text-slate-500 mb-4">Dicas Pro</h4>
|
|
<ul className="space-y-3">
|
|
<li className="flex gap-2 text-[11px] text-slate-400">
|
|
<span className="text-emerald-500 font-bold">01.</span> Use o Toggler de Tema para testar contraste.
|
|
</li>
|
|
<li className="flex gap-2 text-[11px] text-slate-400">
|
|
<span className="text-emerald-500 font-bold">02.</span> Verifique o Mobile View para responsividade.
|
|
</li>
|
|
{versionMode === 'debug' && (
|
|
<li className="flex gap-2 text-[11px] text-purple-400">
|
|
<span className="text-purple-500 font-bold">04.</span> Modo Debug ativo: Visualizando versões experimentais.
|
|
</li>
|
|
)}
|
|
</ul>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="h-full flex flex-col items-center justify-center text-center gap-8 animate-in fade-in zoom-in-95 duration-700">
|
|
<div className="relative">
|
|
<div className="w-32 h-32 bg-emerald-500/5 rounded-[2.5rem] flex items-center justify-center border border-emerald-500/10 shadow-2xl relative z-10">
|
|
<Layers className="text-emerald-500/40" size={64} />
|
|
</div>
|
|
<div className="absolute -inset-4 bg-emerald-500/5 rounded-[3rem] blur-2xl animate-pulse" />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<h2 className="text-2xl font-black uppercase italic tracking-tighter">Laboratório de Componentes</h2>
|
|
<p className="text-slate-500 max-w-sm text-sm font-medium">Selecione um componente na barra lateral para começar a testar comportamentos e designs.</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</main>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default PlaygroundView;
|