fix: implement option A for Cpi, adjust LLM config fields stack layout on mobile, update main icons, and fix tests

This commit is contained in:
2026-07-10 14:43:01 +00:00
parent 8a357022c4
commit 892409cee3
5 changed files with 56 additions and 57 deletions
+50 -8
View File
@@ -11,8 +11,8 @@ import DynamicsModule from './pages/DynamicsModule';
import SettingsModule from './pages/SettingsModule';
import TowerModule from './pages/TowerModule';
import {
Wind, Home, Settings, Menu, Cylinder, Church, CircleDot,
Square, Layers, BarChart3, Activity, Building2,
Home, Settings, Menu, Cylinder, Church, CircleDot,
Square, Layers, BarChart3, Activity, Warehouse,
} from 'lucide-react';
import { useState } from 'react';
import { cn } from '@/lib/utils';
@@ -22,6 +22,48 @@ import { useI18n } from './store/i18nStore';
import LanguageSwitcher from './components/LanguageSwitcher';
import { GlobalWindSettingsModal } from './components/GlobalWindSettingsModal';
// Ícones Customizados de Engenharia Premium
const BridgeIcon = (props: React.SVGProps<SVGSVGElement>) => (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={props.className}
>
<path d="M3 11h18" />
<path d="M3 16h18" />
<path d="M6 11v5" />
<path d="M12 11v5" />
<path d="M18 11v5" />
<path d="M3 11c3-3 5-3 9-3s6 0 9 3" />
</svg>
);
const TowerIcon = (props: React.SVGProps<SVGSVGElement>) => (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={props.className}
>
<path d="M9 22L11 2h2l2 20" />
<path d="M10 7h4" />
<path d="M9.5 12h5" />
<path d="M9 17h6" />
<path d="M9 22h6" />
<path d="M11 2l4 5M13 2l-4 5" />
<path d="M11 7l4 5M13 7l-4 5" />
<path d="M10 12l5 5M14 12l-5 5" />
<path d="M9.5 17l5.5 5M14.5 17l-5.5 5" />
</svg>
);
function AppLayout({ children }: { children: React.ReactNode }) {
const location = useLocation();
const [isCollapsed, setIsCollapsed] = useState(false);
@@ -29,15 +71,15 @@ function AppLayout({ children }: { children: React.ReactNode }) {
const navItems = [
{ path: '/', icon: <Home className="w-5 h-5" />, labelKey: 'nav_home' as const },
{ path: '/galpao', icon: <Wind className="w-5 h-5" />, labelKey: 'nav_warehouse' as const },
{ path: '/galpao', icon: <Warehouse className="w-5 h-5" />, labelKey: 'nav_warehouse' as const },
{ path: '/cilindro', icon: <Cylinder className="w-5 h-5" />, labelKey: 'nav_cylinder' as const },
{ path: '/abobada', icon: <Church className="w-5 h-5" />, labelKey: 'nav_vault' as const },
{ path: '/cupula', icon: <CircleDot className="w-5 h-5" />, labelKey: 'nav_dome' as const },
{ path: '/muros', icon: <Square className="w-5 h-5" />, labelKey: 'nav_sign' as const },
{ path: '/cobertura-isolada', icon: <Layers className="w-5 h-5" />, labelKey: 'nav_isolated_roof' as const },
{ path: '/barras', icon: <BarChart3 className="w-5 h-5" />, labelKey: 'nav_bar' as const },
{ path: '/pontes', icon: <Activity className="w-5 h-5" />, labelKey: 'nav_bridge' as const },
{ path: '/torre', icon: <Building2 className="w-5 h-5" />, labelKey: 'nav_tower' as const },
{ path: '/pontes', icon: <BridgeIcon className="w-5 h-5" />, labelKey: 'nav_bridge' as const },
{ path: '/torre', icon: <TowerIcon className="w-5 h-5" />, labelKey: 'nav_tower' as const },
{ path: '/dinamica', icon: <Activity className="w-5 h-5" />, labelKey: 'nav_dynamics' as const },
{ path: '/settings', icon: <Settings className="w-5 h-5" />, labelKey: 'nav_settings' as const },
];
@@ -177,15 +219,15 @@ function AppLayout({ children }: { children: React.ReactNode }) {
function HomeMock() {
const { t } = useI18n();
const modules = [
{ to: '/galpao', icon: Wind, label: 'Galpão Retangular', desc: 'Paredes A/B/C/D, telhados E-J, excentricidade, atrito, alta turbulência.' },
{ to: '/galpao', icon: Warehouse, label: 'Galpão Retangular', desc: 'Paredes A/B/C/D, telhados E-J, excentricidade, atrito, alta turbulência.' },
{ to: '/cilindro', icon: Cylinder, label: 'Cilindro Vertical', desc: 'Silos, reservatórios, chaminés. Cpe por ângulo (Tab. 13), Reynolds.' },
{ to: '/abobada', icon: Church, label: 'Abóbada Cilíndrica', desc: 'Coberturas curvas em arco. Tab. 15-20, 6 zonas.' },
{ to: '/cupula', icon: CircleDot, label: 'Cúpula', desc: 'Sobre terreno (Tab. 21) ou parede cilíndrica (Tab. 22).' },
{ to: '/muros', icon: Square, label: 'Muros e Placas', desc: 'Cf para vento perpendicular e oblíquo (Tab. 23).' },
{ to: '/cobertura-isolada', icon: Layers, label: 'Coberturas Isoladas', desc: 'Uma ou duas águas, abas perpendiculares (Tab. 24-25).' },
{ to: '/barras', icon: BarChart3, label: 'Barras Prismáticas', desc: 'Faces planas (Tab. 26) ou circulares (Tab. 27).' },
{ to: '/pontes', icon: Activity, label: 'Pontes', desc: 'Pse, Cx/Cz do tabuleiro, flutter, galope (sec. 11).' },
{ to: '/torre', icon: Building2, label: 'Torres', desc: 'Torres treliçadas e mastros (sec. 8.4).' },
{ to: '/pontes', icon: BridgeIcon, label: 'Pontes', desc: 'Pse, Cx/Cz do tabuleiro, flutter, galope (sec. 11).' },
{ to: '/torre', icon: TowerIcon, label: 'Torres', desc: 'Torres treliçadas e mastros (sec. 8.4).' },
];
return (
@@ -14,7 +14,6 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { searchStations } from '@/lib/stations-lookup';
import type { PermeabilityCase } from '@/lib/internal-pressure';
import { Settings2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
@@ -29,16 +28,11 @@ export function GlobalWindSettingsModal() {
s2,
vk,
q,
permeabilityCase,
cpiRatio,
cpi,
setV0,
setS1,
setS3,
setS3Group,
setTerrainCategory,
setPermeabilityCase,
setCpiRatio,
} = useWindStore();
const [stationQuery, setStationQuery] = useState('');
@@ -63,7 +57,6 @@ export function GlobalWindSettingsModal() {
<Tabs defaultValue="norma" className="w-full mt-2 min-w-0">
<TabsList className="flex w-full mb-4 bg-muted p-1 text-xs min-w-0">
<TabsTrigger value="norma" className="text-xs py-1 px-1.5 flex-1">NBR 6123</TabsTrigger>
<TabsTrigger value="cpi" className="text-xs py-1 px-1.5 flex-1">Cpi</TabsTrigger>
<TabsTrigger value="local" className="text-xs py-1 px-1.5 flex-1">Local</TabsTrigger>
</TabsList>
@@ -133,46 +126,6 @@ export function GlobalWindSettingsModal() {
</div>
</TabsContent>
<TabsContent value="cpi" className="space-y-4 min-w-0">
<div className="space-y-1">
<label className="text-xs font-medium text-foreground">Caso de Permeabilidade</label>
<Select value={permeabilityCase} onValueChange={(val) => setPermeabilityCase(val as PermeabilityCase)}>
<SelectTrigger className="w-full text-xs h-8"><SelectValue placeholder="Selecione" /></SelectTrigger>
<SelectContent>
<SelectItem value="two-opposite-permeable">Duas faces opostas permeáveis</SelectItem>
<SelectItem value="four-equally-permeable">Quatro faces igualmente permeáveis</SelectItem>
<SelectItem value="dominant-windward">Abertura dominante barlavento</SelectItem>
<SelectItem value="dominant-leeward">Abertura dominante sotavento</SelectItem>
<SelectItem value="dominant-lateral">Abertura dominante lateral</SelectItem>
<SelectItem value="airtight">Edificação estanque</SelectItem>
</SelectContent>
</Select>
</div>
{(permeabilityCase === 'dominant-windward' || permeabilityCase === 'dominant-lateral') && (
<div className="space-y-2 mt-3">
<div className="flex justify-between items-center">
<label className="text-xs font-medium text-foreground">Razão de áreas</label>
<span className="text-xs text-muted-foreground font-mono">{cpiRatio.toFixed(2)}</span>
</div>
<Slider min={0.1} max={5} step={0.05} value={[cpiRatio]} onValueChange={(vals) => setCpiRatio(vals[0])} className="py-1 cursor-pointer" />
<p className="text-[10px] leading-tight text-muted-foreground">
Razão entre a área da abertura dominante e a área total das demais aberturas em faces com sucção externa.
</p>
</div>
)}
<div className="rounded-md border bg-muted/40 p-2 mt-3">
<div className="flex justify-between items-center">
<span className="text-xs font-medium">Cpi calculado:</span>
<Badge variant="default" className="font-mono text-sm">{cpi.toFixed(2)}</Badge>
</div>
<p className="text-[10px] leading-tight text-muted-foreground mt-1">
Limitado a ±0,9 conforme norma. A pressão final usada é p = q · (Cpe Cpi).
</p>
</div>
</TabsContent>
<TabsContent value="local" className="space-y-3 min-w-0">
<div className="space-y-2">
<Input
+2 -2
View File
@@ -159,7 +159,7 @@ describe('M9.8 — Chaves principais em pt-BR', () => {
it('nav_cylinder = Cilindro', () => expect(t('nav_cylinder', 'pt-BR')).toBe('Cilindro'));
it('nav_vault = Abóbada', () => expect(t('nav_vault', 'pt-BR')).toBe('Abóbada'));
it('nav_dome = Cúpula', () => expect(t('nav_dome', 'pt-BR')).toBe('Cúpula'));
it('nav_settings = Configurações', () => expect(t('nav_settings', 'pt-BR')).toBe('Configurações'));
it('nav_settings = Preferências', () => expect(t('nav_settings', 'pt-BR')).toBe('Preferências'));
});
describe('M9.8 — Chaves principais em en-US', () => {
@@ -168,7 +168,7 @@ describe('M9.8 — Chaves principais em en-US', () => {
it('nav_cylinder = Cylinder', () => expect(t('nav_cylinder', 'en-US')).toBe('Cylinder'));
it('nav_vault = Vault', () => expect(t('nav_vault', 'en-US')).toBe('Vault'));
it('nav_dome = Dome', () => expect(t('nav_dome', 'en-US')).toBe('Dome'));
it('nav_settings = Settings', () => expect(t('nav_settings', 'en-US')).toBe('Settings'));
it('nav_settings = Preferences', () => expect(t('nav_settings', 'en-US')).toBe('Preferences'));
});
describe('M9.8 — Conteúdo dos módulos (M9.2-M9.4)', () => {
+2
View File
@@ -5,6 +5,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import { useWindStore } from '@/store/appStore';
import { CpiSettingsCard } from '@/components/CpiSettingsCard';
import { calculateCylinder, type CylinderEndType } from '@/lib/modules/cylinder';
import Cylinder3DViewer from '@/components/three/Cylinder3D';
import SceneCapturePanel from '../components/SceneCapturePanel';
@@ -129,6 +130,7 @@ const CylinderModule: React.FC = () => {
</div>
</CardContent>
</Card>
<CpiSettingsCard />
</div>
{/* Coluna Central: 3D */}
+2
View File
@@ -5,6 +5,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import { useWindStore } from '@/store/appStore';
import { CpiSettingsCard } from '@/components/CpiSettingsCard';
import { calculateDome, type DomeType } from '@/lib/modules/dome';
import Dome3DViewer from '@/components/three/Dome3D';
import SceneCapturePanel from '../components/SceneCapturePanel';
@@ -109,6 +110,7 @@ const DomeModule: React.FC = () => {
</div>
</CardContent>
</Card>
<CpiSettingsCard />
</div>
{/* Coluna Central: 3D */}