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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

All notable changes to Workout Lens are documented here.

## [1.5.14] — 2026-05-17

### Developer / Infrastructure
- **Resolve all ESLint warnings (issue #253)** — `npm run lint` now exits with 0 problems. Changes are purely structural with no runtime impact:
- Split `bodymap.jsx` into `bodymap.js` (constants, `MUSCLES`, `SHAPES`, `EX_DB`, `calcMuscles`, `useIsMobile`, color constants) and `bodymap.jsx` (only `BodySVG` + `HeatmapBodySVG`); fixes 5 fast-refresh warnings.
- Moved `useTheme` and `ThemeCtx` from `theme.jsx` to `hooks.js`; `theme.jsx` now exports only `ThemeProvider`; fixes 1 fast-refresh warning.
- Moved `useNavHints` from `PageShell.jsx` to `hooks.js`; fixes 1 fast-refresh warning.
- Removed the standalone `useEffect` in `App.jsx` that called `setIntroOpen` synchronously — merged the intro check into the existing Supabase auth event handlers; fixes 1 `set-state-in-effect` warning.

## [1.5.13] — 2026-05-16

### Developer / Infrastructure
Expand Down
20 changes: 11 additions & 9 deletions CLAUDE.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion app/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "workout-lens",
"private": true,
"version": "1.5.13",
"version": "1.5.14",
"author": "Christopher Rotnes",
"license": "MIT",
"repository": {
Expand Down
10 changes: 5 additions & 5 deletions app/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,19 @@ function App() {
};
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session);
if (session) runEnsures(session.user);
if (session) {
runEnsures(session.user);
if (!localStorage.getItem("wl-intro-seen")) setIntroOpen(true);
}
});
const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => {
setSession(session);
if (session && !localStorage.getItem("wl-intro-seen")) setIntroOpen(true);
if ((event === "SIGNED_IN" || event === "INITIAL_SESSION") && session) runEnsures(session.user);
});
return () => subscription.unsubscribe();
}, []);

useEffect(() => {
if (session && !localStorage.getItem("wl-intro-seen")) setIntroOpen(true);
}, [session]);

function handleShowIntro() {
localStorage.removeItem("wl-intro-seen");
setIntroOpen(true);
Expand Down
3 changes: 2 additions & 1 deletion app/src/components/BodyPanel.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useState } from "react";
import { Button } from "@carbon/react";
import { useTranslation } from "react-i18next";
import { BodySVG, useIsMobile } from "../lib/bodymap.jsx";
import { BodySVG } from "../lib/bodymap.jsx";
import { useIsMobile } from "../lib/bodymap";

// Renders a front+back body map pair: side-by-side on desktop, toggled on mobile.
// Manages its own mobile view state so parents don't need to.
Expand Down
2 changes: 1 addition & 1 deletion app/src/components/ChangelogModal.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Modal, Accordion, AccordionItem, Theme } from "@carbon/react";
import { useTheme } from "../theme";
import { useTheme } from "../lib/hooks";
import { CHANGELOG } from "../lib/changelog";

const MONTHS = [
Expand Down
2 changes: 1 addition & 1 deletion app/src/components/ExFlyt.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useState, useMemo } from "react";
import { Button } from "@carbon/react";
import { Add, Close, Search } from "@carbon/icons-react";
import { useTranslation } from "react-i18next";
import { MUSCLES } from "../lib/bodymap.jsx";
import { MUSCLES } from "../lib/bodymap";
import { AccentChip } from "./PageShell";

