Skip to content

Commit 7c24abb

Browse files
authored
Merge pull request #13 from GilbN/feat/enhance-ux
Refactor PeerList and SeriesProgress components; add modal for peer list and enhance series metadata display
2 parents 266a9ca + bf6275e commit 7c24abb

11 files changed

Lines changed: 752 additions & 207 deletions

File tree

src/components/ControlBar.svelte

Lines changed: 200 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,127 @@
11
<script>
2-
import { timerState } from '../lib/stores.js'
3-
import { t } from '../lib/i18n.js'
2+
import { timerState, preferences } from '../lib/stores.js'
3+
import { t, getLocalizedName } from '../lib/i18n.js'
4+
import { getProgramById } from '../lib/programs/registry.js'
45
5-
let { onStart, onPause, onResume, onStop, onNext, onReset } = $props()
6+
let {
7+
onStart,
8+
onPause,
9+
onResume,
10+
onStop,
11+
onRestart,
12+
onNextSeries,
13+
onNext,
14+
onReset,
15+
bottomExtra = undefined,
16+
} = $props()
17+
18+
let program = $derived(getProgramById($timerState.programId))
19+
let stage = $derived(program?.stages[$timerState.stageIndex])
20+
let lang = $derived($preferences.lang)
21+
let reason = $derived($timerState.stoppedReason)
22+
23+
// Label for the next exercise in the current stage (unnamed exercises fall back to numbered label).
24+
function exerciseLabelFor(idx) {
25+
return `${$t('exercise')} ${idx + 1}`
26+
}
27+
28+
// Slot 3 descriptor: { kind: 'stage' | 'exercise', name }
29+
// null when there is no forward target (program end).
30+
let slot3 = $derived.by(() => {
31+
if (!program || !stage) return null
32+
if (reason === 'programComplete') return null
33+
34+
// Stage boundary crossed (or about to cross)
35+
if (reason === 'stageComplete') {
36+
const nextStage = program.stages[$timerState.stageIndex + 1]
37+
if (!nextStage) return null
38+
return { kind: 'stage', name: getLocalizedName(nextStage.name, lang) ?? '' }
39+
}
40+
41+
// Next exercise exists in the current stage
42+
const nextExIdx = $timerState.exerciseIndex + 1
43+
if (stage.exercises[nextExIdx]) {
44+
return { kind: 'exercise', name: exerciseLabelFor(nextExIdx) }
45+
}
46+
47+
// No more exercises in stage — show next stage as the forward action
48+
const nextStage = program.stages[$timerState.stageIndex + 1]
49+
if (nextStage) {
50+
return { kind: 'stage', name: getLocalizedName(nextStage.name, lang) ?? '' }
51+
}
52+
53+
return null
54+
})
55+
56+
// Next-series button is visible only when a next series actually exists in the current exercise.
57+
// - After 'seriesComplete' seriesIndex was advanced, so it points at the upcoming series.
58+
// - After 'aborted' seriesIndex still points at the aborted series, so "next" = seriesIndex + 1.
59+
let showNextSeries = $derived.by(() => {
60+
const exercise = stage?.exercises[$timerState.exerciseIndex]
61+
if (!exercise) return false
62+
if (reason === 'seriesComplete') return $timerState.seriesIndex < exercise.seriesCount
63+
if (reason === 'aborted') return $timerState.seriesIndex + 1 < exercise.seriesCount
64+
return false
65+
})
66+
67+
// Exactly one primary (green) button per phase. Which slot?
68+
let primary = $derived.by(() => {
69+
if (reason === 'programComplete') return null
70+
if (reason === 'stageComplete' || reason === 'exerciseComplete') return 'slot3'
71+
if (showNextSeries) return 'slot2'
72+
return 'slot3'
73+
})
674
</script>
775
876
<div class="control-bar">
977
<div class="main-actions">
10-
{#if $timerState.phase === 'idle' || $timerState.phase === 'stopped'}
11-
<button class="btn-action btn-start" onclick={onStart}>
78+
{#if $timerState.phase === 'idle'}
79+
<button class="btn-action primary" onclick={onStart}>
1280
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
13-
{$timerState.phase === 'stopped' ? $t('startNextSeries') : $t('start')}
81+
{$t('start')}
82+
</button>
83+
84+
{:else if $timerState.phase === 'stopped' && reason === 'programComplete'}
85+
<div class="program-complete-label">{$t('programComplete')}</div>
86+
87+
{:else if $timerState.phase === 'stopped'}
88+
<!-- Slot 1: Restart series (always shown while stopped) -->
89+
<button
90+
class="btn-action btn-restart"
91+
class:primary={primary === 'slot1'}
92+
onclick={onRestart}
93+
>
94+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
95+
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/>
96+
<path d="M3 3v5h5"/>
97+
</svg>
98+
{$t('restartSeries')}
1499
</button>
15-
{#if $timerState.phase === 'stopped'}
16-
<button class="btn-action btn-next" onclick={onNext}>
100+
101+
<!-- Slot 2: Next series -->
102+
{#if showNextSeries}
103+
<button
104+
class="btn-action btn-next-series"
105+
class:primary={primary === 'slot2'}
106+
onclick={onNextSeries}
107+
>
108+
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
109+
{$t('nextSeries')}
110+
</button>
111+
{/if}
112+
113+
<!-- Slot 3: Next exercise / Next stage -->
114+
{#if slot3}
115+
<button
116+
class="btn-action btn-next-context"
117+
class:primary={primary === 'slot3'}
118+
onclick={onNext}
119+
>
17120
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 18l8.5-6L6 6v12zm2-8.14L11.03 12 8 14.14V9.86zM16 6h2v12h-2z"/></svg>
18-
{$t('nextExercise')}
121+
{slot3.kind === 'stage' ? $t('nextStageLabel') : $t('nextExerciseLabel')}
19122
</button>
20123
{/if}
124+
21125
{:else if $timerState.phase === 'loading' || $timerState.phase === 'shooting'}
22126
<button class="btn-action btn-pause" onclick={onPause}>
23127
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>
@@ -27,8 +131,9 @@
27131
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 6h12v12H6z"/></svg>
28132
{$t('stop')}
29133
</button>
134+
30135
{:else if $timerState.phase === 'paused'}
31-
<button class="btn-action btn-start" onclick={onResume}>
136+
<button class="btn-action primary" onclick={onResume}>
32137
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
33138
{$t('resume')}
34139
</button>
@@ -39,13 +144,26 @@
39144
{/if}
40145
</div>
41146
42-
<button class="btn-reset" onclick={onReset}>
43-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
44-
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/>
45-
<path d="M3 3v5h5"/>
46-
</svg>
47-
{$t('reset')}
48-
</button>
147+
{#if $timerState.phase === 'stopped' && reason !== 'programComplete' && slot3?.name}
148+
<div class="slot3-subline-row">
149+
<div class="slot3-spacer"></div>
150+
{#if showNextSeries}<div class="slot3-spacer"></div>{/if}
151+
<div class="slot3-subline" class:slot3-subline-primary={primary === 'slot3'}>{slot3.name}</div>
152+
</div>
153+
{/if}
154+
155+
<div class="bottom-row">
156+
<button class="btn-reset" onclick={onReset}>
157+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
158+
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/>
159+
<path d="M3 3v5h5"/>
160+
</svg>
161+
{$t('reset')}
162+
</button>
163+
{#if bottomExtra}
164+
{@render bottomExtra()}
165+
{/if}
166+
</div>
49167
</div>
50168
51169
<style>
@@ -63,60 +181,103 @@
63181
64182
.btn-action {
65183
flex: 1;
184+
min-width: 0;
66185
display: flex;
67186
align-items: center;
68187
justify-content: center;
69188
gap: 0.5rem;
70189
min-height: 54px;
190+
padding: 0 0.75rem;
71191
font-size: 1rem;
72192
font-weight: 700;
73193
letter-spacing: 0.04em;
74194
border-radius: var(--radius);
195+
background: var(--bg-surface);
196+
color: var(--text-primary);
197+
border: 1px solid rgba(255,255,255,0.08);
75198
}
76199
200+
.btn-action:hover { background: #252550; }
201+
77202
.btn-action svg {
78203
width: 1.25em;
79204
height: 1.25em;
80205
flex-shrink: 0;
81206
}
82207
83-
.btn-start {
208+
/* Primary (green) — exactly one per phase */
209+
.btn-action.primary {
84210
background: var(--accent);
85211
color: #0d0d1a;
212+
border-color: transparent;
86213
}
87214
88-
.btn-start:hover { background: var(--accent-dim); }
89-
90-
.btn-pause {
91-
background: var(--bg-surface);
92-
color: var(--text-primary);
93-
border: 1px solid rgba(255,255,255,0.08);
94-
}
95-
96-
.btn-pause:hover { background: #252550; }
215+
.btn-action.primary:hover { background: var(--accent-dim); }
97216
217+
/* Stop button keeps its red treatment regardless of primary */
98218
.btn-stop {
99219
background: rgba(244, 67, 54, 0.15);
100220
color: var(--danger);
101221
border: 1px solid rgba(244, 67, 54, 0.3);
102222
}
103-
104223
.btn-stop:hover { background: rgba(244, 67, 54, 0.25); }
105224
106-
.btn-next {
107-
background: var(--bg-surface);
108-
color: var(--text-primary);
109-
border: 1px solid rgba(255,255,255,0.08);
225+
/* Subline row mirrors button row layout so the name centers under slot 3 */
226+
.slot3-subline-row {
227+
display: flex;
228+
gap: 0.6rem;
229+
margin-top: -0.25rem;
230+
}
231+
232+
.slot3-spacer {
233+
flex: 1;
234+
min-width: 0;
235+
}
236+
237+
.slot3-subline {
238+
flex: 1;
239+
min-width: 0;
240+
text-align: center;
241+
font-size: 0.72rem;
242+
font-weight: 600;
243+
color: var(--text-secondary);
244+
opacity: 0.7;
245+
white-space: nowrap;
246+
overflow: hidden;
247+
text-overflow: ellipsis;
248+
}
249+
250+
.slot3-subline-primary {
251+
color: var(--accent);
252+
opacity: 0.85;
253+
}
254+
255+
/* Program-complete sentinel */
256+
.program-complete-label {
257+
flex: 1;
258+
display: flex;
259+
align-items: center;
260+
justify-content: center;
261+
min-height: 54px;
262+
font-size: 1rem;
263+
font-weight: 700;
264+
color: var(--accent);
265+
letter-spacing: 0.04em;
266+
text-transform: uppercase;
110267
}
111268
112-
.btn-next:hover { background: #252550; }
269+
.bottom-row {
270+
display: flex;
271+
align-items: center;
272+
justify-content: center;
273+
gap: 0.75rem;
274+
}
113275
114276
.btn-reset {
115277
display: flex;
116278
align-items: center;
117279
justify-content: center;
118280
gap: 0.4rem;
119-
align-self: center;
120281
padding: 0.4rem 1.25rem;
121282
min-height: 36px;
122283
font-size: 0.8rem;
@@ -139,4 +300,9 @@
139300
color: var(--text-primary);
140301
border-color: rgba(255,255,255,0.15);
141302
}
303+
304+
/* Narrow phones: tighten buttons */
305+
@media (max-width: 420px) {
306+
.btn-action { font-size: 0.92rem; padding: 0 0.5rem; }
307+
}
142308
</style>

0 commit comments

Comments
 (0)