diff --git a/design/README.md b/design/README.md
index 184ba2f..6d75ebf 100644
--- a/design/README.md
+++ b/design/README.md
@@ -251,20 +251,22 @@ grid-template-areas:
```
- **Primary row** (`.dl-lg-primary`, top-left) — pulse dot + the uppercase live label `DOWNLOADING` in `color-mix(in srgb, var(--accent) 80%, white)`, 13px / 600, `letter-spacing: 0.02em`. This is the only place the word "Downloading" appears in the component.
-- **Secondary row** (`.dl-lg-secondary`, bottom-left) — the live stats. 12px, three groups separated by `·` (0.45 opacity):
+- **Secondary row** (`.dl-lg-secondary`, bottom-left) — the live stats. 12px, four groups separated by `·` (0.45 opacity):
1. `11.4 GB / 35 GB` (`var(--t-1)` strong + `var(--t-2)` rest)
2. `47.6 MB/s` (`var(--t-1)`)
- 3. `8 min left` (`var(--t-2)`)
+ 3. `[users-icon] from 5 peers` — `.dl-peers`, inline-flex with 5px gap, icon at 0.7 opacity, count bold + tabular-nums in `var(--t-1)`, rest in `var(--t-2)`. Singular/plural switches on `peers === 1`. Hidden entirely when `game.peers` is falsy. Communicates that this is a LAN swarm transfer, not a single-source pull.
+ 4. `8 min left` (`var(--t-2)`)
- **pct column** — large percentage, 20px / 700, `letter-spacing: -0.01em`, `var(--t-1)`. `%` glyph at 12px / 600 / 0.55 opacity.
- **cancel column** — 28×28 square, `1px solid var(--bd-2)`, `border-radius: 6px`, X icon. Hover: bg `rgba(239,68,68,0.12)`, border `rgba(239,68,68,0.40)`, text `#fca5a5`. Cancelling reverts the game to its prior state (`local` if any data was kept, `none` otherwise) — dev decides the underlying behavior.
**Graceful degradation in narrow modals:**
```css
-@container (max-width: 380px) { .dl-lg-secondary .dl-eta, .dl-lg-secondary .dl-sep-eta { display: none; } }
+@container (max-width: 380px) { .dl-lg-secondary .dl-eta, .dl-lg-secondary .dl-sep-eta { display: none; } }
+@container (max-width: 300px) { .dl-lg-secondary .dl-peers, .dl-lg-secondary .dl-sep-peers { display: none; } }
```
-The ETA drops first if the modal narrows; bytes + speed stay (they're the actionable numbers). The pct/cancel column never collapses.
+ETA drops first, then peers; bytes + speed always stay (they're the actionable numbers). The pct/cancel column never collapses.
### Number formatting
@@ -287,10 +289,11 @@ type Game = {
state: 'installed' | 'local' | 'downloading' | 'none';
progress?: number; // 0–1, only when state === 'downloading'
speed?: number; // current throughput in MB/s
+ peers?: number; // number of LAN peers currently seeding
};
```
-In the real app, `progress` and `speed` come from the download worker (Tauri command emitting events). The mock's `useLiveDownload(game)` hook (in `components.jsx`) is just a placeholder — 600ms `setInterval` advancing `progress` proportional to `speed`, with `speed` smoothed via a low-pass filter and small random drift so the number doesn't look fake. Replace with a `useEffect` that subscribes to your real progress events; the rendering layer needs nothing else.
+In the real app, `progress`, `speed`, and `peers` come from the download worker (Tauri command emitting events). The mock's `useLiveDownload(game)` hook (in `components.jsx`) is just a placeholder — 600ms `setInterval` advancing `progress` proportional to `speed`, with `speed` smoothed via a low-pass filter and small random drift so the number doesn't look fake. `peers` is read straight off the game object (static in the mock); in production, push updates as peers join/leave the swarm — the `.dl-peers` chip re-renders silently. Replace the hook with a `useEffect` that subscribes to your real progress events; the rendering layer needs nothing else.
Filter changes:
- `Local` filter includes `installed` + `local` + `downloading` (in-flight downloads belong on the Local tab — you're managing them).
diff --git a/design/design_reference/components.jsx b/design/design_reference/components.jsx
index 64e6cb8..647426b 100644
--- a/design/design_reference/components.jsx
+++ b/design/design_reference/components.jsx
@@ -159,6 +159,15 @@ function DownloadProgress({ game, accent, size = 'md', full = false }) {
·
{fmtSpeed(speed)}
+ {game.peers > 0 && (
+
+ ·
+
+
+ from {game.peers} {game.peers === 1 ? 'peer' : 'peers'}
+
+
+ )}
·
{fmtEta(etaSec)} left
diff --git a/design/design_reference/data.jsx b/design/design_reference/data.jsx
index 15032ee..9d95a5d 100644
--- a/design/design_reference/data.jsx
+++ b/design/design_reference/data.jsx
@@ -18,7 +18,7 @@ const GAMES = [
{
id: 'avp', title: 'Aliens vs. Predator', size: 35.0, version: '2019.10.01',
desc: "Three campaigns, three nightmares. Be the alien stalking the dark, the predator hunting both, or the marine just trying to make it home with a working flashlight.",
- state: 'downloading', progress: 0.32, speed: 49.4, players: '2–16', tags: ['FPS', 'Horror', 'Multiplayer'],
+ state: 'downloading', progress: 0.32, speed: 49.4, peers: 5, players: '2–16', tags: ['FPS', 'Horror', 'Multiplayer'],
cover: { c1: '#064e3b', c2: '#020617', accent: '#34d399', mood: 'dark' },
},
{
@@ -120,7 +120,7 @@ const GAMES = [
{
id: 'quake3', title: 'Quake III Arena', size: 0.5, version: '2010.08.15',
desc: "Arena FPS in its most distilled form. Rocket jumps, rail-gun duels, and a netcode that's still the benchmark.",
- state: 'downloading', progress: 0.71, speed: 12.8, players: '2–16', tags: ['FPS', 'Arena', 'LAN'],
+ state: 'downloading', progress: 0.71, speed: 12.8, peers: 3, players: '2–16', tags: ['FPS', 'Arena', 'LAN'],
cover: { c1: '#7f1d1d', c2: '#0a0a0a', accent: '#fbbf24', mood: 'dark' },
},
{
diff --git a/design/design_reference/styles.css b/design/design_reference/styles.css
index 6c98a32..059d97b 100644
--- a/design/design_reference/styles.css
+++ b/design/design_reference/styles.css
@@ -784,6 +784,12 @@
}
.dl-lg-secondary .dl-bytes .dl-of { color: var(--t-2); font-weight: 500; }
.dl-lg-secondary .dl-speed { color: var(--t-1); font-weight: 600; white-space: nowrap; }
+.dl-lg-secondary .dl-peers {
+ display: inline-flex; align-items: center; gap: 5px;
+ color: var(--t-2); white-space: nowrap;
+}
+.dl-lg-secondary .dl-peers strong { color: var(--t-1); font-weight: 600; font-variant-numeric: tabular-nums; }
+.dl-lg-secondary .dl-peers svg { opacity: 0.7; }
.dl-lg-secondary .dl-eta { white-space: nowrap; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.dl-sep { opacity: 0.45; }
@@ -792,6 +798,11 @@
.dl-lg-secondary .dl-eta,
.dl-lg-secondary .dl-sep-eta { display: none; }
}
+/* Even narrower: drop peers too, keep size + speed */
+@container (max-width: 300px) {
+ .dl-lg-secondary .dl-peers,
+ .dl-lg-secondary .dl-sep-peers { display: none; }
+}
.dl-lg-pct {
grid-area: pct;