🚀 Auto-deploy: BrainWind atualizado em 15/07/2026 11:22:37

This commit is contained in:
2026-07-15 11:22:37 +00:00
parent 212dde0e5d
commit d3b1597b74
4 changed files with 331 additions and 50 deletions
+46 -23
View File
@@ -14,8 +14,15 @@ import PiperackModule from './pages/PiperackModule';
import { import {
Home, Settings, Menu, Cylinder, Church, CircleDot, Home, Settings, Menu, Cylinder, Church, CircleDot,
Square, Layers, BarChart3, Activity, Warehouse, Square, Layers, BarChart3, Activity, Warehouse,
Settings2, Sun, Moon, Frame, FolderOpen, BookOpen Settings2, Sun, Moon, Frame, FolderOpen, BookOpen, MoreVertical, Info
} from 'lucide-react'; } from 'lucide-react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useState } from 'react'; import { useState } from 'react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@@ -220,16 +227,48 @@ function AppLayout({ children }: { children: React.ReactNode }) {
> >
{effectiveTheme === 'dark' ? <Sun className="h-4.5 w-4.5" /> : <Moon className="h-4.5 w-4.5" />} {effectiveTheme === 'dark' ? <Sun className="h-4.5 w-4.5" /> : <Moon className="h-4.5 w-4.5" />}
</Button> </Button>
<Link <DropdownMenu>
to="/settings" <DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className={cn( className={cn(
'h-8 w-8 flex items-center justify-center text-muted-foreground hover:text-foreground shrink-0 rounded-full border border-border/40 bg-background/50', 'h-8 w-8 text-muted-foreground hover:text-foreground shrink-0 rounded-full border border-border/40 bg-background/50',
location.pathname === '/settings' && 'text-primary border-primary/20 bg-primary/5' (location.pathname === '/settings' || location.pathname === '/arquivos') && 'text-primary border-primary/20 bg-primary/5'
)} )}
title={t('nav_settings')}
> >
<Settings className="h-4.5 w-4.5" /> <MoreVertical className="h-4.5 w-4.5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<GlobalWindSettingsModal trigger={
<DropdownMenuItem onSelect={(e: Event) => e.preventDefault()} className="cursor-pointer gap-2">
<Settings2 className="w-4 h-4" /> Parâmetros do Vento
</DropdownMenuItem>
} />
<DropdownMenuItem asChild>
<Link to="/settings?tab=gerenciador" className="cursor-pointer gap-2">
<FolderOpen className="w-4 h-4" /> Arquivos
</Link> </Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to="/settings" className="cursor-pointer gap-2">
<Settings className="w-4 h-4" /> Configurações
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to="/settings?tab=glossary" className="cursor-pointer gap-2">
<BookOpen className="w-4 h-4" /> Glossário
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<TermsOfUseDialog trigger={
<DropdownMenuItem onSelect={(e: Event) => e.preventDefault()} className="cursor-pointer gap-2">
<Info className="w-4 h-4" /> Termos de Uso
</DropdownMenuItem>
} />
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
</header> </header>
<div className="flex-1 overflow-auto">{children}</div> <div className="flex-1 overflow-auto">{children}</div>
@@ -280,22 +319,6 @@ function AppLayout({ children }: { children: React.ReactNode }) {
); );
})} })}
</div> </div>
{/* Parâmetros do Vento (Fixo na direita) */}
<GlobalWindSettingsModal
trigger={
<div
className={cn(
'flex flex-col items-center justify-center h-full px-3 text-xs transition-colors shrink-0 border-l bg-card z-20 shadow-[-4px_0_8px_-4px_rgba(0,0,0,0.15)] cursor-pointer text-muted-foreground hover:text-primary',
)}
>
<div className="p-1 rounded-full">
<Settings2 className="w-5 h-5 text-primary" />
</div>
<span className="scale-90 text-[10px] text-primary font-medium">Vento</span>
</div>
}
/>
</nav> </nav>
</div> </div>
); );
+4 -1
View File
@@ -6,14 +6,16 @@ import { useState } from 'react';
interface TermsOfUseDialogProps { interface TermsOfUseDialogProps {
isCollapsed?: boolean; isCollapsed?: boolean;
trigger?: React.ReactNode;
} }
export function TermsOfUseDialog({ isCollapsed = false }: TermsOfUseDialogProps) { export function TermsOfUseDialog({ isCollapsed = false, trigger }: TermsOfUseDialogProps) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
{trigger ? trigger : (
<button <button
className={cn( className={cn(
'flex items-center gap-3 px-3 py-2.5 rounded-md transition-colors text-sm w-full cursor-pointer text-muted-foreground hover:bg-secondary/50 hover:text-secondary-foreground', 'flex items-center gap-3 px-3 py-2.5 rounded-md transition-colors text-sm w-full cursor-pointer text-muted-foreground hover:bg-secondary/50 hover:text-secondary-foreground',
@@ -24,6 +26,7 @@ export function TermsOfUseDialog({ isCollapsed = false }: TermsOfUseDialogProps)
<Info className="w-5 h-5 shrink-0" /> <Info className="w-5 h-5 shrink-0" />
{!isCollapsed && <span>Termos de Uso</span>} {!isCollapsed && <span>Termos de Uso</span>}
</button> </button>
)}
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-w-2xl max-h-[85vh] p-0 flex flex-col"> <DialogContent className="max-w-2xl max-h-[85vh] p-0 flex flex-col">
+255
View File
@@ -0,0 +1,255 @@
import * as React from "react"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
)
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!",
className
)}
{...props}
/>
)
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
)
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[inset]:pl-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
)
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
className
)}
{...props}
/>
)
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}
+12 -12
View File
@@ -82,7 +82,7 @@ describe('M9.7 — parseProjectJson', () => {
describe('M9.7 — detectFormat', () => { describe('M9.7 — detectFormat', () => {
it('Detecta SavedProject', () => { it('Detecta SavedProject', () => {
expect(detectFormat({ module: 'galpao', inputs: {} })).toBe('saved-project'); expect(detectFormat({ type: 'projeto', module: 'galpao', inputs: {} })).toBe('saved-project');
}); });
it('Detecta snapshot do windStore', () => { it('Detecta snapshot do windStore', () => {
@@ -106,7 +106,7 @@ describe('M9.7 — validateSavedProject', () => {
it('Aceita SavedProject válido', () => { it('Aceita SavedProject válido', () => {
const project = { const project = {
name: 'Galpão Teste', name: 'Galpão Teste',
module: 'galpao', type: 'projeto', module: 'galpao',
inputs: {}, inputs: {},
createdAt: 1000, createdAt: 1000,
updatedAt: 2000, updatedAt: 2000,
@@ -117,28 +117,28 @@ describe('M9.7 — validateSavedProject', () => {
}); });
it('Rejeita projeto sem name', () => { it('Rejeita projeto sem name', () => {
const project = { module: 'galpao', inputs: {} }; const project = { type: 'projeto', module: 'galpao', inputs: {} };
const v = validateSavedProject(project); const v = validateSavedProject(project);
expect(v.ok).toBe(false); expect(v.ok).toBe(false);
expect(v.errors.some((e) => e.includes('name'))).toBe(true); expect(v.errors.some((e) => e.includes('name'))).toBe(true);
}); });
it('Rejeita módulo inválido', () => { it('Rejeita módulo inválido', () => {
const project = { name: 'X', module: 'invalido', inputs: {} }; const project = { name: 'X', type: 'projeto', module: 'invalido', inputs: {} };
const v = validateSavedProject(project); const v = validateSavedProject(project);
expect(v.ok).toBe(false); expect(v.ok).toBe(false);
expect(v.errors.some((e) => e.includes('module'))).toBe(true); expect(v.errors.some((e) => e.includes('module'))).toBe(true);
}); });
it('Rejeita inputs não-objeto', () => { it('Rejeita inputs não-objeto', () => {
const project = { name: 'X', module: 'galpao', inputs: 'não-objeto' }; const project = { name: 'X', type: 'projeto', module: 'galpao', inputs: 'não-objeto' };
const v = validateSavedProject(project); const v = validateSavedProject(project);
expect(v.ok).toBe(false); expect(v.ok).toBe(false);
expect(v.errors.some((e) => e.includes('inputs'))).toBe(true); expect(v.errors.some((e) => e.includes('inputs'))).toBe(true);
}); });
it('Emite warning se timestamps faltarem', () => { it('Emite warning se timestamps faltarem', () => {
const project = { name: 'X', module: 'galpao', inputs: {} }; const project = { name: 'X', type: 'projeto', module: 'galpao', inputs: {} };
const v = validateSavedProject(project); const v = validateSavedProject(project);
expect(v.warnings.length).toBeGreaterThan(0); expect(v.warnings.length).toBeGreaterThan(0);
}); });
@@ -177,7 +177,7 @@ describe('M9.7 — applySavedProject', () => {
it('Aplica wind.v0 corretamente', () => { it('Aplica wind.v0 corretamente', () => {
const project: SavedProject = { const project: SavedProject = {
name: 'Teste', name: 'Teste',
module: 'galpao', type: 'projeto', module: 'galpao',
inputs: { wind: { v0: 50 } }, inputs: { wind: { v0: 50 } },
createdAt: Date.now(), createdAt: Date.now(),
updatedAt: Date.now(), updatedAt: Date.now(),
@@ -190,7 +190,7 @@ describe('M9.7 — applySavedProject', () => {
it('Aplica múltiplos campos do windStore', () => { it('Aplica múltiplos campos do windStore', () => {
const project: SavedProject = { const project: SavedProject = {
name: 'Teste', name: 'Teste',
module: 'galpao', type: 'projeto', module: 'galpao',
inputs: { inputs: {
wind: { wind: {
v0: 45, v0: 45,
@@ -215,7 +215,7 @@ describe('M9.7 — applySavedProject', () => {
it('Aplica galpaoStore quando module=galpao', () => { it('Aplica galpaoStore quando module=galpao', () => {
const project: SavedProject = { const project: SavedProject = {
name: 'Galpão', name: 'Galpão',
module: 'galpao', type: 'projeto', module: 'galpao',
inputs: { inputs: {
galpao: { galpao: {
width: 20, width: 20,
@@ -243,7 +243,7 @@ describe('M9.7 — applySavedProject', () => {
it('Não aplica galpaoStore quando module ≠ galpao', () => { it('Não aplica galpaoStore quando module ≠ galpao', () => {
const project: SavedProject = { const project: SavedProject = {
name: 'Cilindro', name: 'Cilindro',
module: 'cilindro', type: 'projeto', module: 'cilindro',
inputs: { galpao: { width: 20 } }, inputs: { galpao: { width: 20 } },
createdAt: Date.now(), createdAt: Date.now(),
updatedAt: Date.now(), updatedAt: Date.now(),
@@ -255,7 +255,7 @@ describe('M9.7 — applySavedProject', () => {
it('Adiciona warning para categoria inválida', () => { it('Adiciona warning para categoria inválida', () => {
const project: SavedProject = { const project: SavedProject = {
name: 'Teste', name: 'Teste',
module: 'galpao', type: 'projeto', module: 'galpao',
inputs: { wind: { terrainCategory: 'INVALID' } }, inputs: { wind: { terrainCategory: 'INVALID' } },
createdAt: Date.now(), createdAt: Date.now(),
updatedAt: Date.now(), updatedAt: Date.now(),
@@ -281,7 +281,7 @@ describe('M9.7 — importProjectFromText (orquestrador)', () => {
it('Roundtrip: export → import preserva campos principais', () => { it('Roundtrip: export → import preserva campos principais', () => {
const project: SavedProject = { const project: SavedProject = {
name: 'Roundtrip', name: 'Roundtrip',
module: 'galpao', type: 'projeto', module: 'galpao',
inputs: { inputs: {
wind: { v0: 45, s1: 1, s3Group: 2 }, wind: { v0: 45, s1: 1, s3Group: 2 },
galpao: { width: 18, length: 35, height: 7 }, galpao: { width: 18, length: 35, height: 7 },