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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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`.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions app/public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@
"darkTheme": "Dark theme",
"darkThemeOff": "Off",
"darkThemeOn": "On",
"navHints": "Show navigation hints",
"account": "Account",
"signOut": "Sign out",
"displayNameLabel": "Display name",
Expand Down
1 change: 1 addition & 0 deletions app/public/locales/fa/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@
"darkTheme": "تم تاریک",
"darkThemeOff": "خاموش",
"darkThemeOn": "روشن",
"navHints": "نمایش راهنمای ناوبری",
"account": "حساب کاربری",
"signOut": "خروج",
"displayNameLabel": "نام نمایشی",
Expand Down
1 change: 1 addition & 0 deletions app/public/locales/nb/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@
"darkTheme": "Mørkt tema",
"darkThemeOff": "Av",
"darkThemeOn": "På",
"navHints": "Vis navigasjonsforklaringer",
"account": "Konto",
"signOut": "Logg ut",
"displayNameLabel": "Visningsnavn",
Expand Down
48 changes: 40 additions & 8 deletions app/src/components/PageShell.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,41 @@
import { forwardRef, useState, useEffect } from "react";
import { Camera, RecentlyViewed, Analytics, Book, EventSchedule, Settings, ArrowLeft } from "@carbon/icons-react";
import { Button } 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() {

Check warning on line 8 in app/src/components/PageShell.jsx

View workflow job for this annotation

GitHub Actions / Test, Build and Deploy

Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components
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, tooltip, children, ...rest }, ref) {
return (
<button
ref={ref}
{...rest}
aria-label={ariaLabel}
data-tooltip={tooltip || undefined}
onClick={onClick}
style={{
background: active ? "var(--cds-layer-01)" : "none",
Expand All @@ -26,7 +55,7 @@
{children}
</button>
);
}
});

export function SectionLabel({ children, style, renderIcon: Icon }) {
return (
Expand Down Expand Up @@ -127,6 +156,9 @@
export default function PageShell({ children }) {
const { t } = useTranslation();
const { currentView, onShowHome, onShowLogger, onShowHistory, onShowReport, onShowBibliotek, onShowSettings, onShowPlanlegger } = useNav();
const isMobile = useIsMobile();
const [navHints] = useNavHints();
const tt = (key) => (!isMobile && navHints) ? t(key) : undefined;

return (
<div style={{ background: "var(--bg-canvas)", minHeight: "100vh" }}>
Expand Down Expand Up @@ -161,22 +193,22 @@
</button>

<div style={{ display: "flex", alignItems: "center" }}>
<NavBtn ariaLabel={t("nav.logSession")} onClick={onShowLogger} active={currentView === "logger"}>
<NavBtn ariaLabel={t("nav.logSession")} tooltip={tt("nav.logSession")} onClick={onShowLogger} active={currentView === "logger"}>
<Camera size={20} />
</NavBtn>
<NavBtn ariaLabel={t("nav.history")} onClick={onShowHistory} active={currentView === "history"}>
<NavBtn ariaLabel={t("nav.history")} tooltip={tt("nav.history")} onClick={onShowHistory} active={currentView === "history"}>
<RecentlyViewed size={20} />
</NavBtn>
<NavBtn ariaLabel={t("nav.report")} onClick={onShowReport} active={currentView === "report"}>
<NavBtn ariaLabel={t("nav.report")} tooltip={tt("nav.report")} onClick={onShowReport} active={currentView === "report"}>
<Analytics size={20} />
</NavBtn>
<NavBtn ariaLabel={t("nav.planner")} onClick={onShowPlanlegger} active={currentView === "planlegger"}>
<NavBtn ariaLabel={t("nav.planner")} tooltip={tt("nav.planner")} onClick={onShowPlanlegger} active={currentView === "planlegger"}>
<EventSchedule size={20} />
</NavBtn>
<NavBtn ariaLabel={t("nav.library")} onClick={onShowBibliotek} active={currentView === "bibliotek"}>
<NavBtn ariaLabel={t("nav.library")} tooltip={tt("nav.library")} onClick={onShowBibliotek} active={currentView === "bibliotek"}>
<Book size={20} />
</NavBtn>
<NavBtn ariaLabel={t("nav.settings")} onClick={onShowSettings} active={currentView === "settings"}>
<NavBtn ariaLabel={t("nav.settings")} tooltip={tt("nav.settings")} onClick={onShowSettings} active={currentView === "settings"}>
<Settings size={20} />
</NavBtn>
</div>
Expand Down
13 changes: 12 additions & 1 deletion app/src/components/Settings.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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("");
Expand Down Expand Up @@ -138,6 +139,16 @@ export default function Settings() {
toggled={theme === "g100"}
onToggle={(checked) => setTheme(checked ? "g100" : "g10")}
/>
<div style={{ marginTop: 16 }}>
<Toggle
id="nav-hints-toggle"
labelText={t("settings.navHints")}
labelA={t("settings.darkThemeOff")}
labelB={t("settings.darkThemeOn")}
toggled={navHints}
onToggle={setNavHints}
/>
</div>
</div>
<BodyPanel
primary={PREVIEW_PRIMARY}
Expand Down
27 changes: 27 additions & 0 deletions app/src/styles/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,33 @@ 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); }

/* 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 / فارسی) ===== */
[dir="rtl"] {
font-family: var(--fa-font);
Expand Down
Loading