Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .Jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@
## 2025-05-27 - [Unified Loading States and Derived ARIA Labels]
**Learning:** Centralizing loading logic within the `CustomButton` component (using `isPending` and `isPendingText` props) ensures consistent visual feedback and simplifies form components. Furthermore, automatically deriving `aria-label` from `tooltip` for icon-only buttons provides a robust accessibility fallback without requiring repetitive developer effort.
**Action:** Prefer integrated loading states in core button components and use tooltips to automatically populate ARIA labels for icon-only interactive elements.

## 2025-06-17 - [Dynamic Theme Feedback and Functional Branding]
**Learning:** Converting static branding elements into functional navigation links (e.g., logo to dashboard) fulfills standard user expectations. Furthermore, synchronizing the theme toggle's icon and ARIA labels with the active theme via React state, combined with subtle CSS transitions (scale and rotation), creates a more accessible and "delightful" interaction that feels responsive to the user's current environment.
**Action:** Always ensure branding logos are functional links and use dynamic state for theme-related UI elements to provide clear, accessible feedback.
## 2025-02-03 - [Unified Loading Feedback in Buttons and Components]
**Learning:** Moving loading logic (spinners, text, disabled state) into base components like `CustomButton` reduces boilerplate and ensures consistent UX across the app. In Spanish interfaces, using semantic icons like `check_circle` within a `valign-wrapper` in toasts provides clear, professional feedback for critical actions like saving.
**Action:** Abstract loading states into reusable UI components and always include visual markers (icons) for status updates.
Expand Down
34 changes: 23 additions & 11 deletions src/v2/components/V2Navi.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { NavLink } from 'react-router-dom';
import { useState } from 'react';
import { NavLink, Link } from 'react-router-dom';
import '../styles/v2-theme.css';

const V2Navi = () => {
const [theme, setTheme] = useState(document.documentElement.getAttribute('theme') || 'light');
const navItems = [
{ label: "Inicio", icon: "home", path: "/v2/dashboard" },
{ label: "Práctica", icon: "medical_services", path: "/v2/practica" },
Expand All @@ -20,11 +22,23 @@ const V2Navi = () => {
{ label: "Perfil", icon: "person", path: "/v2/perfil" },
];

const toggleTheme = () => {
const newTheme = theme === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('theme', newTheme);
localStorage.setItem('theme', newTheme);
setTheme(newTheme);
};

return (
<nav className="v2-nav-rail" aria-label="Navegación principal">
<div className="v2-nav-logo">
<Link
to="/v2/dashboard"
className="v2-nav-logo"
aria-label="Ir al inicio"
title="Ir al inicio"
>
<i className="material-icons" style={{ fontSize: '32px' }} aria-hidden="true">stethoscope</i>
</div>
</Link>

<div className="v2-nav-items-container">
{navItems.map((item) => (
Expand All @@ -46,15 +60,13 @@ const V2Navi = () => {
<div className="v2-nav-footer">
<button
className="v2-nav-item v2-theme-toggle"
onClick={() => {
const currentTheme = document.documentElement.getAttribute('theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('theme', newTheme);
localStorage.setItem('theme', newTheme);
}}
aria-label="Cambiar tema"
onClick={toggleTheme}
aria-label={`Cambiar a modo ${theme === 'light' ? 'oscuro' : 'claro'}`}
title={`Cambiar a modo ${theme === 'light' ? 'oscuro' : 'claro'}`}
>
<i className="material-icons" aria-hidden="true">dark_mode</i>
<i className="material-icons" aria-hidden="true">
{theme === 'light' ? 'dark_mode' : 'light_mode'}
</i>
</button>
</div>
</nav>
Expand Down
17 changes: 16 additions & 1 deletion src/v2/components/V2Navi.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,29 @@ describe('V2Navi', () => {
</MemoryRouter>
);

const themeButton = screen.getByLabelText('Cambiar tema');
const themeButton = screen.getByLabelText('Cambiar a modo oscuro');
fireEvent.click(themeButton);

expect(setAttributeSpy).toHaveBeenCalledWith('theme', 'dark');
expect(setItemSpy).toHaveBeenCalledWith('theme', 'dark');

// Icon and label should update
expect(screen.getByLabelText('Cambiar a modo claro')).toBeDefined();
expect(screen.getByText('light_mode')).toBeDefined();

setAttributeSpy.mockRestore();
getItemSpy.mockRestore();
setItemSpy.mockRestore();
});

it('renders logo as a link to dashboard', () => {
render(
<MemoryRouter>
<V2Navi />
</MemoryRouter>
);

const logoLink = screen.getByLabelText('Ir al inicio');
expect(logoLink.getAttribute('href')).toBe('/v2/dashboard');
});
});
17 changes: 17 additions & 0 deletions src/v2/styles/v2-theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,23 @@
background: none;
cursor: pointer;
padding: 8px;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

.v2-theme-toggle:hover {
transform: scale(1.1);
}

.v2-theme-toggle:active {
transform: scale(0.95);
}

.v2-theme-toggle i {
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}

.v2-theme-toggle:active i {
transform: rotate(45deg);
}

.v2-content-wrapper {
Expand Down
Loading