From 81e0e68aea441e1f664228fdd563b6470b9728c8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 10 May 2026 07:45:12 +0000 Subject: [PATCH 1/3] Add nav tooltips on desktop + settings toggle (#155) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - NavBtn converted to forwardRef so Carbon Tooltip can inject its event handlers and positioning ref - Carbon Tooltip wraps each nav icon on desktop (>500 px), showing the translated nav.* label on hover/focus; mobile layout unchanged - NavTooltip helper component (outside PageShell) conditionally renders the wrapper to avoid re-mounting on every render - useNavHints() hook exported from PageShell.jsx: reads/writes wl-nav-hints in localStorage (default true), syncs across all hook instances in the same tab via a wl-nav-hints-change custom event - Settings → Utseende: new Carbon Toggle "Vis navigasjonsforklaringer" wired to useNavHints(); change takes effect immediately - i18n: settings.navHints added to nb, en, fa locale files - Docs: CHANGELOG 1.2.1, CLAUDE.md, README.md updated https://claude.ai/code/session_015LGACMKdTtRyAMCXnbUgrZ --- CHANGELOG.md | 5 ++ CLAUDE.md | 4 +- README.md | 2 +- app/public/locales/en/translation.json | 1 + app/public/locales/fa/translation.json | 1 + app/public/locales/nb/translation.json | 1 + app/src/components/PageShell.jsx | 88 ++++++++++++++++++++------ app/src/components/Settings.jsx | 13 +++- 8 files changed, 91 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 297c70b..d3d0083 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to Workout Lens are documented here. +## [1.2.1] — 2026-05-10 + +### Added +- **Nav tooltips (#155)** — hovering or focusing any navigation icon on desktop now shows a Carbon tooltip with the full translated label (works in all three locales). Mobile layout is unchanged. Settings → Utseende has a new "Vis navigasjonsforklaringer" toggle (default: on) that disables tooltips immediately and persists to `localStorage` key `wl-nav-hints` for users who already know the app. `useNavHints()` hook exported from `PageShell.jsx` for shared state across Settings and the nav bar. + ## [1.2.0] — 2026-05-07 ### Open source diff --git a/CLAUDE.md b/CLAUDE.md index 447d17d..cea8dac 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -60,7 +60,7 @@ Fully migrated to IBM Carbon Design System (issue #8, resolved 2026-04-29). - `Home.jsx` → `SectionLabel` + `PageHeading` headings; last session card with gym-class identity hero; 7-day weekly strip with heat colors — clicking a day that has a session navigates to History pre-selected on that date; `fetchThisWeekSessions` in `db.js` - `Report.jsx` → `SectionLabel` eyebrow with period + active day filters on two separate `display:block` spans; three separate `flexWrap: wrap` filter rows (period / weekdays / session types) with `1px solid var(--border-subtle-wl)` top borders between groups; "Nullstill filter" always rendered (opacity-toggled); KPI tiles → heatmap body → hover detail → heat legend → frequency table → gap callout card (with `AccentChip` per untrained muscle) → recommendation button → recs list; when all primary muscles trained shows positive fallback message; when some muscles secondary-only shows those as blue tags; recommendation rows have 3px accent left strip + round `+` button that saves the exercise inline via `saveLibraryExercise`; "Oppdater anbefalinger" ghost button (`Renew` icon) below the recs list — re-runs Claude call and overwrites the cache entry; no `StickyCta`; recs are persisted in the shared `recommendation_cache` Supabase table (see data model) and restored on mount/filter-change via `fetchRecsCache`; prefill prop applied on mount via `useRef` — supports `periodDays`, `selectedDays`, `selectedTypes`, `weekday`, `sessionType`; `KpiTile` (42px Plex Light value); `muscleLastDate` in useMemo - `History.jsx` → custom `MonthGrid` (7-column CSS grid, heat fill, today/selected outlines, month nav); `sessionCountMap` useMemo; `SectionLabel` + `PageHeading` at top; removed `react-day-picker` dependency entirely -- `PageShell.jsx` → exports: `SectionLabel` (mono 12px, 0.16em tracking, 3px `var(--accent)` left border; accepts optional `renderIcon` prop — renders the Carbon icon at 14px before the label text), `PageHeading` (Cond 700 28px), `PageTitle` (alias for SectionLabel), `AccentChip` (magenta pill: `var(--accent-bg-14)` bg, `var(--accent-soft)` text), `StickyCta` (sticky bottom bar with top border), `BackButton`; `NavBtn` active state: 2px `var(--accent)` bottom border + `var(--cds-layer-01)` background; nav icons in order: Camera → RecentlyViewed → Analytics → EventSchedule (Planlegger) → Book (Bibliotek) → Settings — 6 icons, each 48px wide; theme toggle and logout removed from header (now in Settings view); `ChangelogModal` no longer rendered here +- `PageShell.jsx` → exports: `SectionLabel` (mono 12px, 0.16em tracking, 3px `var(--accent)` left border; accepts optional `renderIcon` prop — renders the Carbon icon at 14px before the label text), `PageHeading` (Cond 700 28px), `PageTitle` (alias for SectionLabel), `AccentChip` (magenta pill: `var(--accent-bg-14)` bg, `var(--accent-soft)` text), `StickyCta` (sticky bottom bar with top border), `BackButton`, `useNavHints()` hook (returns `[hints: boolean, toggle(val): void]`; reads/writes `localStorage` key `wl-nav-hints`, defaults `true`; syncs across all instances in the same tab via a `wl-nav-hints-change` custom event); `NavBtn` is a `forwardRef` component spreading `...rest` — required so Carbon `Tooltip` can inject its event handlers; on desktop (>500 px via `useIsMobile`) each nav icon is wrapped in a Carbon `Tooltip align="bottom"` showing the translated `nav.*` label when `useNavHints` returns true; mobile layout is unchanged; nav icons in order: Camera → RecentlyViewed → Analytics → EventSchedule (Planlegger) → Book (Bibliotek) → Settings — 6 icons, each 48px wide; theme toggle and logout removed from header (now in Settings view); `ChangelogModal` no longer rendered here - `carbon-tokens.css` → added `--heat-1..5` green scale (#044317 → #42be65); WL custom tokens: `--accent` (#ee2c80 magenta), `--surface-card`, `--border-subtle-wl`, `--text-muted-wl`, `--accent-bg-08/14/30`, `--accent-soft`, `--r-card` (16px), `--r-pill` (999px), `--r-tile` (10px), `--cond` (IBM Plex Sans Condensed); g10 light-mode overrides for all WL tokens - `app.css` → global `html, body { overflow-x: hidden }` to prevent horizontal viewport bleed from chip rows; do not use `overflow: hidden` on direct parents of `flexWrap: wrap` chip containers — it clips instead of scrolling - Removed: Bebas Neue, DM Sans, Google Fonts import, custom `C` token objects, all raw hex colors, rounded corners, `react-day-picker`, `date-fns` @@ -203,7 +203,7 @@ recommendation_cache - `useIsMobile(breakpoint=500)` — exported hook from `bodymap.jsx`. Below breakpoint: single body view with Front/Bak toggle. Above: side-by-side. Consumed via `BodyPanel` — do not use directly in page components. - **Shared exercise row:** `app/src/components/ExerciseRow.jsx` — renders one editable exercise row (checkbox, inline name edit, sets/reps inputs, delete). Props: `exercise`, `onChange(updates)`, `onDelete()`, `layer` ("layer-01"/"layer-02"), `validateNumbers`, `autoFocusName`, `onNameBlur` (optional callback fired when the name input blurs — used by `ExerciseRowWithAutocomplete` to trigger muscle inference). The outer row div has no click handler — only the Checkbox toggles `enabled` (prevents accidental untick when editing fields). Used by `MuscleMap.jsx`, `History.jsx`, and `TemplateSessionEditor.jsx`. - **Planlegger:** `app/src/components/Planlegger.jsx` — weekly training planner view (issue #59). State: `weekOffset` (±week navigation), `assignments` (`{ [dow 1-7]: template | null }`), `templates`, `weekSessions` (logged sessions for the visible ISO week — issue #143), `pickerDow`, `saving`, `saveError`, `hoveredMuscle`. Computed via `useMemo`: `monday`, `weekIso`, `weekLabel` (built inline with `Intl.DateTimeFormat` for the locale-aware month abbreviation + `t("planlegger.weekLabel", ...)`), `untrainedThisWeekIds` (muscle IDs not trained in any logged session for the visible ISO week — derived from `weekSessions` via `extractMuscles`; issue #143), `projectedExerciseMap` (union of all assigned templates' exercises via `buildMuscleMapFromExercises`), `sessionCount`, `muscleGroupCount`, `untrainedMuscleIds`, `showForslag` (≥2 untrained muscles), `forslagTemplates` (up to 3 templates from library covering untrained muscles). Layout: week nav chevrons → `PageHeading` → `SectionLabel "IKKE TRENT DENNE UKEN"` → wrap row of mono pill chips (History-style: `var(--r-pill)`, `var(--border-subtle-wl)`, `var(--text-muted-wl)`, `var(--cds-font-mono)` 11px) listing muscles not yet trained that week (or a single mono message when all 17 are trained) → `SectionLabel "PROJISERT DEKNING"` → projected `HeatmapBodySVG` (side-by-side/toggle) → fixed-height 48px hover-detail container (always rendered, prevents layout shift) → optional Forslag card → `SectionLabel "UKESPLAN"` → 7 × DayRow → inline `TemplatePicker` bottom-sheet overlay. No sticky save/delete bar — plan auto-saves on every add/remove; `deleteWeekPlan` is called automatically when all slots are cleared. Persists via `fetchWeekPlan` / `saveWeekPlan` / `deleteWeekPlan` in `db.js`; loads logged sessions via `fetchSessionsForWeek` in parallel with the plan fetch. Duration (`N MIN`) omitted — `session_templates` has no duration column. -- **Settings:** `app/src/components/Settings.jsx` — settings view reachable via the gear icon in the header (issue #123). Sections in order: (1) Språk — `RadioButtonGroup` for nb/en/fa; calls `i18n.changeLanguage()` + persists to `localStorage`; (2) Utseende — Carbon `Toggle` for dark/light theme with a live `BodyPanel` preview (fixed sample: primary `chest, quads, lats`; secondary `shoulders_front, hamstrings, triceps`); (3) Kontakt — feedback text + GitHub link; (4) Om appen — version number + "Vis endringslogg" opening `ChangelogModal`; (5) Konto — logged-in email (read-only) + danger logout button. `ChangelogModal` is no longer rendered in `PageShell` — it lives here exclusively. +- **Settings:** `app/src/components/Settings.jsx` — settings view reachable via the gear icon in the header (issue #123). Sections in order: (1) Språk — `RadioButtonGroup` for nb/en/fa; calls `i18n.changeLanguage()` + persists to `localStorage`; (2) Utseende — Carbon `Toggle` for dark/light theme + Carbon `Toggle` for nav hints (`useNavHints()`) with a live `BodyPanel` preview (fixed sample: primary `chest, quads, lats`; secondary `shoulders_front, hamstrings, triceps`); (3) Kontakt — feedback text + GitHub link; (4) Om appen — version number + "Vis endringslogg" opening `ChangelogModal`; (5) Konto — logged-in email (read-only) + danger logout button. `ChangelogModal` is no longer rendered in `PageShell` — it lives here exclusively. - **BodyPanel:** `app/src/components/BodyPanel.jsx` — shared front/back body map. Manages its own `mobileView` toggle state internally. Props: `primary[]`, `secondary[]`, `muscleMap`, `marginBottom`. Replaces the duplicated mobile/desktop render pattern that previously existed in `MuscleMap`, `History`, and `TemplateSessionEditor`. - **MusclePicker:** `app/src/components/MusclePicker.jsx` — interactive body map where clicking a muscle cycles off → primary → secondary → off. Props: `primary[]`, `secondary[]`, `onChange({ primary, secondary })`, `instanceId` (unique suffix to avoid SVG filter ID collisions). Used inside `ExerciseForm.jsx`. - **ExerciseForm:** `app/src/components/ExerciseForm.jsx` — form for creating/editing a library exercise (name, default sets/reps, MusclePicker). Props: `initial`, `onSave(fields)`, `onCancel()`, `saving`. On name field blur, fires `inferMusclesFromName` if no muscles are set yet — shows `InlineLoading` spinner → finished flourish → static AI label. Shows a red warning when name is filled but muscles are still empty. Extracted from inline definition in `Bibliotek.jsx`. diff --git a/README.md b/README.md index 49e083c..2e6798f 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Photograph a handwritten gym whiteboard workout, and the app tells you which mus 8. **Library** — build a named exercise library with click-to-toggle muscle selection; AI muscle inference fires when you type an exercise name and leave the field — muscles are filled in automatically and marked "Muskler satt av AI"; create session templates (e.g. "CrossFit - Anna - mandag") as reusable collections of library exercises 9. **Weekly planner** — assign templates to each day of the week; an "Ikke trent denne uken" chip row lists the muscles you have not yet trained in logged sessions for the visible ISO week (History-style mono pills); a live "Projisert dekning" heatmap body map shows projected cumulative muscle coverage from the assigned templates; a Forslag card flags muscle groups with no planned coverage; plan is saved to Supabase and reloaded on next visit 10. **Language** — switch between Norsk, English and فارسی (RTL) at any time from Settings; all UI strings, date formats, and month names update instantly -11. **Settings** — language selector (top), theme toggle (dark/light) with live body map preview, contact, changelog, and account section: display name input + sign-out (bottom) +11. **Settings** — language selector (top), theme toggle (dark/light) + nav hints toggle with live body map preview, contact, changelog, and account section: display name input + sign-out (bottom) 12. **Joint class history** — when a gym-linked session is expanded in History, a "Kolleger i denne klassen" panel shows co-instructor sessions for the same class slot (display name + exercise list). All sessions are always visible to co-instructors at the same gym — this cross-instructor transparency is the core value of the shared view ## Tech stack diff --git a/app/public/locales/en/translation.json b/app/public/locales/en/translation.json index 5a5e234..950d12f 100644 --- a/app/public/locales/en/translation.json +++ b/app/public/locales/en/translation.json @@ -274,6 +274,7 @@ "darkTheme": "Dark theme", "darkThemeOff": "Off", "darkThemeOn": "On", + "navHints": "Show navigation hints", "account": "Account", "signOut": "Sign out", "displayNameLabel": "Display name", diff --git a/app/public/locales/fa/translation.json b/app/public/locales/fa/translation.json index dc2e4fb..834d6d3 100644 --- a/app/public/locales/fa/translation.json +++ b/app/public/locales/fa/translation.json @@ -274,6 +274,7 @@ "darkTheme": "تم تاریک", "darkThemeOff": "خاموش", "darkThemeOn": "روشن", + "navHints": "نمایش راهنمای ناوبری", "account": "حساب کاربری", "signOut": "خروج", "displayNameLabel": "نام نمایشی", diff --git a/app/public/locales/nb/translation.json b/app/public/locales/nb/translation.json index 857e70b..91e71ce 100644 --- a/app/public/locales/nb/translation.json +++ b/app/public/locales/nb/translation.json @@ -274,6 +274,7 @@ "darkTheme": "Mørkt tema", "darkThemeOff": "Av", "darkThemeOn": "På", + "navHints": "Vis navigasjonsforklaringer", "account": "Konto", "signOut": "Logg ut", "displayNameLabel": "Visningsnavn", diff --git a/app/src/components/PageShell.jsx b/app/src/components/PageShell.jsx index 84c825f..e7dcfe3 100644 --- a/app/src/components/PageShell.jsx +++ b/app/src/components/PageShell.jsx @@ -1,11 +1,39 @@ +import { forwardRef, useState, useEffect } from "react"; import { Camera, RecentlyViewed, Analytics, Book, EventSchedule, Settings, ArrowLeft } from "@carbon/icons-react"; -import { Button } from "@carbon/react"; +import { Button, Tooltip } from "@carbon/react"; import { useTranslation } from "react-i18next"; import { useNav } from "../lib/NavContext"; +import { useIsMobile } from "../lib/bodymap"; -function NavBtn({ onClick, ariaLabel, active, children }) { +export function useNavHints() { + const [hints, setHints] = useState(() => localStorage.getItem("wl-nav-hints") !== "false"); + + useEffect(() => { + function handler() { + setHints(localStorage.getItem("wl-nav-hints") !== "false"); + } + window.addEventListener("storage", handler); + window.addEventListener("wl-nav-hints-change", handler); + return () => { + window.removeEventListener("storage", handler); + window.removeEventListener("wl-nav-hints-change", handler); + }; + }, []); + + function toggle(val) { + localStorage.setItem("wl-nav-hints", val ? "true" : "false"); + window.dispatchEvent(new Event("wl-nav-hints-change")); + setHints(val); + } + + return [hints, toggle]; +} + +const NavBtn = forwardRef(function NavBtn({ onClick, ariaLabel, active, children, ...rest }, ref) { return (
- - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/app/src/components/Settings.jsx b/app/src/components/Settings.jsx index cc4a457..48aded9 100644 --- a/app/src/components/Settings.jsx +++ b/app/src/components/Settings.jsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; import { Toggle, Button, RadioButtonGroup, RadioButton, Tag, TextInput, InlineNotification, Accordion, AccordionItem } from "@carbon/react"; import { ChevronDown, ChevronUp } from "@carbon/icons-react"; import { useTranslation } from "react-i18next"; -import PageShell, { SectionLabel, PageHeading } from "./PageShell"; +import PageShell, { SectionLabel, PageHeading, useNavHints } from "./PageShell"; import BodyPanel from "./BodyPanel"; import { useTheme } from "../theme"; import { supabase } from "../lib/supabase"; @@ -71,6 +71,7 @@ function CollapsibleSection({ label, children }) { export default function Settings() { const { t } = useTranslation(); const { theme, setTheme } = useTheme(); + const [navHints, setNavHints] = useNavHints(); const [userEmail, setUserEmail] = useState(""); const [lang, setLang] = useState(() => localStorage.getItem("wl-lang") || "nb"); const [displayName, setDisplayName] = useState(""); @@ -138,6 +139,16 @@ export default function Settings() { toggled={theme === "g100"} onToggle={(checked) => setTheme(checked ? "g100" : "g10")} /> +
+ +
Date: Sun, 10 May 2026 07:58:58 +0000 Subject: [PATCH 2/3] Speed up tooltip fade-out animation to prevent overlapping text Reduce tooltip transition duration from Carbon's default (200-300ms) to 100ms to avoid overlapping text when tooltips fade out. Uses !important to override Carbon's built-in transition timing. https://claude.ai/code/session_015LGACMKdTtRyAMCXnbUgrZ --- app/src/styles/app.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/styles/app.css b/app/src/styles/app.css index 2fb3ed8..b111daa 100644 --- a/app/src/styles/app.css +++ b/app/src/styles/app.css @@ -38,6 +38,12 @@ input[type=number] { -moz-appearance: textfield; } .cds--select-input { color: var(--cds-text-primary); background-color: var(--cds-field-01); } .cds--select-input:hover { background-color: var(--cds-field-hover); color: var(--cds-text-primary); } +/* Faster tooltip transitions to prevent overlapping text on fade-out */ +.bx--tooltip, +[role="tooltip"] { + transition-duration: 100ms !important; +} + /* ===== RTL (Persian / فارسی) ===== */ [dir="rtl"] { font-family: var(--fa-font); From ecabb90bc13c70b8ed4317308df6ce91ba5c06ca Mon Sep 17 00:00:00 2001 From: Christopher Rotnes Date: Sun, 10 May 2026 10:06:57 +0200 Subject: [PATCH 3/3] Speed up tooltip fade-out animation to prevent overlapping text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace Carbon Tooltip wrapper with a CSS ::after pseudo-element on NavBtn. CSS :hover is mutually exclusive — hovering a new button immediately ends the previous button's hover state, so only one tooltip is ever visible at a time. Removes the dependency on Carbon Tooltip's internal fade timing entirely. - Drop Tooltip import from @carbon/react and NavTooltip wrapper component - Add optional tooltip prop to NavBtn; set data-tooltip attribute when truthy (undefined when hints disabled or on mobile, so attribute is not rendered) - Simplify nav render loop: tt(key) helper returns translated label or undefined based on showTooltip state - app.css: replace broken .bx--tooltip rule (Carbon v10 class, no-op in v11) with [data-tooltip]::after CSS tooltip; 120ms fade-in, instant close on mouse-leave, prefers-reduced-motion safe Co-Authored-By: Claude Sonnet 4.6 --- app/src/components/PageShell.jsx | 60 ++++++++++++-------------------- app/src/styles/app.css | 29 ++++++++++++--- 2 files changed, 47 insertions(+), 42 deletions(-) diff --git a/app/src/components/PageShell.jsx b/app/src/components/PageShell.jsx index e7dcfe3..a931da4 100644 --- a/app/src/components/PageShell.jsx +++ b/app/src/components/PageShell.jsx @@ -1,6 +1,6 @@ import { forwardRef, useState, useEffect } from "react"; import { Camera, RecentlyViewed, Analytics, Book, EventSchedule, Settings, ArrowLeft } from "@carbon/icons-react"; -import { Button, Tooltip } from "@carbon/react"; +import { Button } from "@carbon/react"; import { useTranslation } from "react-i18next"; import { useNav } from "../lib/NavContext"; import { useIsMobile } from "../lib/bodymap"; @@ -29,12 +29,13 @@ export function useNavHints() { return [hints, toggle]; } -const NavBtn = forwardRef(function NavBtn({ onClick, ariaLabel, active, children, ...rest }, ref) { +const NavBtn = forwardRef(function NavBtn({ onClick, ariaLabel, active, tooltip, children, ...rest }, ref) { return (
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + +
diff --git a/app/src/styles/app.css b/app/src/styles/app.css index b111daa..fe5cfa4 100644 --- a/app/src/styles/app.css +++ b/app/src/styles/app.css @@ -38,10 +38,31 @@ input[type=number] { -moz-appearance: textfield; } .cds--select-input { color: var(--cds-text-primary); background-color: var(--cds-field-01); } .cds--select-input:hover { background-color: var(--cds-field-hover); color: var(--cds-text-primary); } -/* Faster tooltip transitions to prevent overlapping text on fade-out */ -.bx--tooltip, -[role="tooltip"] { - transition-duration: 100ms !important; +/* Nav tooltips — CSS ::after approach so only one is ever visible at a time */ +[data-tooltip] { position: relative; } +[data-tooltip]::after { + content: attr(data-tooltip); + position: absolute; + top: calc(100% + 6px); + left: 50%; + transform: translateX(-50%); + background: var(--cds-layer-01); + border: 1px solid var(--cds-border-subtle-01); + color: var(--cds-text-primary); + font-family: var(--cds-font-sans); + font-size: 12px; + line-height: 1; + white-space: nowrap; + padding: 4px 8px; + pointer-events: none; + opacity: 0; + transition: opacity 120ms ease; + z-index: 1000; +} +[data-tooltip]:hover::after, +[data-tooltip]:focus-visible::after { opacity: 1; } +@media (prefers-reduced-motion: reduce) { + [data-tooltip]::after { transition: none; } } /* ===== RTL (Persian / فارسی) ===== */