Improve timer interval refresh behavior
All checks were successful
Deploy Sokko G / deploy (push) Successful in 7s

This commit is contained in:
Shinuwa 2026-08-03 15:57:25 +02:00
parent fdc81373c6
commit a6b2388763
9 changed files with 305 additions and 39 deletions

View file

@ -171,6 +171,8 @@ Regles UI :
- types visibles : `Duree`, `Heure precise`, `Pattern horaire`, `Intervalle` ;
- les libelles au-dessus des champs `hh:mm:ss` restent accessibles en `sr-only`, mais ne sont pas visibles ;
- les boutons de controle utilisent les icones `play-circle`, `pause-circle`, `record-circle`, `stop-circle`, `refresh`, `sort-time`, `scrollable`.
- pour `Heure precise` et `Intervalle`, le refresh inline est un toggle compact de repetition automatique ; il est bloque si la frequence est inferieure a 5 minutes.
- le formulaire `Intervalle` affiche deux champs `hh:mm:ss` compacts : duree de repetition et depart optionnel ; un depart vide signifie `Maintenant`.
Alertes timer :

View file

@ -258,6 +258,7 @@ Structure initiale :
"label": "Boss",
"type": "daily_time",
"alertMode": "visible",
"autoRefresh": true,
"time": "12:03:00",
"targetAt": 1760000000000
},
@ -271,8 +272,12 @@ Structure initiale :
"id": "timer4",
"label": "Event",
"type": "interval",
"autoRefresh": true,
"intervalMs": 1800000,
"anchorAt": 1760000000000
"startMode": "time",
"startTime": "06:00:00",
"anchorAt": 1760000000000,
"targetAt": 1760001800000
}
]
}
@ -281,9 +286,9 @@ Structure initiale :
Types de compte à rebours :
- `duration` : durée ponctuelle, avec `durationMs` et `targetAt`.
- `daily_time` : occurrence ponctuelle à une heure précise, avec `time` au format `HH:MM:SS` et `targetAt`.
- `daily_time` : occurrence à une heure précise, avec `time` au format `HH:MM:SS`, `targetAt` et `autoRefresh` optionnel.
- `time_pattern` : prochaine occurrence automatique correspondant à un pattern `HH:MM:SS`, où chaque segment peut valoir `X`.
- `interval` : prochaine occurrence automatique toutes les X millisecondes, avec `intervalMs` et `anchorAt`.
- `interval` : intervalle basé sur `intervalMs`, `anchorAt`, `targetAt`, `startMode`, `startTime` et `autoRefresh` optionnel. `startMode` vaut `now` ou `time`; `startTime` est présent uniquement avec `time`. `autoRefresh` n'est conservé que si l'intervalle dure au moins 5 minutes ; les anciens intervalles sans `targetAt` restent interprétés comme répétables.
Pour `time_pattern`, un segment `X` situé après un segment plus large fixé cible le premier instant de l'occurrence. Exemple : `X:24:X` cible `hh:24:00` et se répète toutes les heures.
@ -298,6 +303,8 @@ Stockage compact :
- `laps` est omis tant qu'aucune étape n'est enregistrée.
- `countdowns` est omis tant qu'aucun compte à rebours n'est configuré.
- `countdowns[].alertMode` vaut `off`, `visible` ou `site`, et est omis si `off`.
- `countdowns[].autoRefresh` est omis si `false`.
- `countdowns[].startMode` et `countdowns[].startTime` sont omis pour les intervalles qui démarrent à la création.
- L'activation d'une alerte est bloquée dans l'interface si la fréquence configurée est inférieure à 5 minutes pour les comptes à rebours récurrents (`time_pattern` et `interval`). Le blocage ne dépend pas du temps restant avant la prochaine échéance.
## Outil Task Planner

View file