export default function ExFlyt({ libraryExercises, onAdd, onClose }) {
Expand Down
2 changes: 1 addition & 1 deletion app/src/components/GruppetimeEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import PageShell, { SectionLabel, BackButton } from "./PageShell";
import BodyPanel from "./BodyPanel";
import ExerciseRowWithAutocomplete from "./ExerciseRowWithAutocomplete";
import ExFlyt from "./ExFlyt";
import { MUSCLES } from "../lib/bodymap.jsx";
import { MUSCLES } from "../lib/bodymap";
import { buildMuscleMapFromExercises, logDevError, getIntlLocale } from "../lib/utils";
import { fetchLibraryExercises, replaceTemplateExercises, updateTemplateDetails, touchTemplate } from "../lib/db";

Expand Down
2 changes: 1 addition & 1 deletion app/src/components/History.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState, useEffect, useRef, useMemo } from "react";
import { fetchSessions, fetchSessionsByDate, fetchGymSessionsByDate, updateSession, fetchLibraryExercises, fetchClassHistory } from "../lib/db";
import { MUSCLES, calcMuscles } from "../lib/bodymap.jsx";
import { MUSCLES, calcMuscles } from "../lib/bodymap";
import { compressImage, buildMuscleMapFromSession, buildMuscleMapFromExercises, callClaude, extractMuscles, logDevError, getIntlLocale, toIsoDate } from "../lib/utils";
import { CLAUDE_MODEL_VISION, ANALYZE_PROMPT } from "../lib/prompts";
import {
Expand Down
2 changes: 1 addition & 1 deletion app/src/components/IntroModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useState } from "react";
import { Modal, Button, Theme } from "@carbon/react";
import { Camera, RecentlyViewed, Analytics, EventSchedule, Notebook } from "@carbon/icons-react";
import { useTranslation } from "react-i18next";
import { useTheme } from "../theme";
import { useTheme } from "../lib/hooks";
import { PageHeading } from "./PageShell";

const SLIDES = [
Expand Down
2 changes: 1 addition & 1 deletion app/src/components/MuscleMap.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useReducer, useRef, useCallback, useEffect, useMemo, useState } from "react";
import { saveSession, fetchGymSessionsByDate, checkGymCalendarConflict, fetchLibraryExercises } from "../lib/db";
import { EX_DB, MUSCLES, calcMuscles } from "../lib/bodymap.jsx";
import { EX_DB, MUSCLES, calcMuscles } from "../lib/bodymap";
import { compressImage, buildMuscleMapFromExercises, callClaude, logDevError } from "../lib/utils";
import { CLAUDE_MODEL_VISION, CLAUDE_MODEL_TEXT, ANALYZE_PROMPT, buildRecommendPrompt } from "../lib/prompts";
import { InlineLoading } from "@carbon/react";
Expand Down
2 changes: 1 addition & 1 deletion app/src/components/MuscleMapConfirm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Button, Select, SelectItem, DatePicker, DatePickerInput, InlineNotifica
import { Add, ArrowLeft, ArrowRight, Edit } from "@carbon/icons-react";
import ExerciseRowWithAutocomplete from "./ExerciseRowWithAutocomplete";
import { SectionLabel, StickyCta } from "./PageShell";
import { EX_DB } from "../lib/bodymap.jsx";
import { EX_DB } from "../lib/bodymap";
import { toIsoDate, getIntlLocale } from "../lib/utils";
import { useTranslation } from "react-i18next";

Expand Down
2 changes: 1 addition & 1 deletion app/src/components/MuscleMapResult.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Button, Tag, InlineNotification, InlineLoading, DefinitionTooltip } fro
import { AiRecommend, Renew } from "@carbon/icons-react";
import BodyPanel from "./BodyPanel";
import { AccentChip } from "./PageShell";
import { MUSCLES } from "../lib/bodymap.jsx";
import { MUSCLES } from "../lib/bodymap";
import { buildRecMuscleMap } from "../lib/utils";
import { useTranslation } from "react-i18next";

Expand Down
2 changes: 1 addition & 1 deletion app/src/components/MusclePicker.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { MUSCLES, SHAPES, BODY_PATH, PRIMARY_FILL, PRIMARY_HOVER, PRIMARY_STROKE, SEC_STROKE, useIsMobile } from "../lib/bodymap.jsx";
import { MUSCLES, SHAPES, BODY_PATH, PRIMARY_FILL, PRIMARY_HOVER, PRIMARY_STROKE, SEC_STROKE, useIsMobile } from "../lib/bodymap";
import { Tag } from "@carbon/react";

function MusclePickerView({ view, primary, secondary, onToggle, instanceId }) {
Expand Down
2 changes: 1 addition & 1 deletion app/src/components/OvelsePicker.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Add, Search, ChevronRight } from "@carbon/icons-react";
import { useTranslation } from "react-i18next";
import PageShell, { SectionLabel, BackButton } from "./PageShell";
import ExerciseForm from "./ExerciseForm";
import { MUSCLES } from "../lib/bodymap.jsx";
import { MUSCLES } from "../lib/bodymap";
import { fetchLibraryExercises, saveLibraryExercise, updateLibraryExercise, fetchExerciseTemplateCounts } from "../lib/db";
import { logDevError } from "../lib/utils";
import { useDebouncedSearch } from "../lib/hooks";
Expand Down
27 changes: 2 additions & 25 deletions app/src/components/PageShell.jsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,10 @@
import { forwardRef, useState, useEffect } from "react";
import { forwardRef } from "react";
import { Camera, RecentlyViewed, Analytics, Notebook, 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";

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];
}
import { useNavHints } from "../lib/hooks";

