feat: version 3.0.0 -- Correcciones de auditoria de seguridad, accesibilidad y redundancia
Permisos (superadmin, viewer, busqueda, tareas recurrentes), bootstrap atomico, revocacion de JWT por version de sesion (028), control optimista de edicion (029), logo sin SVG + CSP, ingesta endurecida, correos escapados, borradores, paginacion de proyectos, accesibilidad de dialogos/controles e i18n. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,47 @@
|
||||
import { DraftMark } from './NavigationGuard.jsx';
|
||||
import { useEffect, useId, useRef } from 'react';
|
||||
|
||||
export function Tabs({ tabs, active, onChange }) {
|
||||
const id = useId();
|
||||
const listRef = useRef(null);
|
||||
const tabRefs = useRef({});
|
||||
useEffect(() => {
|
||||
const panel = listRef.current?.nextElementSibling;
|
||||
if (!panel) return;
|
||||
panel.id = `${id}-panel`;
|
||||
panel.setAttribute('role', 'tabpanel');
|
||||
panel.setAttribute('aria-labelledby', `${id}-${active}`);
|
||||
}, [active, id]);
|
||||
function onKeyDown(event, index) {
|
||||
const delta = event.key === 'ArrowRight' ? 1 : event.key === 'ArrowLeft' ? -1 : 0;
|
||||
const next = event.key === 'Home' ? 0 : event.key === 'End' ? tabs.length - 1 : delta ? (index + delta + tabs.length) % tabs.length : null;
|
||||
if (next === null) return;
|
||||
event.preventDefault();
|
||||
onChange(tabs[next].key);
|
||||
tabRefs.current[tabs[next].key]?.focus();
|
||||
}
|
||||
return (
|
||||
<div className="tabs">
|
||||
{tabs.map((tab) => (
|
||||
<button key={tab.key} className={`tab-item ${active === tab.key ? 'active' : ''}`} onClick={() => onChange(tab.key)}>
|
||||
<div className="tabs" role="tablist" ref={listRef}>
|
||||
{tabs.map((tab, index) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
type="button"
|
||||
id={`${id}-${tab.key}`}
|
||||
ref={(node) => { tabRefs.current[tab.key] = node; }}
|
||||
role="tab"
|
||||
aria-selected={active === tab.key}
|
||||
aria-controls={`${id}-panel`}
|
||||
tabIndex={active === tab.key ? 0 : -1}
|
||||
onKeyDown={(event) => onKeyDown(event, index)}
|
||||
className={`tab-item ${active === tab.key ? 'active' : ''} ${tab.dirty ? 'tab-dirty' : ''}`}
|
||||
onClick={() => onChange(tab.key)}
|
||||
>
|
||||
{tab.label}
|
||||
{/* count=0 SI se muestra (badge "0") -- solo se omite si la pestaña ni siquiera manda
|
||||
la prop (undefined), para pestañas que no tienen numero que mostrar (Info/Gantt). */}
|
||||
{tab.count !== undefined && <span className="tab-badge">{tab.count}</span>}
|
||||
{/* Pestaña con cambios sin guardar (borrador pendiente). */}
|
||||
{tab.dirty && <DraftMark />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user