From f13310da7850afad11df68652a712ca5707c9b59 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Apr 2026 16:02:57 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20add=20TiltFix=20=E2=80=94=20anterio?= =?UTF-8?q?r=20pelvic=20tilt=20PWA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Full progressive web app with: - 11-exercise daily routine (5 stretches + 6 strengthening) - Interactive timer with play/pause/reset for timed exercises - Set tracking with visual dot indicators - Day streak + weekly calendar on Progress tab - Exercise library with expandable cards and step-by-step instructions - Offline support via service worker (cache-first) - Installable PWA (manifest + beforeinstallprompt banner) - Minimalist sage-green design system with safe-area support Agent-Logs-Url: https://github.com/RehmTheGreat/RehmTheGreat.github.io/sessions/5c7f8e4d-7312-4561-b38c-593ef005a978 Co-authored-by: RehmTheGreat <191198362+RehmTheGreat@users.noreply.github.com> --- app.js | 929 ++++++++++++++++++++++++++++++++++++++++ icons/icon-maskable.svg | 17 + icons/icon.svg | 18 + index.html | 116 +++++ manifest.json | 24 ++ style.css | 875 +++++++++++++++++++++++++++++++++++++ sw.js | 45 ++ 7 files changed, 2024 insertions(+) create mode 100644 app.js create mode 100644 icons/icon-maskable.svg create mode 100644 icons/icon.svg create mode 100644 index.html create mode 100644 manifest.json create mode 100644 style.css create mode 100644 sw.js diff --git a/app.js b/app.js new file mode 100644 index 0000000..fec56f6 --- /dev/null +++ b/app.js @@ -0,0 +1,929 @@ +/* ── Data ────────────────────────────────────────────────── */ +const EXERCISES = [ + // STRETCHES — release tight muscles + { + id: 'cat-cow', + name: 'Cat-Cow Stretch', + category: 'stretch', + emoji: '🐱', + muscle: 'Spine & Lower Back', + sets: 3, + reps: 10, + duration: null, + sides: false, + steps: [ + 'Start on all fours with wrists under shoulders and knees under hips.', + 'Inhale — drop your belly toward the floor, lift your head and tailbone (cow).', + 'Exhale — round your spine toward the ceiling, tuck chin and pelvis (cat).', + 'Flow slowly and deliberately, matching breath to movement.' + ], + tip: 'Focus on tilting the pelvis posteriorly in the cat phase — that is exactly the movement that corrects APT.' + }, + { + id: 'childs-pose', + name: "Child's Pose", + category: 'stretch', + emoji: '🧘', + muscle: 'Lower Back & Glutes', + sets: 3, + reps: null, + duration: 60, + sides: false, + steps: [ + 'Kneel and sit back onto your heels, then fold forward and extend your arms overhead.', + 'Let your forehead rest on the floor and relax your entire back.', + 'Breathe deeply, allowing your lower back to lengthen on each exhale.', + 'Hold for the full duration without moving.' + ], + tip: 'Walk your hands further forward to feel a deeper stretch in the lumbar spine.' + }, + { + id: 'hip-flexor-stretch', + name: 'Kneeling Hip Flexor Stretch', + category: 'stretch', + emoji: '🦵', + muscle: 'Hip Flexors (Psoas)', + sets: 3, + reps: null, + duration: 45, + sides: true, + steps: [ + 'Kneel on one knee with the other foot forward at 90°.', + 'Tuck your pelvis slightly (posterior tilt) — this intensifies the stretch.', + 'Shift your hips gently forward until you feel a pull in the front of the kneeling hip.', + 'Keep your torso upright and core gently braced. Hold, then switch sides.' + ], + tip: 'The posterior pelvic tilt (PPT) during this stretch is critical. Without it, you compensate with the lower back and miss the psoas entirely.' + }, + { + id: 'quad-stretch', + name: 'Standing Quad Stretch', + category: 'stretch', + emoji: '🏃', + muscle: 'Quadriceps & Rectus Femoris', + sets: 3, + reps: null, + duration: 30, + sides: true, + steps: [ + 'Stand on one leg and bring the opposite heel toward your glute.', + 'Hold your ankle (not your toes) and keep knees together.', + 'Tuck your pelvis (posterior tilt) to deepen the rectus femoris stretch.', + 'Hold steady and switch sides.' + ], + tip: 'The rectus femoris crosses both the hip and knee. The PPT cue unlocks its upper portion, which standard quad stretches miss.' + }, + { + id: 'pigeon-pose', + name: 'Pigeon Pose', + category: 'stretch', + emoji: '🕊️', + muscle: 'Hip Flexors & Piriformis', + sets: 2, + reps: null, + duration: 60, + sides: true, + steps: [ + 'From all fours, bring one knee forward and place it behind the same-side wrist.', + 'Extend the opposite leg straight behind you with the top of the foot on the floor.', + 'Square your hips toward the floor as much as possible.', + 'Fold forward over the front leg, resting on forearms or a block. Hold, then switch.' + ], + tip: 'If your hip does not reach the floor, place a folded blanket under it to keep the pelvis level.' + }, + + // STRENGTHENING — activate weak muscles + { + id: 'ppt', + name: 'Posterior Pelvic Tilt', + category: 'strengthen', + emoji: '🎯', + muscle: 'Lower Abs & Glutes', + sets: 3, + reps: 15, + duration: null, + sides: false, + steps: [ + 'Lie on your back with knees bent and feet flat on the floor.', + 'Flatten your lower back against the floor by contracting your lower abs and glutes simultaneously.', + 'Hold the tilt for 3 seconds, feeling your spine press into the ground.', + 'Release slowly and repeat — this is the exact correction movement for APT.' + ], + tip: 'Think of your pelvis as a bowl of water: APT spills it forward. PPT levels the bowl. Practice this movement awareness in sitting and standing throughout your day.' + }, + { + id: 'glute-bridge', + name: 'Glute Bridge', + category: 'strengthen', + emoji: '🌉', + muscle: 'Glutes & Hamstrings', + sets: 3, + reps: 15, + duration: null, + sides: false, + steps: [ + 'Lie on your back with knees bent, feet hip-width apart.', + 'Drive through your heels and squeeze your glutes to lift your hips.', + 'At the top, tuck your pelvis slightly to fully activate the glutes — do NOT arch your lower back.', + 'Lower slowly over 3 seconds and repeat.' + ], + tip: 'Squeeze the glutes hard at the top of each rep. Weak glutes are one of the primary drivers of APT.' + }, + { + id: 'dead-bug', + name: 'Dead Bug', + category: 'strengthen', + emoji: '🐛', + muscle: 'Deep Core (TVA)', + sets: 3, + reps: 10, + duration: null, + sides: true, + steps: [ + 'Lie on your back with arms pointing toward the ceiling and knees at 90° above hips.', + 'Press your lower back firmly into the floor (PPT) and hold it there throughout.', + 'Slowly lower your right arm and left leg toward the floor, breathing out.', + 'Return and alternate sides — if your back lifts off the floor, reduce range.' + ], + tip: 'Speed is the enemy here. Slow, controlled reps with the lower back glued to the floor are the goal.' + }, + { + id: 'plank', + name: 'Forearm Plank', + category: 'strengthen', + emoji: '⚡', + muscle: 'Core & Glutes', + sets: 3, + reps: null, + duration: 30, + sides: false, + steps: [ + 'Place forearms on the floor with elbows under shoulders, hands forward.', + 'Rise onto your toes, keeping the body in a straight line from head to heels.', + 'Actively tuck your pelvis (posterior tilt) and squeeze your glutes — do not let your hips sag or pike.', + 'Breathe steadily and hold for the full duration.' + ], + tip: 'Imagine pulling your elbows toward your feet without them moving — this activates your deep core far more than just holding position.' + }, + { + id: 'reverse-crunch', + name: 'Reverse Crunch', + category: 'strengthen', + emoji: '🔄', + muscle: 'Lower Abs', + sets: 3, + reps: 15, + duration: null, + sides: false, + steps: [ + 'Lie on your back with legs raised and knees at 90°, hands flat beside you.', + 'Use your lower abs to curl your hips off the floor, bringing knees toward your chest.', + 'At the top your hips and lower back should briefly peel off the floor.', + 'Lower slowly and controlled — do not let momentum take over.' + ], + tip: 'The lower abs are the key antagonist to the hip flexors in APT. Strengthening them directly counteracts the anterior pull.' + }, + { + id: 'bird-dog', + name: 'Bird Dog', + category: 'strengthen', + emoji: '🐦', + muscle: 'Glutes & Core Stability', + sets: 3, + reps: 10, + duration: null, + sides: true, + steps: [ + 'Start on all fours with a neutral spine (not arched or rounded).', + 'Extend your right arm and left leg simultaneously until they are parallel to the floor.', + 'Hold for 2 seconds, return, then switch to left arm and right leg.', + 'Keep your hips level — do not rotate or shift weight.' + ], + tip: 'If your lower back arches when you extend the leg, reduce range until strength improves.' + } +]; + +// Ordered daily routine (exercise IDs) +const ROUTINE_ORDER = [ + 'cat-cow', + 'childs-pose', + 'hip-flexor-stretch', + 'quad-stretch', + 'pigeon-pose', + 'ppt', + 'glute-bridge', + 'dead-bug', + 'plank', + 'reverse-crunch', + 'bird-dog' +]; + +const TIPS = [ + 'When sitting, place a small lumbar pillow behind your lower back and consciously level your pelvis.', + 'Set an hourly reminder to check your posture — APT is mostly a habit of inattention.', + 'Avoid extended sitting. Stand and walk for at least 5 minutes every hour.', + 'When standing in line, gently activate your glutes. This alone resets pelvic alignment.', + 'Sleeping on your back with a pillow under your knees removes strain from your hip flexors overnight.', + 'Avoid excessive lower-back arching during gym exercises like squats and deadlifts.', + 'Building the habit of a daily routine is more valuable than any single perfect session.', + 'Consistency over 8–12 weeks produces lasting structural change. Do not rush the process.' +]; + +/* ── State & Storage ─────────────────────────────────────── */ +function loadState() { + try { + return JSON.parse(localStorage.getItem('tiltfix-state') || '{}'); + } catch (_) { + return {}; + } +} + +function saveState(state) { + localStorage.setItem('tiltfix-state', JSON.stringify(state)); +} + +function todayKey() { + return new Date().toISOString().slice(0, 10); +} + +/* ── Timer ───────────────────────────────────────────────── */ +let timerInterval = null; +let timerSeconds = 0; +let timerRunning = false; +let currentExerciseId = null; +let currentSet = 0; +let completedSets = 0; + +function clearTimer() { + if (timerInterval !== null) { + clearInterval(timerInterval); + timerInterval = null; + } + timerRunning = false; +} + +/* ── Navigation ──────────────────────────────────────────── */ +let currentView = 'home'; + +function navigate(view) { + if (view === currentView) return; + clearTimer(); + currentView = view; + document.querySelectorAll('.view').forEach((el) => el.classList.remove('active')); + document.querySelectorAll('.nav-item').forEach((el) => el.classList.remove('active')); + + const viewEl = document.getElementById('view-' + view); + if (viewEl) viewEl.classList.add('active'); + const navEl = document.querySelector('[data-view="' + view + '"]'); + if (navEl) navEl.classList.add('active'); + + document.getElementById('app').scrollTop = 0; + + renderView(view); +} + +function renderView(view) { + if (view === 'home') renderHome(); + if (view === 'exercises') renderExercises(); + if (view === 'routine') renderRoutine(); + if (view === 'progress') renderProgress(); +} + +/* ── HOME ─────────────────────────────────────────────────── */ +function renderHome() { + const state = loadState(); + const today = todayKey(); + const completedToday = state.completedToday || {}; + const doneCount = Object.keys(completedToday[today] || {}).length; + const total = ROUTINE_ORDER.length; + const pct = Math.round((doneCount / total) * 100); + + // Streak + const streak = calcStreak(state); + + // Tip of the day (cycle by day-of-year) + const dayOfYear = Math.floor((Date.now() - new Date(new Date().getFullYear(), 0, 0)) / 86400000); + const tip = TIPS[dayOfYear % TIPS.length]; + + // Greeting + const hour = new Date().getHours(); + const greeting = hour < 12 ? 'Good morning' : hour < 18 ? 'Good afternoon' : 'Good evening'; + + const circumference = 2 * Math.PI * 30; + const offset = circumference - (pct / 100) * circumference; + + document.getElementById('home-content').innerHTML = ` +
+
${greeting} 👋
+
Fix Your Anterior
Pelvic Tilt
+ +
+
+ + + + +
+ ${pct}% + DONE +
+
+
+
${doneCount} / ${total}
+
exercises done
+
Today's routine
+
+
+ + +
+ +
+
Your streak
+
+
🔥
+
+
${streak} day${streak !== 1 ? 's' : ''}
+
Current streak
+
${streak === 0 ? 'Complete today\'s routine to start your streak!' : 'Keep it up — consistency is everything.'}
+
+
+
+ +
+
Daily tip
+
+
💡
+
+