const NavBtn = forwardRef(function NavBtn({ onClick, ariaLabel, active, tooltip, children, ...rest }, ref) {
return (
Expand Down
3 changes: 2 additions & 1 deletion app/src/components/Planlegger.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { ChevronLeft, ChevronRight, Add, Close, Search } from "@carbon/icons-rea
import { useTranslation } from "react-i18next";
import { fetchWeekPlan, saveWeekPlan, deleteWeekPlan, fetchTemplates } from "../lib/db";
import { buildMuscleMapFromExercises, toWeekIso, logDevError, getIntlLocale } from "../lib/utils";
import { calcMuscles, MUSCLES, HeatmapBodySVG, useIsMobile } from "../lib/bodymap.jsx";
import { HeatmapBodySVG } from "../lib/bodymap.jsx";
import { calcMuscles, MUSCLES, useIsMobile } from "../lib/bodymap";
import PageShell, { SectionLabel, AccentChip } from "./PageShell";
import { useDebouncedSearch } from "../lib/hooks";

Expand Down
3 changes: 2 additions & 1 deletion app/src/components/Report.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useState, useEffect, useRef, useMemo } from "react";
import { fetchSessionsForReport, saveLibraryExercise, fetchRecsCache, saveRecsCache } from "../lib/db";
import { HeatmapBodySVG, MUSCLES } from "../lib/bodymap.jsx";
import { HeatmapBodySVG } from "../lib/bodymap.jsx";
import { MUSCLES } from "../lib/bodymap";
import { callClaude, logDevError, getIntlLocale } from "../lib/utils";
import { CLAUDE_MODEL_TEXT, buildPeriodRecommendPrompt, RECS_PROMPT_VERSION } from "../lib/prompts";
import {
Expand Down
2 changes: 1 addition & 1 deletion app/src/components/SessionEditPanel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Camera, Add, Renew } from "@carbon/icons-react";
import ExerciseRowWithAutocomplete from "./ExerciseRowWithAutocomplete";
import BodyPanel from "./BodyPanel";
import { checkGymCalendarConflict } from "../lib/db";
import { MUSCLES } from "../lib/bodymap.jsx";
import { MUSCLES } from "../lib/bodymap";
import { getIntlLocale } from "../lib/utils";
import { useTranslation } from "react-i18next";

Expand Down
4 changes: 2 additions & 2 deletions app/src/components/Settings.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useEffect, useState } from "react";
import { ArrowLeft } from "@carbon/icons-react";
import { useTranslation } from "react-i18next";
import PageShell, { useNavHints } from "./PageShell";
import { useTheme } from "../theme";
import PageShell from "./PageShell";
import { useTheme, useNavHints } from "../lib/hooks";
import { supabase } from "../lib/supabase";
import { fetchDisplayName, updateDisplayName } from "../lib/db";
import i18n from "../lib/i18n";
Expand Down
2 changes: 1 addition & 1 deletion app/src/components/TemplateSessionEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Add, ArrowLeft, ArrowRight, Save, Edit } from "@carbon/icons-react";
import { useTranslation } from "react-i18next";
import PageShell, { SectionLabel, BackButton } from "./PageShell";
import { fetchLibraryExercises, replaceTemplateExercises, touchTemplate, updateTemplateName } from "../lib/db";
import { calcMuscles } from "../lib/bodymap.jsx";
import { calcMuscles } from "../lib/bodymap";
import { buildMuscleMapFromExercises, logDevError } from "../lib/utils";
import ExerciseRowWithAutocomplete from "./ExerciseRowWithAutocomplete";
import BodyPanel from "./BodyPanel";
Expand Down
2 changes: 1 addition & 1 deletion app/src/lib/__tests__/bodymap.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { calcMuscles } from '../bodymap.jsx';
import { calcMuscles } from '../bodymap';

