From a529416599d60f82fd4e2b257ac1145bbec62132 Mon Sep 17 00:00:00 2001 From: Danny Date: Mon, 13 Apr 2026 20:41:47 +0200 Subject: [PATCH] feat(tg-fitness-bot): structured mini-app, edit workouts, draft persistence Rebuilt Log view: exercise name input with autocomplete, per-set entry (reps x weight), exercise cards with edit/remove. Comma and dot both work as decimal separators. Notes field. Collapsible raw text input as fallback. Edit saved workouts from History (pencil icon). Loads exercises into editor, Save becomes Update, Cancel returns to History. localStorage draft persistence: auto-saves on every state change, restores on reopen (24h expiry), clears on save. Co-Authored-By: Claude Opus 4.6 (1M context) --- webapp/app.js | 613 ++++++++++++++++++++++++++++++++++++++++++---- webapp/index.html | 64 ++++- webapp/style.css | 251 +++++++++++++++++-- 3 files changed, 852 insertions(+), 76 deletions(-) diff --git a/webapp/app.js b/webapp/app.js index 44479ac..75df6d9 100644 --- a/webapp/app.js +++ b/webapp/app.js @@ -42,6 +42,110 @@ function showToast(msg) { setTimeout(() => toast.classList.remove("show"), 2000); } +// ── Draft persistence (localStorage) ──────────────────────────── +const DRAFT_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours + +function draftKey() { + return "draft_workout_" + userId; +} + +function saveDraft() { + if (!userId) return; + try { + const draft = { + workout, + currentExercise, + currentSets: getCurrentSets(), + note: noteInput?.value || "", + editingWorkoutId, + activeView: document.querySelector(".tab.active")?.dataset.view || "log", + rawDetailsOpen: document.getElementById("raw-details")?.open || false, + savedAt: Date.now(), + }; + localStorage.setItem(draftKey(), JSON.stringify(draft)); + } catch (e) { + console.warn("Failed to save draft", e); + } +} + +function clearDraft() { + if (!userId) return; + try { + localStorage.removeItem(draftKey()); + } catch (e) { + console.warn("Failed to clear draft", e); + } +} + +function restoreDraft() { + if (!userId) return false; + try { + const raw = localStorage.getItem(draftKey()); + if (!raw) return false; + + const draft = JSON.parse(raw); + + // Expire old drafts + if (draft.savedAt && Date.now() - draft.savedAt > DRAFT_EXPIRY_MS) { + clearDraft(); + return false; + } + + // Restore completed exercises + if (Array.isArray(draft.workout) && draft.workout.length > 0) { + workout = draft.workout; + renderWorkout(); + } + + // Restore in-progress exercise + if (draft.currentExercise) { + currentExercise = draft.currentExercise; + setsSection.classList.remove("hidden"); + setsLabel.textContent = currentExercise.name; + setsList.innerHTML = ""; + if (Array.isArray(draft.currentSets)) { + draft.currentSets.forEach((s) => addSetToDOM(s.reps, s.weight_kg)); + } + } + + // Restore active tab + if (draft.activeView && draft.activeView !== "log") { + document.querySelectorAll(".tab").forEach((t) => t.classList.remove("active")); + document.querySelectorAll(".view").forEach((v) => v.classList.remove("active")); + const tab = document.querySelector(`.tab[data-view="${draft.activeView}"]`); + if (tab) { + tab.classList.add("active"); + document.getElementById("view-" + draft.activeView)?.classList.add("active"); + if (draft.activeView === "history") loadHistory(); + if (draft.activeView === "stats") loadStats(); + } + } + + // Restore note + if (draft.note && noteInput) { + noteInput.value = draft.note; + } + + // Restore editing state + if (draft.editingWorkoutId) { + editingWorkoutId = draft.editingWorkoutId; + updateEditingUI(); + } + + // Restore raw details open state + if (draft.rawDetailsOpen) { + const details = document.getElementById("raw-details"); + if (details) details.open = true; + } + + return true; + } catch (e) { + console.warn("Failed to restore draft", e); + clearDraft(); + return false; + } +} + // ── Tab navigation ────────────────────────────────────────────── document.querySelectorAll(".tab").forEach((tab) => { tab.addEventListener("click", () => { @@ -50,17 +154,433 @@ document.querySelectorAll(".tab").forEach((tab) => { tab.classList.add("active"); document.getElementById("view-" + tab.dataset.view).classList.add("active"); tg.HapticFeedback.selectionChanged(); + saveDraft(); - // Lazy-load data when switching tabs if (tab.dataset.view === "history") loadHistory(); if (tab.dataset.view === "stats") loadStats(); }); }); -// ── Log View ──────────────────────────────────────────────────── -let historyOffset = 0; +// Persist raw details toggle +document.getElementById("raw-details")?.addEventListener("toggle", saveDraft); -document.getElementById("btn-save").addEventListener("click", async () => { +// Persist note changes (debounced) +let noteSaveTimer; +document.getElementById("inp-note")?.addEventListener("input", () => { + clearTimeout(noteSaveTimer); + noteSaveTimer = setTimeout(saveDraft, 400); +}); + +// ── State ─────────────────────────────────────────────────────── +let workout = []; +let knownExercises = []; +let currentExercise = null; +let editingWorkoutId = null; // non-null when editing a saved workout + +// ── Structured Log View ───────────────────────────────────────── + +const nameInput = document.getElementById("inp-exercise-name"); +const btnAddExercise = document.getElementById("btn-add-exercise"); +const autocompleteList = document.getElementById("autocomplete-list"); +const setsSection = document.getElementById("sets-section"); +const setsLabel = document.getElementById("sets-label"); +const setsList = document.getElementById("sets-list"); +const repsInput = document.getElementById("inp-reps"); +const weightInput = document.getElementById("inp-weight"); +const btnAddSet = document.getElementById("btn-add-set"); +const btnSaveWorkout = document.getElementById("btn-save-workout"); +const workoutExercises = document.getElementById("workout-exercises"); +const notesSection = document.getElementById("notes-section"); +const noteInput = document.getElementById("inp-note"); + +// Exercise name input — autocomplete +nameInput.addEventListener("input", () => { + const val = nameInput.value.trim().toLowerCase(); + autocompleteList.innerHTML = ""; + if (!val) { + autocompleteList.classList.remove("visible"); + return; + } + + const matches = knownExercises.filter((n) => + n.toLowerCase().includes(val) + ).slice(0, 8); + + if (matches.length === 0) { + autocompleteList.classList.remove("visible"); + return; + } + + matches.forEach((name) => { + const item = document.createElement("div"); + item.className = "autocomplete-item"; + item.textContent = name; + item.addEventListener("click", () => { + nameInput.value = name; + autocompleteList.innerHTML = ""; + autocompleteList.classList.remove("visible"); + startExercise(name); + }); + autocompleteList.appendChild(item); + }); + autocompleteList.classList.add("visible"); +}); + +// Close autocomplete on outside click +document.addEventListener("click", (e) => { + if (!e.target.closest("#add-exercise-card")) { + autocompleteList.innerHTML = ""; + autocompleteList.classList.remove("visible"); + } +}); + +// Enter on name input → start exercise +nameInput.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + e.preventDefault(); + const name = nameInput.value.trim(); + if (name) { + autocompleteList.innerHTML = ""; + autocompleteList.classList.remove("visible"); + startExercise(name); + } + } +}); + +btnAddExercise.addEventListener("click", () => { + const name = nameInput.value.trim(); + if (name) { + autocompleteList.innerHTML = ""; + autocompleteList.classList.remove("visible"); + startExercise(name); + } +}); + +const btnDeleteExercise = document.getElementById("btn-delete-exercise"); + +function startExercise(name) { + if (currentExercise && getCurrentSets().length > 0) { + finishCurrentExercise(); + } + + currentExercise = { name, machine_id: null }; + setsSection.classList.remove("hidden"); + setsLabel.textContent = name; + setsList.innerHTML = ""; + nameInput.value = ""; + repsInput.value = ""; + weightInput.value = ""; + repsInput.focus(); + notesSection.classList.remove("hidden"); + btnDeleteExercise.classList.add("hidden"); + tg.HapticFeedback.selectionChanged(); + saveDraft(); +} + +function getCurrentSets() { + return Array.from(setsList.querySelectorAll(".set-entry")).map((el) => ({ + reps: parseInt(el.dataset.reps), + weight_kg: parseFloat(el.dataset.weight), + })); +} + +// Add a set entry to the DOM (used by both addSet and restoreDraft) +function addSetToDOM(reps, weight) { + const entry = document.createElement("div"); + entry.className = "set-entry"; + entry.dataset.reps = reps; + entry.dataset.weight = weight; + + const label = weight + ? `${reps} x ${fmtWeight(weight)}kg` + : `${reps} reps`; + + entry.innerHTML = ` + ${label} + + `; + entry.querySelector(".btn-remove").addEventListener("click", () => { + entry.remove(); + tg.HapticFeedback.selectionChanged(); + saveDraft(); + }); + + setsList.appendChild(entry); +} + +// Parse a weight string, accepting both comma and dot as decimal separators +function parseWeight(s) { + if (!s) return 0; + return parseFloat(String(s).replace(",", ".")) || 0; +} + +// Add set from input fields +function addSet() { + const reps = parseInt(repsInput.value); + if (!reps || reps <= 0) { + showToast("Enter reps"); + tg.HapticFeedback.notificationOccurred("error"); + return; + } + const weight = parseWeight(weightInput.value); + + addSetToDOM(reps, weight); + + repsInput.value = ""; + weightInput.value = weight ? String(weight) : ""; + repsInput.focus(); + tg.HapticFeedback.impactOccurred("light"); + saveDraft(); +} + +btnAddSet.addEventListener("click", addSet); + +repsInput.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + e.preventDefault(); + if (repsInput.value.trim()) { + weightInput.focus(); + } + // Empty reps → stay put, keyboard stays open + } +}); +weightInput.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + e.preventDefault(); + if (!repsInput.value.trim()) { + // No reps yet — jump back to reps + repsInput.focus(); + return; + } + addSet(); + // After addSet, focus is already on repsInput (set inside addSet) + } +}); + +// Delete the current exercise being edited (discard without saving back) +btnDeleteExercise.addEventListener("click", () => { + currentExercise = null; + setsSection.classList.add("hidden"); + setsList.innerHTML = ""; + btnDeleteExercise.classList.add("hidden"); + renderWorkout(); + tg.HapticFeedback.notificationOccurred("warning"); + saveDraft(); +}); + +// Finish current exercise → add to workout +function finishCurrentExercise() { + if (!currentExercise) return; + const sets = getCurrentSets(); + if (sets.length === 0) return; + + workout.push({ + name: currentExercise.name, + machine_id: null, + sets_detail: sets, + sets: sets.length, + reps: sets[0].reps, + weight_kg: sets[0].weight_kg, + }); + + currentExercise = null; + setsSection.classList.add("hidden"); + setsList.innerHTML = ""; + renderWorkout(); + saveDraft(); +} + +function renderWorkout() { + workoutExercises.innerHTML = ""; + const hasAny = workout.length > 0 || currentExercise !== null; + + if (workout.length === 0) { + btnSaveWorkout.classList.add("hidden"); + } else { + btnSaveWorkout.classList.remove("hidden"); + } + + // Show notes section when there's any workout activity + if (hasAny) { + notesSection.classList.remove("hidden"); + } else { + notesSection.classList.add("hidden"); + } + + workout.forEach((ex, idx) => { + const card = document.createElement("div"); + card.className = "exercise-card"; + + const machine = ex.machine_id ? ` (${ex.machine_id})` : ""; + const setsHtml = ex.sets_detail + .map((s) => (s.weight_kg ? `${s.reps}x${fmtWeight(s.weight_kg)}kg` : `${s.reps}`)) + .join(", "); + + card.innerHTML = ` +
+ ${ex.name}${machine} + +
+
${setsHtml}
+ `; + + card.querySelector(".btn-edit").addEventListener("click", (e) => { + e.stopPropagation(); + editExercise(idx); + }); + + workoutExercises.appendChild(card); + }); +} + +// Reopen a saved exercise to add/remove sets +function editExercise(idx) { + // Finish any in-progress exercise first + if (currentExercise && getCurrentSets().length > 0) { + finishCurrentExercise(); + } + + const ex = workout[idx]; + if (!ex) return; + + // Pop it back into the current-exercise slot + workout.splice(idx, 1); + currentExercise = { name: ex.name, machine_id: ex.machine_id }; + setsSection.classList.remove("hidden"); + setsLabel.textContent = ex.name; + setsList.innerHTML = ""; + (ex.sets_detail || []).forEach((s) => addSetToDOM(s.reps, s.weight_kg)); + // Pre-fill weight input with last set's weight for convenience + const lastWeight = ex.sets_detail?.length ? ex.sets_detail[ex.sets_detail.length - 1].weight_kg : 0; + weightInput.value = lastWeight ? String(lastWeight) : ""; + repsInput.value = ""; + repsInput.focus(); + btnDeleteExercise.classList.remove("hidden"); + + renderWorkout(); + tg.HapticFeedback.selectionChanged(); + saveDraft(); +} + +// ── Edit saved workout ────────────────────────────────────────── + +function editSavedWorkout(workoutData) { + // Clear any in-progress work + if (currentExercise && getCurrentSets().length > 0) { + finishCurrentExercise(); + } + workout = []; + currentExercise = null; + setsList.innerHTML = ""; + setsSection.classList.add("hidden"); + + // Load all exercises from the saved workout + (workoutData.superset_groups || []).forEach((group) => { + group.forEach((ex) => { + workout.push({ + name: ex.name, + machine_id: ex.machine_id || null, + sets_detail: ex.sets_detail || [], + sets: ex.sets || 0, + reps: ex.reps || 0, + weight_kg: ex.weight_kg || 0, + }); + }); + }); + + // Set note + noteInput.value = workoutData.note || ""; + + // Mark as editing + editingWorkoutId = workoutData.id; + + // Switch to Log tab + document.querySelectorAll(".tab").forEach((t) => t.classList.remove("active")); + document.querySelectorAll(".view").forEach((v) => v.classList.remove("active")); + document.querySelector('.tab[data-view="log"]').classList.add("active"); + document.getElementById("view-log").classList.add("active"); + + renderWorkout(); + updateEditingUI(); + saveDraft(); + tg.HapticFeedback.impactOccurred("medium"); +} + +function updateEditingUI() { + const banner = document.getElementById("editing-banner"); + if (editingWorkoutId) { + if (banner) banner.classList.remove("hidden"); + btnSaveWorkout.textContent = "Update Workout"; + } else { + if (banner) banner.classList.add("hidden"); + btnSaveWorkout.textContent = "Save Workout"; + } +} + +function cancelEdit() { + editingWorkoutId = null; + workout = []; + currentExercise = null; + noteInput.value = ""; + setsList.innerHTML = ""; + setsSection.classList.add("hidden"); + renderWorkout(); + updateEditingUI(); + clearDraft(); + + // Switch to history tab + document.querySelectorAll(".tab").forEach((t) => t.classList.remove("active")); + document.querySelectorAll(".view").forEach((v) => v.classList.remove("active")); + document.querySelector('.tab[data-view="history"]').classList.add("active"); + document.getElementById("view-history").classList.add("active"); + loadHistory(); + tg.HapticFeedback.selectionChanged(); +} + +document.getElementById("btn-cancel-edit")?.addEventListener("click", cancelEdit); + +// Save workout +btnSaveWorkout.addEventListener("click", async () => { + if (currentExercise && getCurrentSets().length > 0) { + finishCurrentExercise(); + } + + if (workout.length === 0) { + showToast("Add at least one exercise"); + tg.HapticFeedback.notificationOccurred("error"); + return; + } + + tg.HapticFeedback.impactOccurred("medium"); + + const superset_groups = workout.map((ex) => [ex]); + const note = noteInput.value.trim() || null; + + try { + let data; + if (editingWorkoutId) { + data = await api("PUT", `/workouts/${editingWorkoutId}`, { superset_groups, note }); + showToast("Workout updated!"); + } else { + data = await api("POST", "/workouts", { superset_groups, note }); + showToast("Workout #" + data.workout_id + " saved!"); + } + workout = []; + currentExercise = null; + editingWorkoutId = null; + noteInput.value = ""; + renderWorkout(); + updateEditingUI(); + clearDraft(); + tg.HapticFeedback.notificationOccurred("success"); + } catch (e) { + showToast(e.message); + tg.HapticFeedback.notificationOccurred("error"); + } +}); + +// ── Raw text fallback ─────────────────────────────────────────── +document.getElementById("btn-save-raw").addEventListener("click", async () => { const raw = document.getElementById("inp-raw").value.trim(); if (!raw) { showToast("Enter your workout first"); @@ -72,6 +592,7 @@ document.getElementById("btn-save").addEventListener("click", async () => { try { const data = await api("POST", "/workouts", { raw_text: raw }); document.getElementById("inp-raw").value = ""; + clearDraft(); showToast("Workout #" + data.workout_id + " saved!"); tg.HapticFeedback.notificationOccurred("success"); } catch (e) { @@ -80,40 +601,8 @@ document.getElementById("btn-save").addEventListener("click", async () => { } }); -// Load exercise name suggestions -async function loadSuggestions() { - try { - const data = await api("GET", "/exercises"); - const container = document.getElementById("suggestion-chips"); - const wrapper = document.getElementById("suggestions"); - - if (!data.exercises || data.exercises.length === 0) { - wrapper.style.display = "none"; - return; - } - wrapper.style.display = "block"; - container.innerHTML = ""; - - data.exercises.slice(0, 20).forEach((name) => { - const chip = document.createElement("button"); - chip.className = "chip"; - chip.textContent = name; - chip.addEventListener("click", () => { - const textarea = document.getElementById("inp-raw"); - const val = textarea.value; - const suffix = name + ": "; - textarea.value = val ? val + "\n" + suffix : suffix; - textarea.focus(); - tg.HapticFeedback.selectionChanged(); - }); - container.appendChild(chip); - }); - } catch (e) { - console.error("Failed to load suggestions", e); - } -} - // ── History View ──────────────────────────────────────────────── +let historyOffset = 0; async function loadHistory(append = false) { try { @@ -146,13 +635,15 @@ async function loadHistory(append = false) { minute: "2-digit", }); - // Calculate volume let volume = 0; - let totalSets = 0; (w.superset_groups || []).forEach((group) => { group.forEach((ex) => { - volume += ex.sets * ex.reps * ex.weight_kg; - totalSets += ex.sets; + const details = ex.sets_detail || []; + if (details.length > 0) { + details.forEach((s) => { volume += s.reps * s.weight_kg; }); + } else { + volume += ex.sets * ex.reps * ex.weight_kg; + } }); }); @@ -166,10 +657,22 @@ async function loadHistory(append = false) { } group.forEach((ex) => { const machine = ex.machine_id ? ` (${ex.machine_id})` : ""; + const details = ex.sets_detail || []; + let detailStr; + if (details.length > 0 && !details.every( + (d) => d.reps === details[0].reps && d.weight_kg === details[0].weight_kg + )) { + detailStr = details.map((d) => + d.weight_kg ? `${d.reps}x${fmtWeight(d.weight_kg)}kg` : `${d.reps}` + ).join(", "); + } else { + const w = ex.weight_kg; + detailStr = w ? `${ex.sets}x${ex.reps}x${fmtWeight(w)}kg` : `${ex.sets}x${ex.reps}`; + } groupsHtml += `
${ex.name}${machine} - — ${ex.sets}x${ex.reps}x${ex.weight_kg}kg + — ${detailStr}
`; }); groupsHtml += ""; @@ -178,10 +681,19 @@ async function loadHistory(append = false) { card.innerHTML = `
${dateStr} - ${Math.round(volume)} kg vol +
+ ${Math.round(volume)} kg vol + +
${groupsHtml} `; + + card.querySelector(".btn-history-edit").addEventListener("click", (e) => { + e.stopPropagation(); + editSavedWorkout(w); + }); + container.appendChild(card); }); @@ -204,7 +716,7 @@ async function loadStats() { const container = document.getElementById("stats-content"); if (data.total_workouts === 0) { - container.innerHTML = '
📊

No workouts yet

'; + container.innerHTML = '

No workouts yet

'; return; } @@ -233,10 +745,21 @@ async function loadStats() { } } +// ── Helpers ───────────────────────────────────────────────────── +function fmtWeight(w) { + return w === Math.floor(w) ? Math.floor(w).toString() : w.toString(); +} + // ── Init ──────────────────────────────────────────────────────── async function init() { if (!userId) return; - await loadSuggestions(); + try { + const data = await api("GET", "/exercises"); + knownExercises = data.exercises || []; + } catch (e) { + console.error("Failed to load exercises", e); + } + restoreDraft(); } init(); diff --git a/webapp/index.html b/webapp/index.html index 035399e..8ed08e6 100644 --- a/webapp/index.html +++ b/webapp/index.html @@ -17,25 +17,65 @@
-
- -
Same format as the bot. Blank line = new group. Consecutive lines = superset.
- + + + - -
-
📋
+
📋

No workouts yet

@@ -44,8 +84,8 @@
-
📊
-

Loading…

+
📊
+

Loading...

diff --git a/webapp/style.css b/webapp/style.css index dd2fdd8..368fceb 100644 --- a/webapp/style.css +++ b/webapp/style.css @@ -14,6 +14,8 @@ body { -webkit-font-smoothing: antialiased; } +.hidden { display: none !important; } + /* ── Tabs ────────────────────────────────────────────────────── */ #tabs { @@ -86,6 +88,46 @@ body { border: 1.5px solid var(--tg-theme-button-color, #3390ec); } +.btn-icon { + width: 44px; + height: 44px; + border-radius: 10px; + border: none; + background: var(--tg-theme-button-color, #3390ec); + color: var(--tg-theme-button-text-color, #fff); + font-size: 22px; + font-weight: 600; + cursor: pointer; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + transition: opacity 0.15s; +} + +.btn-icon:active { opacity: 0.7; } + +.btn-link { + background: none; + border: none; + color: var(--tg-theme-link-color, #3390ec); + font-size: 13px; + cursor: pointer; + padding: 8px 0; +} + +.btn-remove { + background: none; + border: none; + color: var(--tg-theme-hint-color, #999); + font-size: 20px; + cursor: pointer; + padding: 0 4px; + line-height: 1; +} + +.btn-remove:active { opacity: 0.5; } + /* ── Inputs ──────────────────────────────────────────────────── */ .input { @@ -106,6 +148,12 @@ body { border-color: var(--tg-theme-button-color, #3390ec); } +.input-small { + width: auto; + flex: 1; + min-width: 0; +} + .hint { font-size: 12px; color: var(--tg-theme-hint-color, #999); @@ -113,35 +161,191 @@ body { line-height: 1.4; } -/* ── Suggestion chips ────────────────────────────────────────── */ +/* ── Structured log: exercise name row ──────────────────────── */ -.section-label { - font-size: 12px; - font-weight: 600; +.exercise-name-row { + display: flex; + gap: 8px; + margin-bottom: 4px; +} + +.exercise-name-row .input { flex: 1; } + +/* ── Autocomplete ────────────────────────────────────────────── */ + +.autocomplete-list { + border-radius: 10px; + overflow: hidden; + max-height: 0; + opacity: 0; + transition: max-height 0.2s, opacity 0.2s; +} + +.autocomplete-list.visible { + max-height: 280px; + opacity: 1; + margin-top: 4px; + border: 1px solid var(--tg-theme-hint-color, #999)33; +} + +.autocomplete-item { + padding: 10px 12px; + font-size: 14px; + cursor: pointer; + background: var(--tg-theme-bg-color, #fff); + border-bottom: 1px solid var(--tg-theme-hint-color, #999)22; +} + +.autocomplete-item:last-child { border-bottom: none; } +.autocomplete-item:active { background: var(--tg-theme-secondary-bg-color, #f0f0f0); } + +/* ── Sets section ────────────────────────────────────────────── */ + +.sets-section { + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid var(--tg-theme-hint-color, #999)22; +} + +.sets-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.btn-danger { + color: #e53935 !important; + font-size: 12px !important; +} + +.set-input-row { + display: flex; + gap: 8px; + align-items: center; + margin-top: 8px; +} + +.set-separator { color: var(--tg-theme-hint-color, #999); - text-transform: uppercase; - letter-spacing: 0.5px; + font-weight: 600; + font-size: 16px; +} + +.set-entry { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 0; + border-bottom: 1px solid var(--tg-theme-hint-color, #999)15; +} + +.set-text { + font-size: 14px; + font-weight: 500; +} + +/* ── Exercise cards (workout preview) ────────────────────────── */ + +.exercise-card { + background: var(--tg-theme-secondary-bg-color, #f0f0f0); + border-radius: 12px; + padding: 12px 16px; margin-bottom: 8px; } -#suggestion-chips { +.exercise-card-header { display: flex; - flex-wrap: wrap; - gap: 6px; - margin-bottom: 16px; + justify-content: space-between; + align-items: center; } -.chip { - padding: 6px 12px; - border-radius: 16px; +.exercise-card-actions { + display: flex; + gap: 4px; +} + +.btn-edit { + font-size: 14px !important; +} + +.exercise-card-name { + font-weight: 600; + font-size: 15px; +} + +.exercise-card-sets { font-size: 13px; - background: var(--tg-theme-secondary-bg-color, #f0f0f0); - color: var(--tg-theme-text-color, #000); - border: 1px solid var(--tg-theme-hint-color, #999)33; - cursor: pointer; + color: var(--tg-theme-hint-color, #999); + margin-top: 4px; } -.chip:active { opacity: 0.7; } +/* ── Editing banner ──────────────────────────────────────────── */ + +.editing-banner { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 16px; + margin-bottom: 12px; + border-radius: 10px; + background: var(--tg-theme-button-color, #3390ec)15; + border: 1px solid var(--tg-theme-button-color, #3390ec)40; + font-size: 14px; + font-weight: 600; + color: var(--tg-theme-button-color, #3390ec); +} + +/* ── Notes ───────────────────────────────────────────────────── */ + +#notes-section { + margin-bottom: 12px; +} + +#notes-section .input { + font-size: 14px; + resize: none; +} + +/* ── Raw text collapsible ─────────────────────────────────────── */ + +.raw-section { + margin-top: 8px; +} + +.raw-toggle { + font-size: 13px; + color: var(--tg-theme-link-color, #3390ec); + cursor: pointer; + padding: 8px 0; + list-style: none; + text-align: center; +} + +.raw-toggle::-webkit-details-marker { display: none; } + +.raw-toggle::before { + content: "\25B6 "; + font-size: 10px; + transition: transform 0.2s; + display: inline-block; +} + +details[open] .raw-toggle::before { + transform: rotate(90deg); +} + +.raw-card { + margin-top: 8px; +} + +/* ── Section labels ──────────────────────────────────────────── */ + +.section-label { + font-size: 13px; + font-weight: 600; + color: var(--tg-theme-button-color, #3390ec); + margin-bottom: 8px; +} /* ── History cards ───────────────────────────────────────────── */ @@ -164,12 +368,22 @@ body { font-weight: 600; } +.history-header-right { + display: flex; + align-items: center; + gap: 8px; +} + .history-volume { font-size: 13px; font-weight: 600; color: var(--tg-theme-button-color, #3390ec); } +.btn-history-edit { + font-size: 14px !important; +} + .history-group { margin-bottom: 8px; } @@ -233,7 +447,6 @@ body { /* ── Empty state ─────────────────────────────────────────────── */ .empty-state { text-align: center; padding: 48px 16px; } -.empty-icon { font-size: 48px; margin-bottom: 12px; } .empty-state p { color: var(--tg-theme-hint-color, #999); font-size: 16px; } /* ── Toast ───────────────────────────────────────────────────── */