Posture Tip

+

${tip}

+
+
+
+ +
+
What is Anterior Pelvic Tilt?
+
+

The Postural Imbalance

+

Anterior pelvic tilt (APT) is a postural dysfunction where the front of the pelvis drops and the back rises, creating an exaggerated lower-back arch. It is caused by a muscle imbalance between tight and weak groups.

+
+
+
Tight → Stretch
+
    +
  • Hip Flexors
  • +
  • Quadriceps
  • +
  • Lower Back
  • +
+
+
+
Weak → Strengthen
+
    +
  • Glutes
  • +
  • Hamstrings
  • +
  • Core / Abs
  • +
+
+
+
+
+ `; +} + +/* ── EXERCISES ────────────────────────────────────────────── */ +let exerciseFilter = 'all'; + +function renderExercises() { + const filtered = exerciseFilter === 'all' + ? EXERCISES + : EXERCISES.filter((e) => e.category === exerciseFilter); + + document.getElementById('exercises-content').innerHTML = ` +
+ + + +
+
+ ${filtered.map((ex) => exerciseCardHTML(ex)).join('')} +
+ `; +} + +function setFilter(f) { + exerciseFilter = f; + renderExercises(); +} + +function exerciseCardHTML(ex) { + const stat1 = ex.duration + ? `
${ex.duration}s
Duration
` + : `
${ex.reps}
${ex.sides ? 'Reps/Side' : 'Reps'}
`; + + return ` +
+
+
${ex.emoji}
+
+
${ex.name}
+
+ ${ex.category === 'stretch' ? 'Stretch' : 'Strengthen'} + ${ex.muscle} +
+
+ + + +
+
+
+
${ex.sets}
Sets
+ ${stat1} + ${ex.sides ? '
2
Sides
' : ''} +
+
    + ${ex.steps.map((s, i) => `
  1. ${i + 1}${s}
  2. `).join('')} +
+
+ 💡 + ${ex.tip} +
+
+
+ `; +} + +function toggleCard(id) { + const card = document.getElementById('ec-' + id); + if (card) card.classList.toggle('open'); +} + +/* ── ROUTINE ──────────────────────────────────────────────── */ +function renderRoutine() { + const state = loadState(); + const today = todayKey(); + const completedToday = (state.completedToday || {})[today] || {}; + const doneCount = Object.keys(completedToday).length; + const allDone = doneCount === ROUTINE_ORDER.length; + + if (allDone) { + renderRoutineComplete(state); + return; + } + + // Find first not-done exercise as active + const activeId = currentExerciseId + || ROUTINE_ORDER.find((id) => !completedToday[id]) + || null; + if (!currentExerciseId) currentExerciseId = activeId; + + const activeEx = activeId ? EXERCISES.find((e) => e.id === activeId) : null; + + document.getElementById('routine-content').innerHTML = ` +
+

Today's Routine

+

${doneCount} of ${ROUTINE_ORDER.length} exercises completed

+
+ ${activeEx ? timerCardHTML(activeEx) : ''} +
+ ${ROUTINE_ORDER.map((id, idx) => routineItemHTML(id, idx, completedToday, activeId)).join('')} +
+
+ +
+ `; + + if (activeEx && activeEx.duration) { + initTimer(activeEx); + } +} + +function routineItemHTML(id, idx, completedToday, activeId) { + const ex = EXERCISES.find((e) => e.id === id); + if (!ex) return ''; + const done = !!completedToday[id]; + const isActive = id === activeId && !done; + const detail = ex.duration + ? `${ex.sets} sets × ${ex.duration}s${ex.sides ? ' per side' : ''}` + : `${ex.sets} sets × ${ex.reps} reps${ex.sides ? ' per side' : ''}`; + + return ` +
+
${done ? '✓' : idx + 1}
+
+
${ex.emoji} ${ex.name}
+
${detail}
+
+
${done ? '✅' : isActive ? '▶' : ''}
+
+ `; +} + +function selectRoutineExercise(id) { + const state = loadState(); + const today = todayKey(); + const completedToday = (state.completedToday || {})[today] || {}; + if (completedToday[id]) return; // already done + + clearTimer(); + currentExerciseId = id; + completedSets = 0; + currentSet = 1; + renderRoutine(); +} + +function timerCardHTML(ex) { + if (ex.duration) { + const display = formatTime(ex.duration); + const detail = `${ex.sets} sets · ${ex.duration}s${ex.sides ? ' each side' : ''}`; + return ` +
+
${ex.emoji} ${ex.name}
+
${detail}
+
${display}
+
Set ${completedSets + 1} of ${ex.sets}
+
+ ${Array.from({ length: ex.sets }, (_, i) => ` +
+ `).join('')} +
+
+ + + +
+
+ `; + } else { + const detail = `${ex.sets} sets · ${ex.reps} reps${ex.sides ? ' each side' : ''}`; + return ` +
+
${ex.emoji} ${ex.name}
+
${detail}
+
${ex.reps}
+
reps${ex.sides ? ' each side' : ''}
+
+ ${Array.from({ length: ex.sets }, (_, i) => ` +
+ `).join('')} +
+
Set ${completedSets + 1} of ${ex.sets}
+
+ +
+
+ `; + } +} + +function initTimer(ex) { + timerSeconds = ex.duration; + updateTimerDisplay(); +} + +function toggleTimer() { + if (timerRunning) { + pauseTimer(); + } else { + startTimer(); + } +} + +function startTimer() { + timerRunning = true; + const btn = document.getElementById('play-pause-btn'); + if (btn) btn.innerHTML = ` + + + + `; + + timerInterval = setInterval(() => { + if (timerSeconds > 0) { + timerSeconds--; + updateTimerDisplay(); + } else { + clearTimer(); + const ex = EXERCISES.find((e) => e.id === currentExerciseId); + if (ex) markSetDone(ex.id); + } + }, 1000); +} + +function pauseTimer() { + clearTimer(); + const btn = document.getElementById('play-pause-btn'); + if (btn) btn.innerHTML = ` + + + `; +} + +function resetCurrentTimer() { + clearTimer(); + const ex = EXERCISES.find((e) => e.id === currentExerciseId); + if (ex && ex.duration) { + timerSeconds = ex.duration; + updateTimerDisplay(); + } + const btn = document.getElementById('play-pause-btn'); + if (btn) btn.innerHTML = ` + + + `; +} + +function updateTimerDisplay() { + const el = document.getElementById('timer-display'); + if (el) el.textContent = formatTime(timerSeconds); +} + +function formatTime(secs) { + const m = Math.floor(secs / 60); + const s = secs % 60; + return m > 0 ? `${m}:${String(s).padStart(2, '0')}` : `${s}`; +} + +function markSetDone(exerciseId) { + clearTimer(); + const ex = EXERCISES.find((e) => e.id === exerciseId); + if (!ex) return; + + completedSets++; + + if (completedSets >= ex.sets) { + // Exercise fully complete + const state = loadState(); + const today = todayKey(); + if (!state.completedToday) state.completedToday = {}; + if (!state.completedToday[today]) state.completedToday[today] = {}; + state.completedToday[today][exerciseId] = true; + + // Advance streak + state.lastCompleted = today; + saveState(state); + + // Move to next exercise + const remaining = ROUTINE_ORDER.filter((id) => !state.completedToday[today][id]); + currentExerciseId = remaining.length > 0 ? remaining[0] : null; + completedSets = 0; + currentSet = 1; + + renderRoutine(); + } else { + // Next set + timerSeconds = ex.duration || 0; + renderRoutine(); + } +} + +function resetRoutine() { + const state = loadState(); + const today = todayKey(); + if (state.completedToday) delete state.completedToday[today]; + saveState(state); + clearTimer(); + currentExerciseId = null; + completedSets = 0; + renderRoutine(); +} + +function renderRoutineComplete(state) { + document.getElementById('routine-content').innerHTML = ` +
+
🎉
+
+

Routine Complete!

+

Great work. Your body thanks you. Come back tomorrow to keep your streak alive.

+
+
+
+ ${ROUTINE_ORDER.map((id, idx) => routineItemHTML(id, idx, (state.completedToday || {})[todayKey()] || {}, null)).join('')} +
+
+ +
+ `; +} + +/* ── PROGRESS ─────────────────────────────────────────────── */ +function renderProgress() { + const state = loadState(); + const streak = calcStreak(state); + const sessions = totalSessions(state); + const daysTracked = daysSinceStart(state); + const weekData = getWeekData(state); + + document.getElementById('progress-content').innerHTML = ` +
+
Overview
+
+
+
🔥
+
${streak}
+
Day Streak
+
+
+
+
${sessions}
+
Sessions Done
+
+
+
📅
+
${daysTracked}
+
Days Tracked
+
+
+
💪
+
${sessions > 0 ? Math.min(Math.round((sessions / Math.max(daysTracked, 1)) * 100), 100) : 0}%
+
Consistency
+
+
+
+ +
+
This Week
+
+
+ ${weekData.map((d) => ` +
+
${d.label}
+
${d.date}
+
+ `).join('')} +
+
+
+ +
+
Lifestyle Tips
+
+
+
🪑
+
+
Sitting Posture
+
Sit with feet flat, knees at 90°, and a small lumbar support. Avoid letting your pelvis tuck under (slumping) or arch excessively.
+
+
+
+
🚶
+
+
Walking Gait
+
Push off with your toes and drive your hip through on each stride. Lazy hip extension is a sign of weak glutes contributing to APT.
+
+
+
+
🏋️
+
+
Gym Exercises
+
Avoid over-arching in squats and deadlifts. Cue "ribs down, pelvis neutral" to protect the lower back and build correct patterns.
+
+
+
+
😴
+
+
Sleep Position
+
Back sleepers: put a pillow under your knees. Side sleepers: put a pillow between your knees. Both positions reduce overnight hip flexor shortening.
+
+
+
+
⏱️
+
+
Timeline
+
With daily practice, most people notice meaningful improvement in 6–8 weeks and significant correction by 12 weeks. Consistency beats intensity.
+
+
+
+
+ `; +} + +/* ── Helpers ─────────────────────────────────────────────── */ +function calcStreak(state) { + const completedToday = state.completedToday || {}; + let streak = 0; + const today = new Date(); + today.setHours(0, 0, 0, 0); + + for (let i = 0; i < 365; i++) { + const d = new Date(today); + d.setDate(today.getDate() - i); + const key = d.toISOString().slice(0, 10); + const done = completedToday[key]; + if (done && Object.keys(done).length === ROUTINE_ORDER.length) { + streak++; + } else { + break; + } + } + return streak; +} + +function totalSessions(state) { + const completedToday = state.completedToday || {}; + return Object.values(completedToday).filter( + (d) => d && Object.keys(d).length === ROUTINE_ORDER.length + ).length; +} + +function daysSinceStart(state) { + const completedToday = state.completedToday || {}; + const dates = Object.keys(completedToday).sort(); + if (dates.length === 0) return 0; + const first = new Date(dates[0]); + const now = new Date(); + return Math.floor((now - first) / 86400000) + 1; +} + +function getWeekData(state) { + const completedToday = state.completedToday || {}; + const days = ['S', 'M', 'T', 'W', 'T', 'F', 'S']; + const today = new Date(); + today.setHours(0, 0, 0, 0); + // Get Mon–Sun of current week + const dayOfWeek = today.getDay(); // 0=Sun + const monday = new Date(today); + monday.setDate(today.getDate() - ((dayOfWeek + 6) % 7)); + + return Array.from({ length: 7 }, (_, i) => { + const d = new Date(monday); + d.setDate(monday.getDate() + i); + const key = d.toISOString().slice(0, 10); + const isToday = d.getTime() === today.getTime(); + const done = completedToday[key] + && Object.keys(completedToday[key]).length === ROUTINE_ORDER.length; + return { + label: ['M', 'T', 'W', 'T', 'F', 'S', 'S'][i], + date: d.getDate(), + done, + today: isToday + }; + }); +} + +/* ── PWA Install ─────────────────────────────────────────── */ +let deferredInstallPrompt = null; + +window.addEventListener('beforeinstallprompt', (e) => { + e.preventDefault(); + deferredInstallPrompt = e; + const banner = document.getElementById('install-banner'); + if (banner) banner.classList.add('show'); +}); + +function installApp() { + if (!deferredInstallPrompt) return; + deferredInstallPrompt.prompt(); + deferredInstallPrompt.userChoice.then(() => { + deferredInstallPrompt = null; + dismissInstall(); + }); +} + +function dismissInstall() { + const banner = document.getElementById('install-banner'); + if (banner) banner.classList.remove('show'); +} + +/* ── Boot ─────────────────────────────────────────────────── */ +document.addEventListener('DOMContentLoaded', () => { + // Register service worker + if ('serviceWorker' in navigator) { + navigator.serviceWorker.register('/sw.js').catch(() => {}); + } + + // Wire up navigation + document.querySelectorAll('.nav-item').forEach((btn) => { + btn.addEventListener('click', () => navigate(btn.dataset.view)); + }); + + // Initial render + renderView('home'); +}); diff --git a/icons/icon-maskable.svg b/icons/icon-maskable.svg new file mode 100644 index 0000000..fb58555 --- /dev/null +++ b/icons/icon-maskable.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/icons/icon.svg b/icons/icon.svg new file mode 100644 index 0000000..612dc1d --- /dev/null +++ b/icons/icon.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/index.html b/index.html new file mode 100644 index 0000000..74932db --- /dev/null +++ b/index.html @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + TiltFix — Fix Anterior Pelvic Tilt + + + + +
+
+

📲 Add TiltFix to your Home Screen

+ Use it offline, anytime, like a native app. +
+
+ + +
+
+ + +
+ + +
+
+
+ + +
+ +
+
+ + +
+ +
+
+ + +
+ +
+
+ +
+ + + + + + + diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..127daeb --- /dev/null +++ b/manifest.json @@ -0,0 +1,24 @@ +{ + "name": "TiltFix — Anterior Pelvic Tilt", + "short_name": "TiltFix", + "description": "Fix your anterior pelvic tilt with guided stretches and strengthening exercises.", + "start_url": "/", + "display": "standalone", + "background_color": "#F4F7F5", + "theme_color": "#2F7A58", + "orientation": "portrait-primary", + "icons": [ + { + "src": "/icons/icon.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any" + }, + { + "src": "/icons/icon-maskable.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "maskable" + } + ] +} diff --git a/style.css b/style.css new file mode 100644 index 0000000..4657c6a --- /dev/null +++ b/style.css @@ -0,0 +1,875 @@ +/* ── Reset & Base ─────────────────────────────────────────── */ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +:root { + --clr-bg: #F4F7F5; + --clr-surface: #FFFFFF; + --clr-surface-2: #F0F5F2; + --clr-primary: #2F7A58; + --clr-primary-lt: #E4F2EB; + --clr-primary-dk: #1E5740; + --clr-accent: #F0A500; + --clr-accent-lt: #FFF3D6; + --clr-danger: #E05252; + --clr-danger-lt: #FDEAEA; + --clr-text: #111827; + --clr-text-2: #4B5563; + --clr-text-3: #9CA3AF; + --clr-border: #E5EBE8; + --clr-stretch: #4A7FB5; + --clr-stretch-lt: #EBF2FA; + --clr-strengthen: #2F7A58; + --clr-strengthen-lt:#E4F2EB; + + --radius-sm: 8px; + --radius-md: 14px; + --radius-lg: 20px; + --radius-xl: 28px; + + --shadow-sm: 0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04); + --shadow-md: 0 4px 16px rgba(0,0,0,.08); + --shadow-lg: 0 8px 32px rgba(0,0,0,.10); + + --nav-h: 72px; + --header-h: 64px; + --safe-bottom: env(safe-area-inset-bottom, 0px); + + --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; + --transition: 200ms cubic-bezier(.4,0,.2,1); +} + +html { height: 100%; } +body { + font-family: var(--font); + background: var(--clr-bg); + color: var(--clr-text); + height: 100%; + overflow: hidden; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +#app { + position: fixed; + inset: 0 0 calc(var(--nav-h) + var(--safe-bottom)) 0; + overflow-y: auto; + scroll-behavior: smooth; + -webkit-overflow-scrolling: touch; +} + +/* ── Bottom Navigation ────────────────────────────────────── */ +#bottom-nav { + position: fixed; + bottom: 0; + left: 0; right: 0; + height: calc(var(--nav-h) + var(--safe-bottom)); + padding-bottom: var(--safe-bottom); + background: var(--clr-surface); + border-top: 1px solid var(--clr-border); + display: flex; + align-items: flex-start; + justify-content: space-around; + z-index: 100; + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); +} + +.nav-item { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 4px; + padding: 12px 20px 0; + background: none; + border: none; + cursor: pointer; + color: var(--clr-text-3); + transition: color var(--transition); + flex: 1; + min-width: 0; +} + +.nav-item.active { + color: var(--clr-primary); +} + +.nav-item svg { + width: 24px; height: 24px; + flex-shrink: 0; +} + +.nav-label { + font-size: 10px; + font-weight: 600; + letter-spacing: .3px; + white-space: nowrap; +} + +.nav-indicator { + position: absolute; + bottom: calc(var(--nav-h) + var(--safe-bottom) - 3px); + width: 4px; height: 4px; + border-radius: 50%; + background: var(--clr-primary); + transition: transform var(--transition), opacity var(--transition); + opacity: 0; +} + +.nav-item.active .nav-indicator { opacity: 1; } + +/* ── Views ────────────────────────────────────────────────── */ +.view { + display: none; + min-height: 100%; + padding-bottom: 24px; + animation: fadeUp 260ms cubic-bezier(.4,0,.2,1) both; +} + +.view.active { display: block; } + +@keyframes fadeUp { + from { opacity: 0; transform: translateY(12px); } + to { opacity: 1; transform: translateY(0); } +} + +/* ── Page Header ──────────────────────────────────────────── */ +.page-header { + padding: 56px 20px 20px; + background: var(--clr-surface); + border-bottom: 1px solid var(--clr-border); +} + +.page-header h1 { + font-size: 28px; + font-weight: 700; + letter-spacing: -.5px; + line-height: 1.2; +} + +.page-header p { + font-size: 14px; + color: var(--clr-text-2); + margin-top: 4px; +} + +/* ── Cards ────────────────────────────────────────────────── */ +.card { + background: var(--clr-surface); + border-radius: var(--radius-lg); + padding: 20px; + box-shadow: var(--shadow-sm); + border: 1px solid var(--clr-border); +} + +.section { + padding: 20px 20px 0; +} + +.section-title { + font-size: 13px; + font-weight: 700; + letter-spacing: .8px; + text-transform: uppercase; + color: var(--clr-text-3); + margin-bottom: 12px; +} + +/* ── HOME VIEW ────────────────────────────────────────────── */ +.home-hero { + background: linear-gradient(135deg, var(--clr-primary-dk) 0%, var(--clr-primary) 100%); + padding: 52px 24px 32px; + color: white; +} + +.home-greeting { + font-size: 14px; + font-weight: 500; + opacity: .8; + margin-bottom: 4px; +} + +.home-title { + font-size: 26px; + font-weight: 700; + letter-spacing: -.4px; + line-height: 1.25; + margin-bottom: 24px; +} + +/* Progress ring */ +.progress-ring-wrap { + display: flex; + align-items: center; + gap: 20px; + background: rgba(255,255,255,.12); + border-radius: var(--radius-md); + padding: 16px 20px; + backdrop-filter: blur(8px); +} + +.ring-container { + position: relative; + width: 72px; height: 72px; + flex-shrink: 0; +} + +.ring-svg { transform: rotate(-90deg); } +.ring-track { fill: none; stroke: rgba(255,255,255,.2); stroke-width: 6; } +.ring-fill { + fill: none; + stroke: white; + stroke-width: 6; + stroke-linecap: round; + transition: stroke-dashoffset .6s cubic-bezier(.4,0,.2,1); +} + +.ring-text { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + color: white; +} + +.ring-pct { font-size: 16px; font-weight: 700; line-height: 1; } +.ring-lbl { font-size: 9px; font-weight: 600; opacity: .75; margin-top: 2px; } + +.ring-info { color: white; } +.ring-info .done { font-size: 24px; font-weight: 700; letter-spacing: -.4px; } +.ring-info .total { font-size: 14px; opacity: .7; margin-top: 1px; } +.ring-info .label { font-size: 12px; font-weight: 500; opacity: .6; margin-top: 2px; } + +.btn-start { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + width: 100%; + margin-top: 20px; + padding: 16px; + background: white; + color: var(--clr-primary-dk); + border: none; + border-radius: var(--radius-md); + font-family: var(--font); + font-size: 16px; + font-weight: 700; + cursor: pointer; + transition: transform var(--transition), box-shadow var(--transition); + box-shadow: 0 4px 20px rgba(0,0,0,.15); +} + +.btn-start:active { transform: scale(.98); } +.btn-start svg { width: 20px; height: 20px; } + +/* Streak */ +.streak-card { + display: flex; + align-items: center; + gap: 16px; +} + +.streak-icon { + font-size: 36px; + line-height: 1; +} + +.streak-info .streak-count { + font-size: 28px; + font-weight: 800; + color: var(--clr-accent); + letter-spacing: -.5px; + line-height: 1; +} + +.streak-info .streak-label { + font-size: 13px; + color: var(--clr-text-2); + margin-top: 2px; +} + +.streak-info .streak-sub { + font-size: 12px; + color: var(--clr-text-3); + margin-top: 2px; +} + +/* APT Info */ +.info-card {} +.info-card h3 { font-size: 16px; font-weight: 700; margin-bottom: 10px; } +.info-card p { font-size: 14px; color: var(--clr-text-2); line-height: 1.6; } + +.apt-diagram { + display: flex; + gap: 12px; + margin-top: 16px; +} + +.apt-col { + flex: 1; + background: var(--clr-surface-2); + border-radius: var(--radius-md); + padding: 14px 12px; + text-align: center; +} + +.apt-col .apt-col-title { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: .5px; + margin-bottom: 10px; +} + +.apt-col.tight .apt-col-title { color: var(--clr-danger); } +.apt-col.weak .apt-col-title { color: var(--clr-stretch); } + +.apt-col ul { + list-style: none; + display: flex; + flex-direction: column; + gap: 4px; +} + +.apt-col ul li { + font-size: 12px; + color: var(--clr-text-2); + background: var(--clr-surface); + border-radius: var(--radius-sm); + padding: 5px 8px; +} + +/* Tip */ +.tip-card { + display: flex; + gap: 14px; + align-items: flex-start; + background: var(--clr-accent-lt); + border-color: transparent; +} + +.tip-icon { font-size: 24px; flex-shrink: 0; margin-top: 1px; } +.tip-card h4 { font-size: 13px; font-weight: 700; color: var(--clr-accent); margin-bottom: 4px; } +.tip-card p { font-size: 13px; color: var(--clr-text-2); line-height: 1.5; } + +/* ── EXERCISES VIEW ───────────────────────────────────────── */ +.filter-bar { + display: flex; + gap: 8px; + padding: 16px 20px; + overflow-x: auto; + scrollbar-width: none; +} +.filter-bar::-webkit-scrollbar { display: none; } + +.filter-chip { + display: flex; + align-items: center; + gap: 6px; + padding: 7px 14px; + border-radius: 999px; + border: 1.5px solid var(--clr-border); + background: var(--clr-surface); + font-family: var(--font); + font-size: 13px; + font-weight: 600; + color: var(--clr-text-2); + cursor: pointer; + white-space: nowrap; + transition: all var(--transition); +} +.filter-chip.active { + background: var(--clr-primary); + border-color: var(--clr-primary); + color: white; +} +.filter-chip .dot { + width: 7px; height: 7px; + border-radius: 50%; +} + +.exercises-list { + padding: 0 20px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.exercise-card { + background: var(--clr-surface); + border-radius: var(--radius-lg); + border: 1px solid var(--clr-border); + overflow: hidden; + box-shadow: var(--shadow-sm); + transition: box-shadow var(--transition); +} + +.exercise-card-header { + display: flex; + align-items: center; + gap: 14px; + padding: 16px 18px; + cursor: pointer; + user-select: none; +} + +.exercise-icon { + width: 44px; height: 44px; + border-radius: var(--radius-md); + display: flex; + align-items: center; + justify-content: center; + font-size: 22px; + flex-shrink: 0; +} + +.exercise-icon.stretch { background: var(--clr-stretch-lt); } +.exercise-icon.strengthen { background: var(--clr-strengthen-lt); } + +.exercise-meta { flex: 1; min-width: 0; } +.exercise-name { font-size: 15px; font-weight: 700; margin-bottom: 3px; } +.exercise-tags { display: flex; gap: 6px; flex-wrap: wrap; } + +.tag { + font-size: 11px; + font-weight: 600; + padding: 2px 8px; + border-radius: 999px; +} +.tag.stretch { background: var(--clr-stretch-lt); color: var(--clr-stretch); } +.tag.strengthen { background: var(--clr-strengthen-lt); color: var(--clr-strengthen); } +.tag.muscle { background: var(--clr-surface-2); color: var(--clr-text-2); } + +.exercise-chevron { + color: var(--clr-text-3); + transition: transform var(--transition); + flex-shrink: 0; +} +.exercise-card.open .exercise-chevron { transform: rotate(180deg); } + +.exercise-body { + display: none; + padding: 0 18px 18px; + border-top: 1px solid var(--clr-border); +} +.exercise-card.open .exercise-body { display: block; } + +.exercise-stats { + display: flex; + gap: 10px; + padding: 14px 0 16px; +} + +.stat-pill { + flex: 1; + background: var(--clr-surface-2); + border-radius: var(--radius-md); + padding: 10px 8px; + text-align: center; +} +.stat-pill .sv { font-size: 16px; font-weight: 800; color: var(--clr-primary); } +.stat-pill .sl { font-size: 11px; color: var(--clr-text-3); font-weight: 600; margin-top: 1px; } + +.exercise-steps { list-style: none; display: flex; flex-direction: column; gap: 8px; } +.exercise-steps li { + display: flex; + gap: 12px; + align-items: flex-start; + font-size: 13px; + color: var(--clr-text-2); + line-height: 1.5; +} +.step-num { + width: 22px; height: 22px; + border-radius: 50%; + background: var(--clr-primary); + color: white; + font-size: 11px; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + margin-top: 1px; +} + +.exercise-tip { + display: flex; + gap: 8px; + margin-top: 14px; + padding: 12px; + background: var(--clr-accent-lt); + border-radius: var(--radius-md); + font-size: 12px; + color: var(--clr-text-2); + line-height: 1.5; +} +.exercise-tip span:first-child { flex-shrink: 0; } + +/* ── ROUTINE VIEW ─────────────────────────────────────────── */ +.routine-header-card { + margin: 16px 20px 0; + background: linear-gradient(135deg, var(--clr-primary-dk), var(--clr-primary)); + color: white; + border-color: transparent; +} + +.routine-header-card h2 { font-size: 20px; font-weight: 700; margin-bottom: 4px; } +.routine-header-card p { font-size: 13px; opacity: .75; } + +.routine-complete-banner { + margin: 16px 20px 0; + padding: 20px; + background: var(--clr-primary-lt); + border-radius: var(--radius-lg); + border: 1.5px solid var(--clr-primary); + display: flex; + align-items: center; + gap: 14px; +} +.routine-complete-banner .rbc-icon { font-size: 32px; } +.routine-complete-banner h3 { font-size: 16px; font-weight: 700; color: var(--clr-primary-dk); } +.routine-complete-banner p { font-size: 13px; color: var(--clr-text-2); margin-top: 2px; } + +/* Active exercise timer card */ +.timer-card { + margin: 16px 20px 0; + background: var(--clr-surface); + border-radius: var(--radius-xl); + padding: 24px 20px; + box-shadow: var(--shadow-lg); + border: 1px solid var(--clr-border); + text-align: center; +} + +.timer-exercise-name { + font-size: 18px; + font-weight: 700; + margin-bottom: 6px; +} + +.timer-exercise-meta { + font-size: 13px; + color: var(--clr-text-2); + margin-bottom: 24px; +} + +.timer-display { + font-size: 64px; + font-weight: 800; + letter-spacing: -3px; + color: var(--clr-primary); + line-height: 1; + margin-bottom: 8px; + font-variant-numeric: tabular-nums; +} + +.timer-label { font-size: 13px; color: var(--clr-text-3); margin-bottom: 24px; } + +.timer-controls { + display: flex; + gap: 12px; + justify-content: center; +} + +.btn-icon { + width: 52px; height: 52px; + border-radius: 50%; + border: none; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + transition: transform var(--transition), opacity var(--transition); + background: var(--clr-surface-2); + color: var(--clr-text-2); +} +.btn-icon:active { transform: scale(.94); } +.btn-icon.primary { + width: 64px; height: 64px; + background: var(--clr-primary); + color: white; + box-shadow: 0 4px 16px rgba(47,122,88,.35); + font-size: 24px; +} +.btn-icon.primary:active { transform: scale(.94); } +.btn-icon.done-btn { + background: var(--clr-primary-lt); + color: var(--clr-primary); +} + +/* Reps card (non-timed) */ +.reps-display { + font-size: 56px; + font-weight: 800; + letter-spacing: -2px; + color: var(--clr-primary); + line-height: 1; + margin-bottom: 6px; +} +.reps-label { font-size: 13px; color: var(--clr-text-3); margin-bottom: 24px; } + +.set-indicators { + display: flex; + gap: 8px; + justify-content: center; + margin-bottom: 24px; +} + +.set-dot { + width: 10px; height: 10px; + border-radius: 50%; + border: 2px solid var(--clr-border); + background: var(--clr-surface-2); + transition: all var(--transition); +} +.set-dot.done { background: var(--clr-primary); border-color: var(--clr-primary); } +.set-dot.current { border-color: var(--clr-primary); } + +/* Routine list */ +.routine-list { + padding: 16px 20px 0; + display: flex; + flex-direction: column; + gap: 8px; +} + +.routine-item { + display: flex; + align-items: center; + gap: 14px; + padding: 14px 16px; + background: var(--clr-surface); + border-radius: var(--radius-md); + border: 1px solid var(--clr-border); + transition: all var(--transition); + cursor: pointer; +} + +.routine-item.active-item { + border-color: var(--clr-primary); + background: var(--clr-primary-lt); + box-shadow: 0 0 0 3px rgba(47,122,88,.12); +} + +.routine-item.completed-item { + opacity: .55; +} + +.routine-item-num { + width: 28px; height: 28px; + border-radius: 50%; + background: var(--clr-surface-2); + border: 2px solid var(--clr-border); + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: 700; + color: var(--clr-text-3); + flex-shrink: 0; + transition: all var(--transition); +} +.routine-item.active-item .routine-item-num { + background: var(--clr-primary); + border-color: var(--clr-primary); + color: white; +} +.routine-item.completed-item .routine-item-num { + background: var(--clr-primary); + border-color: var(--clr-primary); + color: white; +} + +.routine-item-info { flex: 1; min-width: 0; } +.routine-item-name { font-size: 14px; font-weight: 600; } +.routine-item-detail { font-size: 12px; color: var(--clr-text-3); margin-top: 2px; } + +.routine-item-status { flex-shrink: 0; font-size: 18px; } + +/* Reset button */ +.btn-text { + display: block; + width: 100%; + margin: 20px 0 0; + padding: 12px; + background: none; + border: 1.5px solid var(--clr-border); + border-radius: var(--radius-md); + font-family: var(--font); + font-size: 14px; + font-weight: 600; + color: var(--clr-text-2); + cursor: pointer; + transition: all var(--transition); +} +.btn-text:active { background: var(--clr-surface-2); } + +/* ── PROGRESS VIEW ────────────────────────────────────────── */ +.stats-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; +} + +.stat-card { + background: var(--clr-surface); + border-radius: var(--radius-lg); + padding: 18px 16px; + border: 1px solid var(--clr-border); + box-shadow: var(--shadow-sm); +} +.stat-card .sc-icon { font-size: 26px; margin-bottom: 8px; } +.stat-card .sc-value { font-size: 28px; font-weight: 800; letter-spacing: -.6px; color: var(--clr-text); } +.stat-card .sc-label { font-size: 12px; color: var(--clr-text-3); font-weight: 600; margin-top: 2px; } + +/* Weekly calendar */ +.week-grid { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 8px; +} + +.week-day { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; +} + +.week-day-label { + font-size: 11px; + font-weight: 700; + color: var(--clr-text-3); + text-transform: uppercase; +} + +.week-day-dot { + width: 32px; height: 32px; + border-radius: 50%; + background: var(--clr-surface-2); + border: 2px solid var(--clr-border); + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + font-weight: 700; + color: var(--clr-text-3); + transition: all var(--transition); +} + +.week-day-dot.done { + background: var(--clr-primary); + border-color: var(--clr-primary); + color: white; +} + +.week-day-dot.today { + border-color: var(--clr-primary); + color: var(--clr-primary); + font-weight: 800; +} + +.week-day-dot.done.today { + color: white; +} + +/* Month heatmap */ +.heatmap { + display: flex; + flex-direction: column; + gap: 4px; +} + +.heatmap-row { + display: flex; + gap: 4px; +} + +.hm-cell { + width: 32px; height: 32px; + border-radius: 6px; + background: var(--clr-surface-2); + flex-shrink: 0; +} + +.hm-cell.done { background: var(--clr-primary); opacity: .85; } +.hm-cell.done-full { background: var(--clr-primary); } +.hm-cell.future { background: var(--clr-border); opacity: .5; } + +/* Lifestyle tips */ +.tips-list { display: flex; flex-direction: column; gap: 10px; } + +.lifestyle-tip { + display: flex; + gap: 14px; + align-items: flex-start; + padding: 14px 16px; + background: var(--clr-surface); + border-radius: var(--radius-md); + border: 1px solid var(--clr-border); +} +.lifestyle-tip .lt-icon { font-size: 24px; flex-shrink: 0; } +.lifestyle-tip .lt-title { font-size: 14px; font-weight: 700; margin-bottom: 3px; } +.lifestyle-tip .lt-desc { font-size: 13px; color: var(--clr-text-2); line-height: 1.5; } + +/* ── Utilities ────────────────────────────────────────────── */ +.mt-16 { margin-top: 16px; } +.mt-12 { margin-top: 12px; } +.mt-8 { margin-top: 8px; } + +/* ── Scrollbar ────────────────────────────────────────────── */ +#app::-webkit-scrollbar { width: 0; } + +/* ── Install Banner ───────────────────────────────────────── */ +#install-banner { + display: none; + position: fixed; + top: 12px; + left: 12px; right: 12px; + background: var(--clr-surface); + border-radius: var(--radius-lg); + padding: 16px 18px; + box-shadow: var(--shadow-lg); + border: 1px solid var(--clr-border); + z-index: 200; + flex-direction: column; + gap: 12px; + animation: fadeUp 240ms ease both; +} +#install-banner.show { display: flex; } +#install-banner p { font-size: 14px; font-weight: 600; } +#install-banner small { font-size: 12px; color: var(--clr-text-2); } + +.install-actions { display: flex; gap: 8px; } + +.btn-primary { + flex: 1; + padding: 10px; + background: var(--clr-primary); + color: white; + border: none; + border-radius: var(--radius-md); + font-family: var(--font); + font-size: 14px; + font-weight: 700; + cursor: pointer; + transition: opacity var(--transition); +} +.btn-primary:active { opacity: .85; } + +.btn-secondary { + padding: 10px 16px; + background: var(--clr-surface-2); + color: var(--clr-text-2); + border: none; + border-radius: var(--radius-md); + font-family: var(--font); + font-size: 14px; + font-weight: 600; + cursor: pointer; +} diff --git a/sw.js b/sw.js new file mode 100644 index 0000000..67699f9 --- /dev/null +++ b/sw.js @@ -0,0 +1,45 @@ +const CACHE_NAME = 'tiltfix-v1'; +const ASSETS = [ + '/', + '/index.html', + '/style.css', + '/app.js', + '/manifest.json', + '/icons/icon.svg', + '/icons/icon-maskable.svg' +]; + +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => cache.addAll(ASSETS)) + ); + self.skipWaiting(); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((keys) => + Promise.all( + keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key)) + ) + ) + ); + self.clients.claim(); +}); + +self.addEventListener('fetch', (event) => { + if (event.request.method !== 'GET') return; + event.respondWith( + caches.match(event.request).then((cached) => { + if (cached) return cached; + return fetch(event.request).then((response) => { + if (!response || response.status !== 200 || response.type === 'opaque') { + return response; + } + const clone = response.clone(); + caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone)); + return response; + }); + }) + ); +}); From e03ffc5fe51c73df77218b58432eb8de074f5e43 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Apr 2026 16:04:07 +0000 Subject: [PATCH 2/2] fix: move onclick to button elements; remove unused days variable Agent-Logs-Url: https://github.com/RehmTheGreat/RehmTheGreat.github.io/sessions/5c7f8e4d-7312-4561-b38c-593ef005a978 Co-authored-by: RehmTheGreat <191198362+RehmTheGreat@users.noreply.github.com> --- app.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app.js b/app.js index fec56f6..322c1df 100644 --- a/app.js +++ b/app.js @@ -407,13 +407,13 @@ function renderExercises() { document.getElementById('exercises-content').innerHTML = `
- -
@@ -864,7 +864,6 @@ function daysSinceStart(state) { function getWeekData(state) { const completedToday = state.completedToday || {}; - const days = ['S', 'M', 'T', 'W', 'T', 'F', 'S']; const today = new Date(); today.setHours(0, 0, 0, 0); // Get Mon–Sun of current week