smooth-app
전 세계 식품의 성분·영양·정보를 자유롭게 협업하고 공개하는 데이터베이스
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기A free, offline-capable PWA workout tracker. Log sets, track estimated 1RM progress, and hit new PRs — no ads, no data selling. Built with Vue 3, TypeScript, and Pinia.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
A mobile-first Progressive Web App for tracking strength training, bodyweight, and personal records. Built with Vue 3 + TypeScript, Pinia, Supabase, and hand-rolled SVG — no UI component libraries, no external chart packages. Designed to feel like a native iOS app.
Logbook lets you track any strength exercise over time. Log a set (weight + reps + date), and the app immediately computes your estimated 1-rep max, detects whether you just hit a personal record, and plots your progress on a time-proportional SVG chart. Sign in with Google or email to sync your data across devices via Supabase.README 전문 보기 ↗A mobile-first Progressive Web App for tracking strength training, bodyweight, and personal records. Built with Vue 3 + TypeScript, Pinia, Supabase, and hand-rolled SVG — no UI component libraries, no external chart packages. Designed to feel like a native iOS app.
Logbook lets you track any strength exercise over time. Log a set (weight + reps + date), and the app immediately computes your estimated 1-rep max, detects whether you just hit a personal record, and plots your progress on a time-proportional SVG chart. Sign in with Google or email to sync your data across devices via Supabase.
weight × (1 + reps / 30)) computed on every setprefers-color-scheme? to view the shortcut help dialog| Layer | Choice | Why |
|---|---|---|
| Language | TypeScript (strict) | Type-safe stores, composables, and lib modules |
| UI framework | Vue 3 (<script setup>) | Fine-grained reactivity; single-file components |
| State | Pinia | Lightweight store; syncs to localStorage on every mutation |
| Backend / Auth | Supabase | Postgres with RLS, Google OAuth, email auth |
| Build | Vite 6 | Sub-second HMR, native ESM |
| PWA | vite-plugin-pwa + Workbox | Pre-caches all static assets; installable on iOS & Android |
| Charts | Hand-rolled SVG | <polyline> + <polygon> computed from normalized data — no chart library |
| Styling | CSS custom properties | All themes + glass tokens are a single data-theme attribute swap |
| Deployment | Vercel | Deployed from CI after the DB migration applies; environment variables set in dashboard |
Every action updates Pinia state and localStorage immediately, then fires a Supabase call in the background. The UI never waits on the network.
Tab content components are lazy-loaded via defineAsyncComponent() with dynamic import(). This keeps the initial bundle small — only the auth screen and shell load upfront; heavy components like WorkoutTracker (34 KB) load on demand after sign-in.
getExercisePR(exerciseId, sinceDate) returns the max estimated1RM across an exercise's sets on or after sinceDate (all-time when it is null). Any set where set.estimated1RM === PR gets the gold treatment — in both the workout list and the calendar.
sinceDate comes from one place, usePRBaseline().prBaselineDate, which resolves the strength baseline mode (#1272): Lifetime uses the manual "evaluate PRs since" anchor as-is, while Recent uses a rolling window (8 weeks by default) so a cut is measured against recent work instead of an out-of-reach peak-bulk PR. Both modes collapse to that single day key, so badges, log-set suggestions, and XP scoring stay in agreement without any of them knowing the mode exists.
A prMap computed property (YYYY-MM-DD → Set<exerciseName>) is derived from the store at render time. The calendar reads this to show trophy badges on cells and exercise tags without any extra queries.
Each theme defines --glass-fill, --glass-edge, --glass-shine, --glass-bar, --glass-overlay, and --mesh tokens. When data-glass="on" (default), cards and chrome use backdrop-filter: blur() with translucent fills. data-glass="off" overrides fall back to solid --bg-secondary / --bg-elevated values. The tab bar indicator only renders in glass mode.
A debounced sync queue (lib/syncQueue.ts) batches rapid Pinia mutations into coalesced Supabase writes. A conflict resolver (lib/conflictResolver.ts) implements last-write-wins with updated_at timestamp comparison when merging remote and local state. Sets are resolved one at a time by their own stamp (lib/setConflict.ts), because editing a set never moves its exercise's stamp on the server.
The Privacy Policy and Terms of Service have one source, src/lib/legalCopy.ts. The in-app Legal sheet renders it, and vite-plugin-legal-pages.ts emits it at build as /legal/privacy.html and /legal/terms.html — the public URLs App Store Connect and App Review need — so the two cannot drift. vercel.json excludes legal/ from the SPA fallback so a wrong path 404s.
All interactive elements meet Apple's 44pt minimum touch target. Font sizes are 11pt minimum throughout. Toggle switches are 51×31pt. Text contrast ratios are tuned per-theme for WCAG AA compliance. Safe areas are respected for notched devices.
1279 tests across 54 test files, covering stores, composables, library modules, and Vue components:
| Layer | What's covered |
|---|---|
| Stores | Exercise/set CRUD, Epley 1RM, PR detection, bodyweight stats, preference toggles, progression XP and theme unlocks, sync fuzzing |
| Composables | Theme switching, color modes, auth flows (OAuth, email, sign-up with duplicate detection), keyboard shortcuts, undo toast, swipe-to-dismiss, focus trap, haptics, PR burst, tag recovery/volume |
| Library | Sync queue debouncing/coalescing, conflict resolver (last-write-wins, merge strategies), CSS/meta/manifest/SEO/theme-contrast/vercel-headers regression suites, CSV import, data export, plate calculator, XP migration |
| Components | WorkoutTracker, BodyweightTracker, AuthScreen, ExerciseGraph, OnboardingScreen, CalendarView, ErrorBoundary, StarterPickerFlow, SkeletonLoader, MuscleGroupChart/Recovery, VolumeTrendChart, PR target, accessibility attributes |
npm test # run all tests
npx vitest --ui # interactive test explorer
GitHub Actions runs on every push and PR to master:
npm ci — deterministic installnpm run lint — ESLint with Vue plugin (0 errors)npm run build — Vite production buildnpm test — full test suite@lhci/cli autorun against npm run preview and asserts category budgets (accessibility ≥ 0.87 as a hard gate — the observed baseline, to be ratcheted toward 0.9 as the failing audits are fixed; performance/best-practices/SEO as advisory warnings). Config in lighthouserc.json; full HTML reports upload as the lighthouse-report artifact. Complements the static 512 KB JS bundle budget with real rendered-page Core Web Vitals.Exercise, WorkoutSet, BodyweightEntry, FeatureFlags)eslint-plugin-vue/recommended + typescript-eslint — enforced via CI and pre-commit hook.js and .vue files before every commitWorkoutTracker, CalendarView, BodyweightTracker) lazy-loaded via defineAsyncComponent with dynamic importsgit clone https://github.com/aschung212/Lift.git
cd lift
npm install
Create .env.local with your Supabase credentials:
VITE_SUPABASE_URL=https://your-project.supabase.co
VITE_SUPABASE_ANON_KEY=your-anon-key
.env.local is gitignored, so a git worktree (.claude/worktrees/…) starts without it — and a build made there ships with no Supabase at all: the auth screen reads "Supabase not configured", and a native app built from that bundle carries the same. Copy only the two client lines across before building there (the file also holds admin-only secrets):
grep -E '^VITE_SUPABASE_(URL|ANON_KEY)=' ~/development/lift/.env.local > .env.local
grep -l supabase.co dist/assets/*.js after a build proves the URL made it in.
Run the Supabase migration in supabase/migration.sql to create the required tables and RLS policies, then:
npm run dev # http://localhost:5173
npm run build # outputs to dist/
npm run preview # preview production build locally
Push to GitHub, connect to Vercel, and add VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY as environment variables in the Vercel dashboard. Enable Google as an OAuth provider in Supabase and add your Vercel domain to the allowed redirect URLs. For password resets, add {{ .Token }} to the Reset Password email template (Authentication → Email Templates): the app verifies that 6-digit code in place, which is the only reset path that works inside the native build (the emailed link only completes in the browser that requested it).
Production is not deployed by Vercel's git integration: vercel.json sets git.deploymentEnabled: { master: false }, and CI's deploy-production job builds and deploys after migrate-db has applied the schema, so new code never reaches users ahead of the column it reads. PR branches still get git-integration previews as usual. That job needs three repository secrets — VERCEL_TOKEN (create at https://vercel.com/account/tokens) plus VERCEL_ORG_ID and VERCEL_PROJECT_ID (the orgId/projectId in .vercel/project.json after running vercel link). Without them nothing reaches production, so the job fails fast and says exactly that.
The PWA is wrapped with Capacitor 8 for the App Store. The
shared config lives in capacitor.config.ts (appId: com.aschung212.lift), and the
native ios/ project is committed — it is the App Store build: the icon, the launch
screen, PrivacyInfo.xcprivacy, the HealthKit entitlement and the build settings live in
git like any other shipping artifact. Capacitor's own ios/.gitignore keeps the generated
parts out (App/App/public, capacitor.config.json, DerivedData, xcuserdata), which
is why a fresh clone must sync once before Xcode can build:
# Build the web bundle and sync it into the native project (also stamps the version)
npm run cap:build # = build + cap sync (+ configure-ios.mjs) + guard:dev-surface --native + guard:native-config
# Open the project in Xcode, then build & run on a Simulator or your iPhone
npm run cap:open:ios
cap:build sets CAPACITOR_BUILD=true on both halves — the vite build and the
cap sync — because that is the release discriminator: vite.config.js reads it to
disable the service worker (#532), and capacitor.config.ts reads it to ignore
CAPACITOR_DEV_URL (LIFT-1435). A VAR=value cmd prefix binds to one command, so the
duplication is deliberate. Live reload is unaffected: CAPACITOR_DEV_URL=http://192.168.1.x:5173 npx cap sync sets no such flag and still points the WebView at the Vite dev server, and
npx cap run ios --live-reload (Capacitor's own route, which writes the same server.url
into the generated config and reverts it on Ctrl-C) builds again now that ios.scheme no
longer names a scheme the project does not have — #1442. The last step,
npm run guard:native-config (scripts/check-native-release-config.mjs), re-reads the
capacitor.config.json that cap sync actually emitted and fails the build if it carries
server.url, cleartext or allowNavigation, or if the web bundle was never copied in:
that file is gitignored and copied into the .ipa, so it is what an archive really ships,
and a bundle that loads its whole UI from a LAN dev server is indistinguishable from a good
one until it is installed. Run it by hand any time you are unsure what state the native
project is in; the same script also runs (in --warn mode) at the end of every sync,
including the live-reload one that creates that state. It is not a CI step — the file it
reads is generated and gitignored, so CI never has one to check.
The step before it, npm run guard:dev-surface -- --native, is the same guard CI runs
against dist/, pointed at ios/App/App/public — the copied bundle the .ipa embeds
(LIFT-1454). Every other run of that guard inspects a bundle some automated environment
produced; cap:build builds its own locally, so until this step existed the App Store
build was the one bundle nothing had ever checked. import.meta.env.DEV is false under
vite build, so the way a dev surface gets in is VITE_E2E being true in the archiving
shell — and Vite reads .env.local / .env.* for every build, so a stray line in an
untracked file is enough to ship the "Continue as Dev" auth bypass and the Settings dev
tools to the App Store. It scans the copy rather than the dist/ it came from for the
same reason guard:native-config reads the emitted config rather than the TypeScript, and
it resolves its directories from the platform table in
scripts/check-native-release-config.mjs, so a second platform is covered by being
declared once. Like its sibling it is not a CI step (that tree is generated and
gitignored), and like its sibling it also runs in --warn mode from the
capacitor:sync:after hook — because cap:build is not the only thing that writes the
tree: a plain VITE_E2E=true npm run build && npx cap sync copies the leak in and leaves a
capacitor.config.json that guard:native-config finds perfectly clean.
Every npx cap sync also runs scripts/configure-ios.mjs (first in the
capacitor:sync:after hook in package.json). On the committed project the only thing it changes is the
version pair: MARKETING_VERSION from package.json and CURRENT_PROJECT_VERSION from
the commit count on the current branch, so every archive cut from master carries a build
number App Store Connect accepts as newer than the last — bump package.json's version
for a new App Store version, never the project file. If the project is ever regenerated
(rm -rf ios && npx cap add ios --packagemanager SPM), the same hook re-applies
everything the stock template lacks: the HealthKit usage strings and the
ITSAppUsesNonExemptEncryption export-compliance key in Info.plist, the
com.apple.developer.healthkit entitlement wired into the App target, the iOS 16.0
deployment target, iPhone-only TARGETED_DEVICE_FAMILY, and PrivacyInfo.xcprivacy wired
into Copy Bundle Resources. It is idempotent and can be run by hand with
npm run cap:configure:ios; scripts/__tests__/configure-ios.test.mjs runs it against
the real template shipped in @capacitor/cli and asserts the committed project already
carries all of it, so a setting flipped by hand in Xcode fails CI. The icon and the native
launch screen come from the same sources as the PWA's: public/icon-source.png is copied
into the asset catalog, and node scripts/generate-launch-screens.js --native renders the
Eternal mark at 2732×2732 into Splash.imageset. The Simulator needs no signing; a
physical iPhone needs a signing team, and a free Personal Team works, HealthKit included.
Settings → Apple Health appears only in this build (HealthKit has no web API). Re-run
npm run cap:build after any web change to re-sync the dist/ bundle into the native
shell.
├── public/
│ ├── icon.svg
│ ├── icon-192.png
│ ├── icon-512.png
│ ├── apple-touch-icon.png
│ └── launch/ # iOS PWA launch screens (apple-touch-startup-image), per device
├── scripts/
│ ├── generate-icons.js
│ └── generate-launch-screens.js # Renders themed iOS launch PNGs (pure Node, no deps)
├── src/
│ ├── components/
│ │ ├── WorkoutTracker.vue # Exercise list, detail modal, log/edit modal, rest timer, tags
│ │ ├── ExerciseGraph.vue # Per-exercise SVG line chart (time-proportional)
│ │ ├── CalendarView.vue # Monthly/weekly calendar, PR map, set detail, tag filtering
│ │ ├── BodyweightTracker.vue # Weight log, period stats, SVG chart, low/high badges
│ │ ├── AuthScreen.vue # Email/password + Google sign-in
│ │ ├── ErrorBoundary.vue # Global error handler with fallback UI
│ │ ├── OnboardingScreen.vue # Welcome flow with 3 entry paths
│ │ └── __tests__/ # Component tests (159 tests)
│ ├── composables/
│ │ ├── useTheme.ts # Themes, mode (light/auto/dark), glass toggle, rest timer toggle
│ │ ├── useAuth.ts # Supabase session, OAuth + email auth, store init on sign-in
│ │ ├── useAnalytics.ts # Lightweight event logging
│ │ ├── useUndoToast.ts # Undo toast with timed rollback
│ │ ├── useSwipeToDismiss.ts # Touch gesture dismissal for modals and sheets
│ │ ├── useKeyboardShortcuts.ts # Global keyboard shortcuts with help dialog
│ │ └── __tests__/ # Composable tests (43 tests)
│ ├── stores/
│ │ ├── workout.ts # Exercises + sets CRUD, Epley 1RM, PR getter, tags, Supabase sync
│ │ ├── bodyweight.ts # Weight entries CRUD, min/max getters, Supabase sync
│ │ ├── preferences.ts # Feature toggles (tab visibility), Supabase sync
│ │ ├── templates.ts # Workout template save/load
│ │ └── __tests__/ # Store tests (76 tests)
│ ├── lib/
│ │ ├── supabase.ts # Supabase client singleton
│ │ ├── migrate.ts # One-time localStorage → Supabase migration
│ │ ├── syncQueue.ts # Debounced sync queue for batching Supabase writes
│ │ ├── conflictResolver.ts # Last-write-wins conflict resolution for multi-device sync
│ │ ├── setConflict.ts # Per-set last-write-wins: each set resolved by its own stamp
│ │ ├── tagColors.ts # Theme-aware tag color mapping
│ │ ├── uuid.ts # UUID generation utility
│ │ └── __tests__/ # Library tests (21 tests)
│ ├── App.vue # Tab bar, settings sheet, theme picker, auth gate
│ ├── main.ts # App entry point
│ └── index.css # All theme tokens, glass tokens, component styles
├── supabase/
│ └── migration.sql # Creates exercises, sets, bodyweight_entries with RLS
├── .github/
│ ├── workflows/ci.yml # GitHub Actions: lint, build, test
│ ├── ISSUE_TEMPLATE/ # Bug report and feature request templates
│ └── pull_request_template.md # PR template with checklist
├── CONTRIBUTING.md # Contribution guide with setup, conventions, and PR process
├── index.html
├── vite.config.js
├── vitest.config.js
├── eslint.config.js
└── vercel.json
MIT