mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
feat(player): polish sleep-timer UI — circular ring + in-button countdown (#272)
Replaces the text-pill schedule badge with a circular SVG progress ring
around the play/pause button. Accent→lavender gradient stroke, counter-
clockwise depletion synced to the armed deadline, subtle accent glow.
While a timer is armed, the Play/Pause icon inside the button is
swapped for a two-line stack: a small Moon (sleep timer) or Sunrise
(delayed start) glyph above the countdown (m:ss / h:mm:ss). The icon
is the mode marker so the ring can stay unified with the rest of the
app's accent palette.
Redesigns the schedule modal with a mood-tinted header (Moon for
"Pause after", Sunrise for "Start after"), soft radial accent glow in
the background, slide-up open animation. Circular glass close-button
scoped to this modal, with hover rotation + focus ring. Custom-minutes
field gains an inline "min" suffix. A live preview line at the bottom
shows when the action fires ("Pauses at 23:47" / "Starts at 23:47"),
updating as the user hovers a preset or types. Chips gain a small
hover lift plus accent-coloured shadow.
Also fixes a pre-existing duplicate-tooltip on the play/pause button:
title= attributes removed alongside the data-tooltip (title violates
the project's "never native tooltips" rule, so both showed at once).
Adds scheduledPauseStartMs / scheduledResumeStartMs to the player
store so the progress ring has a total-duration baseline, set and
cleared alongside the existing deadline fields.
New usePlaybackScheduleRemaining hook wraps the store subscription
and 500 ms tick into a single hook used by all three player views
(PlayerBar / FullscreenPlayer / MobilePlayerView).
i18n: delayPreviewPause / delayPreviewStart keys across all 8 locales.
Co-authored-by: Psychotoxical <dev@psysonic.app>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
624ce56faf
commit
694567843f
+210
-38
@@ -2052,14 +2052,102 @@
|
||||
.playback-delay-modal {
|
||||
max-height: min(85vh, 560px);
|
||||
padding: var(--space-6);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
animation: playback-delay-modal-in 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
}
|
||||
|
||||
@keyframes playback-delay-modal-in {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* Mood-tint: subtle gradient overlay behind content, differs per mode. */
|
||||
.playback-delay-modal::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
opacity: 0.6;
|
||||
background: radial-gradient(
|
||||
ellipse at top right,
|
||||
color-mix(in srgb, var(--pd-accent, var(--accent)) 14%, transparent) 0%,
|
||||
transparent 55%
|
||||
);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* Raise content above the ::before mood tint, but do NOT touch .modal-close
|
||||
— it must keep its absolute top-right positioning. */
|
||||
.playback-delay-modal > *:not(.modal-close) { position: relative; z-index: 1; }
|
||||
|
||||
/* Scoped polish for the close button in this modal: circular glass chip with
|
||||
subtle hover lift + rotation. Does not touch the global .modal-close, so
|
||||
other modals keep their existing look. */
|
||||
.playback-delay-modal > .modal-close {
|
||||
z-index: 2;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: color-mix(in srgb, var(--text-primary) 5%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--text-primary) 10%, transparent);
|
||||
color: var(--text-muted);
|
||||
transition:
|
||||
background 180ms ease,
|
||||
border-color 180ms ease,
|
||||
color 180ms ease,
|
||||
transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
}
|
||||
|
||||
.playback-delay-modal > .modal-close:hover {
|
||||
background: color-mix(in srgb, var(--pd-accent, var(--accent)) 16%, transparent);
|
||||
border-color: color-mix(in srgb, var(--pd-accent, var(--accent)) 40%, transparent);
|
||||
color: var(--pd-accent, var(--accent));
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.playback-delay-modal > .modal-close:focus-visible {
|
||||
outline: 2px solid color-mix(in srgb, var(--pd-accent, var(--accent)) 60%, transparent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.playback-delay-modal > .modal-close:active {
|
||||
transform: rotate(90deg) scale(0.94);
|
||||
}
|
||||
|
||||
.playback-delay-modal--pause { --pd-accent: var(--ctp-lavender, #b4befe); }
|
||||
.playback-delay-modal--start { --pd-accent: var(--ctp-peach, #fab387); }
|
||||
.playback-delay-modal--idle { --pd-accent: var(--ctp-overlay1, #7f849c); }
|
||||
|
||||
.playback-delay-modal__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 0 0 var(--space-4);
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
.playback-delay-modal__icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--pd-accent, var(--accent)) 18%, transparent);
|
||||
color: var(--pd-accent, var(--accent));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.playback-delay-modal__title {
|
||||
margin: 0 0 var(--space-4);
|
||||
padding-right: 2rem;
|
||||
margin: 0;
|
||||
font-family: var(--font-display);
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.playback-delay-modal-overlay--anchored {
|
||||
@@ -2074,39 +2162,112 @@
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.playback-schedule-badge--floating {
|
||||
/* ── Custom-minutes input with inline "min" suffix ──────────────────── */
|
||||
.playback-delay-custom__field {
|
||||
position: relative;
|
||||
flex: 1 1 100px;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transform-origin: top right;
|
||||
min-width: 1.55rem;
|
||||
height: 1.45rem;
|
||||
padding: 0 5px;
|
||||
border-radius: 999px;
|
||||
font-size: 0.6rem;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--text-primary);
|
||||
background: color-mix(in srgb, var(--ctp-surface0) 88%, var(--text-primary) 4%);
|
||||
border: 1px solid var(--border-subtle, var(--border));
|
||||
box-shadow: 0 1px 2px color-mix(in srgb, var(--text-primary) 8%, transparent);
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.playback-schedule-badge--floating.playback-schedule-badge--fs {
|
||||
font-size: 0.64rem;
|
||||
min-width: 1.65rem;
|
||||
height: 1.5rem;
|
||||
padding: 0 6px;
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border: 1px solid rgba(255, 255, 255, 0.22);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
|
||||
.playback-delay-custom__field .playback-delay-custom__input {
|
||||
flex: 1 1 auto;
|
||||
padding-right: 2.4rem;
|
||||
}
|
||||
|
||||
.playback-delay-custom__suffix {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-muted);
|
||||
pointer-events: none;
|
||||
opacity: 0.7;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
/* ── Live preview: "Pauses at 23:47" ────────────────────────────────── */
|
||||
.playback-delay-preview {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
margin-top: var(--space-3);
|
||||
min-height: 1.1rem;
|
||||
font-size: 0.82rem;
|
||||
color: var(--text-muted);
|
||||
transition: opacity 150ms ease;
|
||||
}
|
||||
|
||||
.playback-delay-preview[data-empty="true"] { opacity: 0; }
|
||||
.playback-delay-preview[data-empty="false"] { opacity: 1; }
|
||||
|
||||
.playback-delay-preview__label { letter-spacing: 0.01em; }
|
||||
|
||||
.playback-delay-preview__time {
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--pd-accent, var(--accent));
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
/* ── Circular progress ring around play/pause ────────────────────────── */
|
||||
.playback-schedule-ring {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.playback-schedule-ring__svg {
|
||||
display: block;
|
||||
overflow: visible;
|
||||
filter: drop-shadow(0 0 6px color-mix(in srgb, var(--accent) 42%, transparent));
|
||||
}
|
||||
|
||||
.playback-schedule-ring__track {
|
||||
stroke: color-mix(in srgb, var(--text-primary) 10%, transparent);
|
||||
}
|
||||
|
||||
.playback-schedule-ring__fill {
|
||||
transition: stroke-dashoffset 480ms linear;
|
||||
}
|
||||
|
||||
/* Unified accent gradient for both modes — the Moon / Sunrise icon in the
|
||||
button carries the mode distinction, so the ring stays consistent with
|
||||
the rest of the app. */
|
||||
.playback-schedule-ring__grad-a { stop-color: var(--accent); }
|
||||
.playback-schedule-ring__grad-b { stop-color: var(--ctp-lavender, #b4befe); }
|
||||
|
||||
/* Replaces the Play/Pause icon while a schedule timer is armed. Two-line
|
||||
stack: mode icon (Moon for sleep, Sunrise for delayed-start) above the
|
||||
countdown text. The mode icon + ring colour jointly signal which timer
|
||||
is running. Three size variants mirror the three player views. */
|
||||
.player-btn-schedule-stack {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.player-btn-schedule-time {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1;
|
||||
min-width: 2.2em;
|
||||
text-align: center;
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.player-btn-schedule-stack--fs { gap: 3px; }
|
||||
.player-btn-schedule-time--fs { font-size: 0.92rem; }
|
||||
.player-btn-schedule-stack--mobile { gap: 3px; }
|
||||
.player-btn-schedule-time--mobile { font-size: 1rem; }
|
||||
|
||||
.playback-delay-section__head--tight {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -2197,23 +2358,34 @@
|
||||
.playback-delay-chip {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 6px 10px;
|
||||
font-size: 0.8rem;
|
||||
padding: 7px 12px;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 500;
|
||||
background: var(--ctp-surface1);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
|
||||
transition:
|
||||
background 150ms ease,
|
||||
border-color 150ms ease,
|
||||
color 150ms ease,
|
||||
transform 120ms ease,
|
||||
box-shadow 150ms ease;
|
||||
}
|
||||
|
||||
.playback-delay-chip:hover {
|
||||
border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
|
||||
background: color-mix(in srgb, var(--accent) 10%, var(--ctp-surface1));
|
||||
border-color: color-mix(in srgb, var(--pd-accent, var(--accent)) 55%, var(--border));
|
||||
background: color-mix(in srgb, var(--pd-accent, var(--accent)) 14%, var(--ctp-surface1));
|
||||
color: var(--pd-accent, var(--accent));
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 3px 10px color-mix(in srgb, var(--pd-accent, var(--accent)) 22%, transparent);
|
||||
}
|
||||
|
||||
.playback-delay-chip:active { transform: translateY(0); }
|
||||
|
||||
.playback-delay-chip--on {
|
||||
border-color: var(--accent);
|
||||
background: color-mix(in srgb, var(--accent) 18%, var(--ctp-surface1));
|
||||
color: var(--accent);
|
||||
border-color: var(--pd-accent, var(--accent));
|
||||
background: color-mix(in srgb, var(--pd-accent, var(--accent)) 22%, var(--ctp-surface1));
|
||||
color: var(--pd-accent, var(--accent));
|
||||
}
|
||||
|
||||
.playback-delay-actions {
|
||||
|
||||
Reference in New Issue
Block a user