From 74c1c7ea7afee1333bd59e6f6bd50151bf22a851 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 15:37:20 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20Enhance=20V2=20naviga?= =?UTF-8?q?tion=20rail=20accessibility=20and=20delight?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit improves the V2 navigation rail (V2Navi) by: - Converting the brand logo from a static div to a functional Link to the dashboard. - Implementing dynamic state for the Theme Toggle to update icons (dark_mode/light_mode) and ARIA labels based on the active theme. - Adding micro-animations (scale and rotation) to the Theme Toggle for a more tactile and delightful experience. - Improving accessibility by adding descriptive aria-labels and titles to the logo and theme toggle. - Updating and expanding unit tests to verify the new functionality. - Updating Palette's journal with learnings from this task. Co-authored-by: godie <227743+godie@users.noreply.github.com> --- .Jules/palette.md | 4 ++++ src/v2/components/V2Navi.jsx | 34 +++++++++++++++++++++---------- src/v2/components/V2Navi.test.jsx | 17 +++++++++++++++- src/v2/styles/v2-theme.css | 17 ++++++++++++++++ 4 files changed, 60 insertions(+), 12 deletions(-) diff --git a/.Jules/palette.md b/.Jules/palette.md index ee2d8f0..7cb7097 100644 --- a/.Jules/palette.md +++ b/.Jules/palette.md @@ -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. diff --git a/src/v2/components/V2Navi.jsx b/src/v2/components/V2Navi.jsx index b60f372..438e470 100644 --- a/src/v2/components/V2Navi.jsx +++ b/src/v2/components/V2Navi.jsx @@ -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" }, @@ -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 ( diff --git a/src/v2/components/V2Navi.test.jsx b/src/v2/components/V2Navi.test.jsx index 2d58a5f..57fa7d3 100644 --- a/src/v2/components/V2Navi.test.jsx +++ b/src/v2/components/V2Navi.test.jsx @@ -46,14 +46,29 @@ describe('V2Navi', () => { ); - 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( + + + + ); + + const logoLink = screen.getByLabelText('Ir al inicio'); + expect(logoLink.getAttribute('href')).toBe('/v2/dashboard'); + }); }); diff --git a/src/v2/styles/v2-theme.css b/src/v2/styles/v2-theme.css index 3669c70..24e6917 100644 --- a/src/v2/styles/v2-theme.css +++ b/src/v2/styles/v2-theme.css @@ -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 {