describe('calcMuscles', () => {
it('returns empty arrays for empty input', () => {
Expand Down
2 changes: 1 addition & 1 deletion app/src/lib/__tests__/prompts.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { MUSCLES } from '../bodymap.jsx';
import { MUSCLES } from '../bodymap';
import {
ANALYZE_PROMPT,
buildRecommendPrompt,
Expand Down
115 changes: 115 additions & 0 deletions app/src/lib/bodymap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import React from "react";

export const EX_DB = [
{ kw: ["benkpress","bench press","chest press","push up","pushup","armhevinger","brystpress","flies","fly","pec deck"], p: ["chest"], s: ["shoulders_front","triceps"] },
{ kw: ["skulderpress","shoulder press","overhead press","ohp","militærpress","military press","arnold"], p: ["shoulders_front","shoulders_side"], s: ["triceps","traps"] },
{ kw: ["sidehev","lateral raise","lateral"], p: ["shoulders_side"], s: [] },
{ kw: ["fronthev","front raise"], p: ["shoulders_front"], s: [] },
{ kw: ["face pull","rear delt","bakre delt"], p: ["rear_delts","traps"], s: [] },
{ kw: ["pullup","pull-up","chin up","chinup","chins"], p: ["lats","biceps"], s: ["rear_delts"] },
{ kw: ["pulldown","nedtrekk","lat pull"], p: ["lats"], s: ["biceps"] },
{ kw: ["roing","row","rodd","t-bar"], p: ["lats","rear_delts"], s: ["biceps","traps"] },
{ kw: ["markløft","deadlift","rdl","romanian","stiff leg"], p: ["hamstrings","glutes","lower_back"], s: ["traps","lats"] },
{ kw: ["knebøy","squat","goblet"], p: ["quads","glutes"], s: ["hamstrings","calves"] },
{ kw: ["leg press","beinpress","hack squat"], p: ["quads","glutes"], s: ["hamstrings"] },
{ kw: ["lunge","utfall","step up","bulgarian"], p: ["quads","glutes"], s: ["hamstrings","calves"] },
{ kw: ["leg curl","hamstring curl","bein curl"], p: ["hamstrings"], s: [] },
{ kw: ["hip thrust","glute bridge"], p: ["glutes"], s: ["hamstrings"] },
{ kw: ["bicep curl","curl","hammer curl","preacher"], p: ["biceps"], s: ["forearms"] },
{ kw: ["tricep","skull crusher","pushdown","dip"], p: ["triceps"], s: ["shoulders_front"] },
{ kw: ["planke","plank"], p: ["abs","obliques"], s: ["lower_back"] },
{ kw: ["situp","sit up","crunch","cable crunch"], p: ["abs"], s: ["obliques"] },
{ kw: ["russian twist","woodchop","oblique"], p: ["obliques","abs"], s: [] },
{ kw: ["tåhev","calf raise","calf"], p: ["calves","calves_back"], s: [] },
{ kw: ["hyperextension","back extension","ryggstrekning"], p: ["lower_back","glutes"], s: ["hamstrings"] },
{ kw: ["shrug","skuldertrekk","upright row"], p: ["traps"], s: ["shoulders_side"] },
];

export const MUSCLES = {
chest: { label: "Bryst", view: "front" },
shoulders_front: { label: "Fremre skuldre", view: "front" },
shoulders_side: { label: "Laterale skuldre", view: "front" },
biceps: { label: "Biceps", view: "front" },
forearms: { label: "Underarmer", view: "front" },
abs: { label: "Mage", view: "front" },
obliques: { label: "Oblique", view: "front" },
quads: { label: "Quadriceps", view: "front" },
calves: { label: "Legg", view: "front" },
traps: { label: "Trapezius", view: "back" },
rear_delts: { label: "Bakre skuldre", view: "back" },
lats: { label: "Latissimus", view: "back" },
triceps: { label: "Triceps", view: "back" },
lower_back: { label: "Korsrygg", view: "back" },
glutes: { label: "Sete", view: "back" },
hamstrings: { label: "Hamstrings", view: "back" },
calves_back: { label: "Legg (bak)", view: "back" },
};

// shape.d = SVG path string; otherwise ellipse (cx/cy/rx/ry)
export const SHAPES = {
chest: [{ cx:62, cy:80, rx:18, ry:13 }, { cx:98, cy:80, rx:18, ry:13 }],
shoulders_front: [{ cx:42, cy:60, rx:10, ry:8 }, { cx:118, cy:60, rx:10, ry:8 }],
shoulders_side: [{ cx:23, cy:68, rx:9, ry:8 }, { cx:137, cy:68, rx:9, ry:8 }],
biceps: [{ cx:21, cy:96, rx:9, ry:15 }, { cx:139, cy:96, rx:9, ry:15 }],
forearms: [{ cx:17, cy:128, rx:8, ry:14 }, { cx:143, cy:128, rx:8, ry:14 }],
abs: [{ cx:80, cy:108, rx:13, ry:26 }],
obliques: [{ cx:58, cy:110, rx:10, ry:21 }, { cx:102, cy:110, rx:10, ry:21 }],
quads: [{ cx:63, cy:212, rx:18, ry:37 }, { cx:97, cy:212, rx:18, ry:37 }],
calves: [{ cx:63, cy:292, rx:12, ry:24 }, { cx:97, cy:292, rx:12, ry:24 }],
// traps: trapezoid with neck notch — wider at shoulders, tapers to mid-back
traps: [{ d: "M 42,57 L 68,55 L 72,50 L 88,50 L 92,55 L 118,57 L 102,76 L 58,76 Z" }],
// rear_delts: outer shoulder position, distinct from traps
rear_delts: [{ cx:29, cy:68, rx:11, ry:9 }, { cx:131, cy:68, rx:11, ry:9 }],
// lats: wing-shaped paths from armpit down to lower back
lats: [
{ d: "M 33,72 Q 26,96 30,122 Q 36,132 54,130 Q 58,112 55,90 Q 48,74 33,72 Z" },
{ d: "M 127,72 Q 134,96 130,122 Q 124,132 106,130 Q 102,112 105,90 Q 112,74 127,72 Z" },
],
triceps: [{ cx:21, cy:96, rx:9, ry:15 }, { cx:139, cy:96, rx:9, ry:15 }],
lower_back: [{ cx:80, cy:124, rx:20, ry:13 }],
glutes: [{ cx:63, cy:168, rx:18, ry:19 }, { cx:97, cy:168, rx:18, ry:19 }],
hamstrings: [{ cx:63, cy:218, rx:17, ry:33 }, { cx:97, cy:218, rx:17, ry:33 }],
calves_back: [{ cx:63, cy:292, rx:13, ry:24 }, { cx:97, cy:292, rx:13, ry:24 }],
};

// Smooth body silhouette using bezier curves
export const BODY_PATH = "M 73,50 C 57,50 24,53 18,60 Q 10,82 11,110 Q 10,130 15,144 Q 18,152 24,155 Q 25,143 26,126 Q 28,80 33,64 Q 46,57 56,58 Q 55,80 53,115 Q 50,140 47,155 Q 47,162 48,167 L 48,355 L 76,355 L 76,167 Q 78,174 80,174 Q 82,174 84,167 L 84,355 L 112,355 L 112,167 Q 113,162 113,155 Q 110,140 107,115 Q 105,80 104,58 Q 114,57 127,64 Q 132,80 134,126 Q 135,143 136,155 Q 142,152 145,144 Q 150,130 149,110 Q 150,82 142,60 C 136,53 103,50 87,50 Z";
export const BODY_POLY = BODY_PATH; // backward compat alias

export const PRIMARY_FILL = "var(--heat-4, #d02670)";
export const PRIMARY_HOVER = "var(--heat-5, #ee2c80)";
export const PRIMARY_STROKE = "#ee2c80";
export const SEC_FILL = "none";
export const SEC_HOVER = "none";
export const SEC_STROKE = "none";

export function useIsMobile(breakpoint = 500) {
const [mobile, setMobile] = React.useState(() => window.innerWidth < breakpoint);
React.useEffect(() => {
const fn = () => setMobile(window.innerWidth < breakpoint);
window.addEventListener("resize", fn);
return () => window.removeEventListener("resize", fn);
}, [breakpoint]);
return mobile;
}

export function calcMuscles(exercises) {
const p = new Set(), s = new Set();
exercises.forEach(ex => {
if (ex.primary?.length || ex.secondary?.length) {
(ex.primary || []).forEach(m => p.add(m));
(ex.secondary || []).forEach(m => s.add(m));
} else {
const txt = (ex.name + " " + (ex.standardName || "")).toLowerCase();
for (const rule of EX_DB) {
if (rule.kw.some(k => txt.includes(k))) {
rule.p.forEach(m => p.add(m));
rule.s.forEach(m => s.add(m));
break;
}
}
}
});
p.forEach(m => s.delete(m));
return { primary: [...p], secondary: [...s] };
}
Loading