尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證製作者的介紹僅有上面的一句話。 更多詳情請閱讀儲存庫 README。 儲存庫 README ↗
正在載入完整 README。
檢視完整 README ↗Distance–Time–Speed calculator for runners. Features two distinct modes: a Single pace calculator (enter any two of time, distance, or speed/pace to compute the third) and a Multi pace workout planner (upload a GPX route, divide it into segments, and plan varying paces across elevation profiles). Metric and imperial units. Static HTML with modular ES modules. Mobile-first layout with responsive design.
.gpx routes (5km to 45km) into 100m pace blocks+ Add Zone and Delete actions to manage pace zones123456 → 01:23:45) or pace (e.g. 530 → 05:30)| Layer | Technology |
|---|---|
| UI | Static HTML, vanilla JS ES modules |
| Styling | CSS inlined from css/styles.css, self-hosted JetBrains Mono |
| Persistence | localStorage, cache-first service worker (sw.js) |
| Hosting | Vercel (static) |
flowchart TB
subgraph UI [UI Layer]
Mode[Mode Toggle]
subgraph SingleUI [Single Mode]
Fields[Fields: Time/Dist/Speed/Pace]
Circles[Target Circles]
end
subgraph MultiUI [Multi Mode]
Upload[GPX Upload]
Chart[Elevation Chart]
Zones[Pace Zones]
end
Toggles[Unit Toggle]
Reset[Reset Buttons]
end
subgraph Logic [JS Modules]
Calculator[calculator.js]
Normalize[normalize.js]
Convert[convert.js]
GPX[gpx.js]
Multi[multi-pace.js]
end
subgraph Storage [Local Storage]
LS[(localStorage)]
end
Mode --> Calculator
SingleUI --> Calculator
MultiUI --> Multi
Upload --> GPX
GPX --> Multi
Calculator --> Normalize
Calculator --> Convert
Multi --> Convert
Multi --> Normalize
Calculator --> LS
Multi --> LS
DTS-Calc/
├── index.html # App entrypoint (styles inlined for first paint)
├── sw.js # Cache-first shell, one build per cache
├── css/
│ └── styles.css # Source styles; inlined into index.html
├── js/
│ ├── calculator.js
│ ├── viewport-lock.js # Keeps the fixed app above the keyboard
│ ├── normalize.js
│ ├── convert.js
│ ├── gpx.js
│ └── multi-pace.js
├── assets/
│ ├── icon.svg # 8×8 header glyph (transparent, black pixels)
│ ├── dts-calc-favicon.svg # Browser-tab icon (10×10, white on #C20707)
│ ├── dts-calc-favicon-32.png
│ ├── dts-calc-favicon-16.png
│ ├── home-screen-icon.svg # Home-screen source: 8×8 glyph, 2-cell padding (12×12)
│ ├── apple-touch-icon.png # 180×180 home-screen icon
│ ├── icon-192.png # 192×192 installed-app icon
│ ├── icon-512.png # 512×512 installed-app icon
│ └── icon-maskable-512.png # 512×512 maskable icon (same tile; padding covers the safe zone)
├── assets/fonts/
│ ├── jetbrains-mono-latin.woff2 # Latin variable font, axis 400–800
│ └── OFL.txt
├── manifest.json # App name and home-screen / installed-app icon set
├── vercel.json # Rewrite `/` → index.html; `sw.js` is `Cache-Control: no-cache`
└── README.md
The in-app header mark is the inline 8×8 glyph in index.html (same cells as assets/icon.svg). The browser tab uses assets/dts-calc-favicon.svg (<link rel="icon" type="image/svg+xml">) plus dts-calc-favicon-32.png and dts-calc-favicon-16.png. The manifest does not list the favicon.
Home-screen and installed-app icons are a separate tile: the 8×8 glyph in white #FFFFFF on app red #C20707, with 2 grid squares of padding (12×12) and square corners. assets/home-screen-icon.svg is that source. apple-touch-icon.png, icon-192.png, icon-512.png, and icon-maskable-512.png are nearest-neighbour scales of the 1024×1024 master, opaque RGB with no alpha. The manifest points at those files, not the favicon.
| Route | Purpose |
|---|---|
/ | Main calculator (rewrites to index.html) |
compute() (200ms)compute()lastEdited (PACE or SPEED) decides which drives the othersaveState() / loadState() applied on init, compute, clear, and reset events for both modes separately.iOS 26 draws a frosted scroll-edge over the header of an installed home-screen app when html / body can scroll. That was happening here because .ctds-multi-shell was 100dvh under the header (about 82px past the viewport) and the chart height subtracted a hardcoded --mp-header-h of 48px.
html and body now fill the viewport with overflow: hidden and overscroll-behavior: none (no rubber-band). The header stays a flex row at the top of .ctds-app, padded by the existing safe-area inset. .ctds-scroll is the only scroller (overflow-y: auto, overscroll-behavior: contain). Multi mode fills the space under the header when it fits. On a short or landscape screen the elevation panel keeps a minimum height and .ctds-scroll moves so the totals, chart, and footer stay reachable. viewport-fit=cover lets the existing safe-area padding clear the status bar. theme-color is the solid app red and there is no status-bar meta — neither one is the frost.
When the keyboard opens, viewport-lock.js sizes the app to the visible viewport so inputs and the pace sheet stay reachable. The document does not scroll.
123456) → 01:23:45 on blur530) → 05:30 on blurPrerequisites: None to read the page. The service worker needs an http origin, so use a local server rather than opening the file directly.
For regression tests and manual verification, see CONTRIBUTING.md.
python3 -m http.server 8000
# Visit http://localhost:8000/
After editing css/styles.css or any other shell file, run npm run build. That inlines the stylesheet, stamps the service worker's build hash, and copies the shell into dist/. Running it again on an up-to-date tree does not edit committed files. The build uses only Node's own modules, so it does not need the test dependencies. Vercel runs the same command and publishes dist/ (framework is null, outputDirectory is dist). CI fails if the build would edit a committed file.
The home-screen icon opens start_url /, which is inside manifest scope /. There is no redirect. index.html contains the full stylesheet, so the first paint does not wait on a second CSS request or on fonts.googleapis.com. JetBrains Mono is a self-hosted latin subset with font-display: swap (the face is discovered in that first HTML response, so it is not preloaded). The font file's weight axis is 400–800 and the face exposes that range. Text uses monospace until the file arrives, then the same family and tabular figures as before.
sw.js is registered from the load event. It is cache-first for one whole shell (HTML, JS, font, icons). The build hash in the cache name is the hash of those files, stamped by npm run build, so a deploy installs a new worker that precaches the full list. Cached HTML points its scripts and font at /b/<hash>/.... Icons and the manifest stay on stable URLs such as /assets/apple-touch-icon.png and /manifest.json, so a home-screen fetch that does not go through the worker still finds them. vercel.json rewrites /b/<hash>/... to the same file in dist/. A later script request is served from that build's cache, including Multi opened after the worker process has stopped. If that older cache has been evicted, the request falls through to the current file until the next navigation. A navigation also refetches the whole list with cache: 'no-cache' into a new cache and publishes it only when every file is stored. If the current build cache is missing while its meta entry remains, that launch reads the shell from the network and stores the cache again. The open that discovers the deploy keeps the previous shell, including Multi if it is opened later. The second launch after the deploy runs the new shell. Activate deletes caches from other builds once no open page still needs them; which page needs which build is stored in the cache, not only in the worker process. The worker does not reload open pages. css/styles.css is not requested at runtime, so it is not part of the shell. vercel.json sends Cache-Control: no-cache for /sw.js so the browser can detect a new worker script.
Deploy to Vercel:
vercel
vercel.json rewrites / to /index.html, and /b/<hash>/<path> to /<path>. Those routes apply to the published dist/ files, including /sw.js.