@ -261,6 +261,9 @@ export function validateSiteContent(site) {
"toolboxes.modules.timer.dailyTimeLabel",
"toolboxes.modules.timer.timePatternLabel",
"toolboxes.modules.timer.intervalLabel",
"toolboxes.modules.timer.intervalStartNowOption",
"toolboxes.modules.timer.intervalStartTimeOption",
"toolboxes.modules.timer.intervalStartTimeLabel",
"toolboxes.modules.timer.addCountdownButton",
"toolboxes.modules.timer.defaultCountdownLabel",
"toolboxes.modules.timer.finishedLabel",
@ -270,6 +273,7 @@ export function validateSiteContent(site) {
"toolboxes.modules.timer.countdownsTitle",
"toolboxes.modules.timer.scrollableTitle",
"toolboxes.modules.timer.sortTitle",
"toolboxes.modules.timer.autoRefreshTitle",
"toolboxes.modules.timer.alertModeTitle",
"toolboxes.modules.timer.alertOffTitle",
"toolboxes.modules.timer.alertVisibleTitle",

View file

@ -1295,6 +1295,7 @@
"id": "z4",
"label": "Boss Mondial",
"type": "daily_time",
"autoRefresh": true,
"time": "14:00:00",
"targetAt": 1785758400000
},

View file

@ -426,15 +426,19 @@
"dailyTimeLabel": "Heure",
"timePatternLabel": "Heure / minute / seconde",
"intervalLabel": "Toutes les",
"intervalStartNowOption": "Maintenant",
"intervalStartTimeOption": "Heure précise",
"intervalStartTimeLabel": "Départ",
"addCountdownButton": "Ajouter",
"defaultCountdownLabel": "Timer",
"finishedLabel": "Terminé",
"targetLabel": "Prochaine occurrence",
"targetLabel": "Suivant",
"stepLabel": "Étape",
"stepsTitle": "Étapes",
"countdownsTitle": "Temps restants",
"scrollableTitle": "Liste scrollable",
"sortTitle": "Trier par temps restant",
"autoRefreshTitle": "Répéter automatiquement",
"alertModeTitle": "Mode d'alerte",
"alertOffTitle": "Pas d'alerte",
"alertVisibleTitle": "Alerte quand la toolbox est affichée",

View file

@ -12,6 +12,7 @@ import {
getCentiseconds,
getCountdownTargetMs,
getDailyTargetMs,
getIntervalAnchorMs,
getNowMs,
getTimePatternTargetMs,
getTimePatternRecurrenceMs,
@ -32,7 +33,8 @@ const DEFAULT_COUNTDOWN_FORM = {
duration: EMPTY_TIME_PARTS,
time: EMPTY_TIME_PARTS,
pattern: EMPTY_TIME_PARTS,
interval: EMPTY_TIME_PARTS
interval: EMPTY_TIME_PARTS,
intervalStartTime: EMPTY_TIME_PARTS
};
const ALERT_MODES = [
{ mode: "off", icon: "sound-mute" },
@ -41,6 +43,7 @@ const ALERT_MODES = [
];
const ALERT_MIN_DELAY_MS = 5 * 60 * 1000;
const AUTO_REFRESH_COUNTDOWN_TYPES = new Set(["time_pattern", "interval"]);
const TOGGLE_REFRESH_COUNTDOWN_TYPES = new Set(["daily_time", "interval"]);
function isAlertGuarded(countdown) {
if (countdown?.type === "interval") return Number(countdown.intervalMs) < ALERT_MIN_DELAY_MS;
@ -52,6 +55,10 @@ function getAlertGuardMessage(textContent) {
return textContent.alertTooSoonTitle || "Alerte indisponible pour une répétition inférieure à 5 minutes";
}
function canToggleAutoRefresh(countdown) {
return TOGGLE_REFRESH_COUNTDOWN_TYPES.has(countdown?.type) && !isAlertGuarded(countdown);
}
function TimerValue({ ms, compact = false, showCentiseconds = true }) {
if (!showCentiseconds) return <span className="timer-value">{formatDuration(ms)}</span>;
if (!compact) return <span className="timer-value">{formatDurationWithCentiseconds(ms)}</span>;
@ -64,12 +71,17 @@ function TimerValue({ ms, compact = false, showCentiseconds = true }) {
);
}
function getCountdownMeta(countdown, textContent) {
if (countdown.type === "duration") return `${textContent.durationType || "Durée"} · ${formatDuration(countdown.durationMs || 0)}`;
if (countdown.type === "daily_time") return `${textContent.dailyTimeType || "Heure précise"} · ${countdown.time}`;
if (countdown.type === "time_pattern") return `${textContent.timePatternType || "Heure / minute / seconde"} · ${countdown.pattern}`;
if (countdown.type === "interval") return `${textContent.intervalType || "Intervalle"} · ${formatDuration(countdown.intervalMs || 0)}`;
return "";
function getCountdownMetaParts(countdown, textContent) {
if (countdown.type === "duration") return [`${textContent.durationType || "Durée"} ${formatDuration(countdown.durationMs || 0)}`];
if (countdown.type === "daily_time") return [`${textContent.dailyTimeType || "Heure précise"} ${countdown.time}`];
if (countdown.type === "time_pattern") return [`${textContent.timePatternType || "Heure / minute / seconde"} ${countdown.pattern}`];
if (countdown.type === "interval") {
const startLabel = countdown.startMode === "time" && countdown.startTime
? `${textContent.intervalStartTimeLabel || "Départ"} ${countdown.startTime}`
: textContent.intervalStartNowOption || "Maintenant";
return [`${textContent.intervalType || "Intervalle"} ${formatDuration(countdown.intervalMs || 0)}`, startLabel];
}
return [];
}
function handleEditableKeyDown(event, callback) {
@ -78,6 +90,10 @@ function handleEditableKeyDown(event, callback) {
callback();
}
function hasTimePartValue(parts) {
return Boolean(String(parts?.hours || "").trim() || String(parts?.minutes || "").trim() || String(parts?.seconds || "").trim());
}
export function TimerModule({ toolboxId, moduleId, context }) {
const data = context.normalizeTimerData(context.getModuleData(toolboxId, moduleId, { activeTab: "stopwatch" }));
const textContent = context.moduleText?.timer || {};
@ -197,7 +213,23 @@ export function TimerModule({ toolboxId, moduleId, context }) {
}
if (type === "interval") {
const intervalMs = timePartsToDurationMs(draft.interval);
if (intervalMs > 0) countdown = { id: context.uid("timer"), label, type, intervalMs, anchorAt: now };
const startTime = hasTimePartValue(draft.intervalStartTime) ? timePartsToString(draft.intervalStartTime) : "";
const startMode = startTime ? "time" : "now";
const anchorAt = getIntervalAnchorMs(startMode, startTime, now);
const targetAt = getCountdownTargetMs({ type, intervalMs, anchorAt, autoRefresh: true }, now);
if (intervalMs > 0) {
countdown = {
id: context.uid("timer"),
label,
type,
intervalMs,
anchorAt,
targetAt,
startMode,
...(startTime ? { startTime } : {}),
autoRefresh: intervalMs >= ALERT_MIN_DELAY_MS
};
}
}
if (!countdown) return;
@ -216,11 +248,39 @@ export function TimerModule({ toolboxId, moduleId, context }) {
let nextCountdown = null;
if (countdown.type === "duration") nextCountdown = { ...countdown, targetAt: now + countdown.durationMs };
if (countdown.type === "daily_time") nextCountdown = { ...countdown, targetAt: getDailyTargetMs(countdown.time, now) };
if (countdown.type === "interval") nextCountdown = { ...countdown, anchorAt: now };
if (countdown.type === "interval") {
const anchorAt = getIntervalAnchorMs(countdown.startMode, countdown.startTime, now);
const targetAt = getCountdownTargetMs({ ...countdown, anchorAt, autoRefresh: true }, now);
nextCountdown = { ...countdown, anchorAt, targetAt };
}
if (!nextCountdown) return;
save({ ...data, countdowns: data.countdowns.map((item) => item.id === countdown.id ? nextCountdown : item) });
}
function toggleCountdownAutoRefresh(countdownId) {
const countdown = data.countdowns.find((item) => item.id === countdownId);
if (!TOGGLE_REFRESH_COUNTDOWN_TYPES.has(countdown?.type)) return;
if (!countdown.autoRefresh && !canToggleAutoRefresh(countdown)) {
context.notify?.(getAlertGuardMessage(textContent));
return;
}
const now = getNowMs();
save({
...data,
countdowns: data.countdowns.map((item) => {
if (item.id !== countdownId) return item;
const autoRefresh = !item.autoRefresh;
if (item.type === "daily_time") return { ...item, autoRefresh, targetAt: getDailyTargetMs(item.time, now) };
if (item.type === "interval") {
const anchorAt = getIntervalAnchorMs(item.startMode, item.startTime, now);
const targetAt = getCountdownTargetMs({ ...item, anchorAt, autoRefresh: true }, now);
return { ...item, autoRefresh, anchorAt, targetAt };
}
return item;
})
});
}
function renameCountdown(countdownId, label) {
save({
...data,
@ -314,7 +374,7 @@ export function TimerModule({ toolboxId, moduleId, context }) {
{activeTab === "stopwatch" ? (
<StopwatchLapList laps={data.stopwatch.laps} textContent={textContent} onRename={renameLap} onDelete={deleteLap} />
) : (
<CountdownList countdowns={visibleCountdowns} nowMs={nowMs} textContent={textContent} reorder={countdownReorder} reorderEnabled={!data.sortResults} parentId={moduleId} onRename={renameCountdown} onAlertModeChange={setCountdownAlertMode} onReset={resetCountdown} onDelete={deleteCountdown} />
<CountdownList countdowns={visibleCountdowns} nowMs={nowMs} textContent={textContent} reorder={countdownReorder} reorderEnabled={!data.sortResults} parentId={moduleId} onRename={renameCountdown} onAlertModeChange={setCountdownAlertMode} onReset={resetCountdown} onAutoRefreshToggle={toggleCountdownAutoRefresh} onDelete={deleteCountdown} />
)}
</div>
</section>
@ -399,27 +459,36 @@ function CountdownControls({ textContent, form, onChange, onSubmit }) {
</label>
{form.type === "duration" && (
<label>
<span className="timer-field-spacer" aria-hidden="true" />
<span className="sr-only">{textContent.durationLabel || "Durée"}</span>
<TimePartsInput value={form.duration} onChange={(duration) => onChange({ duration })} />
</label>
)}
{form.type === "daily_time" && (
<label>
<span className="timer-field-spacer" aria-hidden="true" />
<span className="sr-only">{textContent.dailyTimeLabel || "Heure"}</span>
<TimePartsInput value={form.time} onChange={(time) => onChange({ time })} />
</label>
)}
{form.type === "time_pattern" && (
<label>
<span className="timer-field-spacer" aria-hidden="true" />
<span className="sr-only">{textContent.timePatternLabel || "Heure / minute / seconde"}</span>
<TimePartsInput value={form.pattern} onChange={(pattern) => onChange({ pattern })} allowWildcard />
</label>
)}
{form.type === "interval" && (
<div className="timer-interval-fields">
<label>
<span className="sr-only">{textContent.intervalLabel || "Toutes les"}</span>
<span>{textContent.intervalLabel || "Toutes les"}</span>
<TimePartsInput value={form.interval} onChange={(interval) => onChange({ interval })} />
</label>
<label>
<span>{textContent.intervalStartTimeLabel || "Départ"}</span>
<TimePartsInput value={form.intervalStartTime} onChange={(intervalStartTime) => onChange({ intervalStartTime })} />
</label>
</div>
)}
<button className="primary" type="submit">{textContent.addCountdownButton || "Ajouter"}</button>
</form>
@ -485,7 +554,7 @@ function StopwatchLapList({ laps, textContent, onRename, onDelete }) {
);
}
function CountdownList({ countdowns, nowMs, textContent, reorder, reorderEnabled, parentId, onRename, onAlertModeChange, onReset, onDelete }) {
function CountdownList({ countdowns, nowMs, textContent, reorder, reorderEnabled, parentId, onRename, onAlertModeChange, onReset, onAutoRefreshToggle, onDelete }) {
const [editingId, setEditingId] = useState("");
if (!countdowns.length) return <p className="muted">{textContent.emptyCountdowns || "Aucun compte à rebours configuré."}</p>;
return (
@ -493,9 +562,12 @@ function CountdownList({ countdowns, nowMs, textContent, reorder, reorderEnabled
{countdowns.map((countdown) => {
const remainingMs = countdown.targetMs - nowMs;
const expired = remainingMs <= 0;
const canReset = countdown.type === "duration"
|| countdown.type === "interval"
|| (countdown.type === "daily_time" && expired);
const canReset = countdown.type === "duration";
const canToggleRefresh = TOGGLE_REFRESH_COUNTDOWN_TYPES.has(countdown.type);
const refreshBlocked = canToggleRefresh && !canToggleAutoRefresh(countdown);
const refreshTitle = refreshBlocked
? getAlertGuardMessage(textContent)
: textContent.autoRefreshTitle || "Répéter automatiquement";
const className = [
"timer-list-item",
expired ? "is-expired" : "",
@ -508,7 +580,7 @@ function CountdownList({ countdowns, nowMs, textContent, reorder, reorderEnabled
return (
<li key={countdown.id} className={className} {...reorderProps}>
<div className={`tool-split-entry timer-entry has-single-action ${reorderEnabled ? "has-drag-handle" : ""} ${editingId !== countdown.id ? "has-inline-controls" : ""} ${canReset && editingId !== countdown.id ? "has-inline-reset" : ""}`}>
<div className={`tool-split-entry timer-entry has-single-action ${reorderEnabled ? "has-drag-handle" : ""} ${editingId !== countdown.id ? "has-inline-controls" : ""} ${(canReset || canToggleRefresh) && editingId !== countdown.id ? "has-inline-reset" : ""}`}>
{reorderEnabled && (
<button
className="timer-drag-handle"
@ -556,6 +628,23 @@ function CountdownList({ countdowns, nowMs, textContent, reorder, reorderEnabled
<Icon name="refresh" />
</button>
)}
{canToggleRefresh && (
<button
className={`timer-inline-refresh-toggle ${countdown.autoRefresh ? "active" : ""} ${refreshBlocked ? "is-blocked" : ""}`}
type="button"
onClick={(event) => {
event.stopPropagation();
onAutoRefreshToggle(countdown.id);
}}
disabled={refreshBlocked}
aria-pressed={Boolean(countdown.autoRefresh)}
aria-label={refreshTitle}
title={refreshTitle}
>
<Icon name="refresh" />
<i aria-hidden="true" />
</button>
)}
<TimerAlertModeSwitch
mode={countdown.alertMode}
blocked={AUTO_REFRESH_COUNTDOWN_TYPES.has(countdown.type) && isAlertGuarded(countdown)}
@ -571,8 +660,14 @@ function CountdownList({ countdowns, nowMs, textContent, reorder, reorderEnabled
</button>
</div>
<p className="timer-entry-meta">
<span>{getCountdownMeta(countdown, textContent)}</span>
<em>{expired ? textContent.finishedLabel || "Terminé" : `${textContent.targetLabel || "Prochaine occurrence"} · ${formatTargetTime(countdown.targetMs)}`}</em>
<span>
{getCountdownMetaParts(countdown, textContent).map((part, index) => (
<b key={`${part}-${index}`}>{part}</b>
))}
</span>
<em>
<b>{expired ? textContent.finishedLabel || "Terminé" : `${textContent.targetLabel || "Prochaine occurrence"} ${formatTargetTime(countdown.targetMs)}`}</b>
</em>
</p>
</li>
);

View file

@ -113,10 +113,22 @@ export function getIntervalTargetMs(intervalMs, anchorAt, nowMs) {
return safeAnchor + Math.ceil((nowMs - safeAnchor + 1) / safeIntervalMs) * safeIntervalMs;
}
export function getIntervalAnchorMs(startMode, startTime, nowMs) {
if (startMode !== "time") return nowMs;
const parsed = parseTimeString(startTime);
if (!parsed) return nowMs;
const anchor = new Date(nowMs);
anchor.setHours(Number(parsed.hours), Number(parsed.minutes), Number(parsed.seconds), 0);
return anchor.getTime();
}
export function getCountdownTargetMs(countdown, nowMs) {
if (countdown.type === "duration") return Number(countdown.targetAt) || 0;
if (countdown.type === "daily_time") return Number(countdown.targetAt) || 0;
if (countdown.type === "daily_time") return countdown.autoRefresh ? getDailyTargetMs(countdown.time, nowMs) : Number(countdown.targetAt) || 0;
if (countdown.type === "time_pattern") return getTimePatternTargetMs(countdown.pattern, nowMs);
if (countdown.type === "interval") return getIntervalTargetMs(countdown.intervalMs, countdown.anchorAt, nowMs);
if (countdown.type === "interval") {
if (countdown.autoRefresh) return getIntervalTargetMs(countdown.intervalMs, countdown.anchorAt, nowMs);
return Number(countdown.targetAt) || 0;
}
return 0;
}

View file

@ -16,6 +16,8 @@ const NOTEPAD_RGB_COLOR_MAP = {
const TIMER_TABS = new Set(["stopwatch", "countdown"]);
const COUNTDOWN_TYPES = new Set(["duration", "daily_time", "time_pattern", "interval"]);
const TIMER_ALERT_MODES = new Set(["off", "visible", "site"]);
const TIMER_INTERVAL_START_MODES = new Set(["now", "time"]);
const TIMER_MIN_AUTO_REFRESH_MS = 5 * 60 * 1000;
const TASK_TYPES = new Set(["unique", "daily", "weekly"]);
const COMBO_DEVICES = new Set(["playstation", "xbox", "switch", "n64", "keyboardMouse"]);
const COMBO_INPUT_KINDS = new Set(["button", "direction", "key", "mouse"]);
@ -553,7 +555,7 @@ export function normalizeTimerData(data) {
if (type === "daily_time") {
const time = String(countdown?.time || "").trim();
const targetAt = Math.max(0, Number(countdown?.targetAt) || 0);
return isTimeString(time) && targetAt > 0 ? { ...normalized, time, targetAt } : null;
return isTimeString(time) && targetAt > 0 ? { ...normalized, time, targetAt, autoRefresh: countdown?.autoRefresh === true } : null;
}
if (type === "time_pattern") {
const pattern = String(countdown?.pattern || "").trim().toUpperCase();
@ -562,7 +564,21 @@ export function normalizeTimerData(data) {
if (type === "interval") {
const intervalMs = Math.max(0, Number(countdown?.intervalMs) || 0);
const anchorAt = Math.max(0, Number(countdown?.anchorAt) || 0);
return intervalMs > 0 ? { ...normalized, intervalMs, anchorAt } : null;
const targetAt = Math.max(0, Number(countdown?.targetAt) || 0);
const startMode = TIMER_INTERVAL_START_MODES.has(countdown?.startMode) ? countdown.startMode : "now";
const startTime = String(countdown?.startTime || "").trim();
const autoRefresh = countdown?.autoRefresh === true || (countdown?.autoRefresh !== false && !targetAt && intervalMs >= TIMER_MIN_AUTO_REFRESH_MS);
return intervalMs > 0
? {
...normalized,
intervalMs,
anchorAt,
targetAt: targetAt || anchorAt + intervalMs,
startMode: startMode === "time" && isTimeString(startTime) ? "time" : "now",
...(startMode === "time" && isTimeString(startTime) ? { startTime } : {}),
autoRefresh: autoRefresh && intervalMs >= TIMER_MIN_AUTO_REFRESH_MS
}
: null;
}
return null;
})
@ -822,11 +838,14 @@ export function compactModuleDataForStorage(type, value) {
}
if (normalized.countdowns.length) {
compact.countdowns = normalized.countdowns.map((countdown) => {
if (countdown.alertMode === "off") {
const { alertMode, ...compactCountdown } = countdown;
return compactCountdown;
const { alertMode, autoRefresh, startMode, startTime, ...compactCountdown } = countdown;
if (alertMode !== "off") compactCountdown.alertMode = alertMode;
if (autoRefresh) compactCountdown.autoRefresh = true;
if (startMode === "time" && startTime) {
compactCountdown.startMode = startMode;
compactCountdown.startTime = startTime;
}
return countdown;
return compactCountdown;
});
}
return Object.keys(compact).length ? compact : null;

View file

@ -3453,6 +3453,11 @@ button.combo-input-token.combo-value-grab .ui-icon {
gap: 7px;
}
.timer-field-spacer {
display: block;
min-height: 17px;
}
.timer-countdown-form input,
.timer-countdown-form select,
.timer-control-panel input {
@ -3578,6 +3583,32 @@ button.combo-input-token.combo-value-grab .ui-icon {
text-align: center;
}
.timer-interval-fields {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--space-2);
min-width: 0;
}
.timer-interval-fields label {
min-width: 0;
gap: 5px;
}
.timer-interval-fields .time-parts-input {
grid-template-columns: minmax(22px, 1fr) 4px minmax(22px, 1fr) 4px minmax(22px, 1fr);
min-height: 38px;
}
.timer-interval-fields .time-parts-input input {
min-height: 36px;
font-size: var(--font-size-xs);
}
.timer-interval-fields .time-parts-input span {
font-size: var(--font-size-xs);
}
.timer-control-panel > span,
.timer-control-panel label span {
color: var(--color-text-secondary);
@ -3658,12 +3689,13 @@ button.combo-input-token.combo-value-grab .ui-icon {
.timer-entry-meta {
display: flex;
grid-column: 1 / -2;
flex-wrap: wrap;
flex-wrap: nowrap;
min-width: 0;
gap: 4px 8px;
margin: 0;
padding: 1px 10px 0;
opacity: 0.68;
overflow: hidden;
}
.timer-entry-summary .timer-value,
@ -3735,7 +3767,49 @@ button.combo-input-token.combo-value-grab .ui-icon {
color: var(--color-text-muted);
}
.timer-inline-reset .ui-icon {
.timer-inline-refresh-toggle {
display: inline-grid;
grid-template-columns: 13px 18px;
width: 42px;
min-width: 42px;
min-height: 26px;
align-items: center;
gap: 3px;
padding: 0 4px;
border-color: rgba(165, 180, 252, 0.08);
border-radius: var(--radius-sm);
background: rgba(5, 7, 17, 0.26);
color: var(--color-text-muted);
}
.timer-inline-refresh-toggle i {
position: relative;
display: block;
width: 18px;
height: 12px;
border: 1px solid rgba(165, 180, 252, 0.18);
border-radius: var(--radius-pill);
background: rgba(7, 10, 24, 0.72);
}
.timer-inline-refresh-toggle i::before {
content: "";
position: absolute;
top: 50%;
left: 2px;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--color-text-muted);
transform: translateY(-50%);
transition:
left var(--duration-fast) var(--ease-standard),
background-color var(--duration-fast) var(--ease-standard),
box-shadow var(--duration-fast) var(--ease-standard);
}
.timer-inline-reset .ui-icon,
.timer-inline-refresh-toggle .ui-icon {
width: 14px;
height: 14px;
}
@ -3766,16 +3840,47 @@ button.combo-input-token.combo-value-grab .ui-icon {
}
.timer-entry-meta span {
display: inline-flex;
align-items: center;
gap: 7px;
min-width: 0;
overflow: hidden;
color: var(--color-text-muted);
font-size: var(--font-size-xs);
font-weight: 700;
text-overflow: ellipsis;
white-space: nowrap;
}
.timer-list-item em {
display: inline-flex;
align-items: center;
gap: 7px;
flex: 0 0 auto;
color: var(--color-text-muted);
font-size: var(--font-size-xs);
font-style: normal;
font-weight: 700;
white-space: nowrap;
}
.timer-entry-meta b {
min-width: 0;
overflow: hidden;
font: inherit;
text-overflow: ellipsis;
white-space: nowrap;
}
.timer-entry-meta b + b::before,
.timer-entry-meta em::before {
content: "";
display: inline-block;
width: 1px;
height: 10px;
margin-right: 7px;
background: rgba(165, 180, 252, 0.22);
vertical-align: -1px;
}
.timer-results-card {
@ -4185,14 +4290,16 @@ button.combo-input-token.combo-value-grab .ui-icon {
}
.timer-alert-mode-button:hover,
.timer-alert-mode-button.active {
.timer-alert-mode-button.active,
.timer-inline-refresh-toggle:hover:not(:disabled) {
border-color: rgba(196, 181, 253, 0.2);
background: rgba(21, 26, 48, 0.42);
color: var(--color-text-primary);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
}
.timer-alert-mode-button.active {
.timer-alert-mode-button.active,
.timer-inline-refresh-toggle.active {
border-color: rgba(246, 196, 83, 0.26);
background:
linear-gradient(135deg, rgba(246, 196, 83, 0.08), rgba(139, 92, 246, 0.08)),
@ -4200,9 +4307,23 @@ button.combo-input-token.combo-value-grab .ui-icon {
color: var(--color-accent-gold);
}
.timer-inline-refresh-toggle.active i {
border-color: rgba(246, 196, 83, 0.36);
background: rgba(246, 196, 83, 0.12);
}
.timer-inline-refresh-toggle.active i::before {
left: 7px;
background: var(--color-accent-gold);
box-shadow: 0 0 10px rgba(246, 196, 83, 0.28);
}
.timer-alert-mode-button:disabled,
.timer-alert-mode-button:disabled:hover,
.timer-alert-mode-button.active:disabled {
.timer-alert-mode-button.active:disabled,
.timer-inline-refresh-toggle:disabled,
.timer-inline-refresh-toggle:disabled:hover,
.timer-inline-refresh-toggle.active:disabled {
cursor: not-allowed;
border-color: rgba(165, 180, 252, 0.025);
background: rgba(5, 7, 17, 0.08);
@ -4211,7 +4332,8 @@ button.combo-input-token.combo-value-grab .ui-icon {
box-shadow: none;
}
.timer-alert-mode-button:disabled .ui-icon {
.timer-alert-mode-button:disabled .ui-icon,
.timer-inline-refresh-toggle:disabled .ui-icon {
opacity: 0.34;
}