Polish client duration and tooltip interactions
All checks were successful
Build and Deploy Gateway / build-and-push (push) Successful in 13s
Build and Deploy Gateway / deploy (push) Successful in 1s

This commit is contained in:
2026-07-11 16:12:09 +03:00
parent 85053f9948
commit 322f5a125b
6 changed files with 109 additions and 16 deletions

View File

@@ -14,6 +14,11 @@ import { formatBytes } from '../utils/format.js';
import { instructionBlocks } from '../instructions.js';
const SUBSCRIPTION_REVEAL_DELAY_MS = 1350;
const DURATION_MODE_STORAGE_KEY = 'harbor-duration-mode';
function CloudTooltip({ children }) {
return <span className="client-tooltip" role="tooltip">{children}</span>;
}
function InstructionStep({ step }) {
if (typeof step === 'string') return step;
@@ -66,7 +71,7 @@ function InstructionBlock({ block, open, onToggle }) {
function DurationPart({ name, children }) {
return (
<span
className={`client-duration-part${name.endsWith('-value') ? ' is-value' : ' is-label'}`}
className={`client-duration-part client-duration-${name}${name.endsWith('-value') ? ' is-value' : ' is-label'}`}
>
{children}
</span>
@@ -176,7 +181,13 @@ export function ClientOverviewPage({
const showPower = hasSubscription && Boolean(selectedTag);
const canStart = Boolean(selectedTag || state?.configExists);
const [now, setNow] = useState(Date.now());
const [durationMode, setDurationMode] = useState('digital');
const [durationMode, setDurationMode] = useState(() => {
try {
return localStorage.getItem(DURATION_MODE_STORAGE_KEY) === 'words' ? 'words' : 'digital';
} catch {
return 'digital';
}
});
const [editingSubscription, setEditingSubscription] = useState(!state?.hasSubscription);
const [subscriptionValidation, setSubscriptionValidation] = useState({ url: '', status: 'idle' });
const [showIntro, setShowIntro] = useState(!hasSubscription);
@@ -449,7 +460,15 @@ export function ClientOverviewPage({
}
function toggleDurationMode() {
setDurationMode((mode) => mode === 'digital' ? 'words' : 'digital');
setDurationMode((mode) => {
const nextMode = mode === 'digital' ? 'words' : 'digital';
try {
localStorage.setItem(DURATION_MODE_STORAGE_KEY, nextMode);
} catch {
// The visual preference still works for this session.
}
return nextMode;
});
}
return (
@@ -502,11 +521,10 @@ export function ClientOverviewPage({
<div className="client-state-detail">
{connected ? (
<button
className={`client-duration-toggle${durationMode === 'words' ? ' is-words' : ''}`}
className={`client-duration-toggle client-tooltip-anchor${durationMode === 'words' ? ' is-words' : ''}`}
type="button"
key="duration"
aria-label={durationMode === 'digital' ? 'Показать время словами' : 'Показать цифровой таймер'}
title={durationMode === 'digital' ? 'Показать время словами' : 'Показать цифровой таймер'}
onClick={toggleDurationMode}
>
<span className="client-duration-stack">
@@ -516,7 +534,7 @@ export function ClientOverviewPage({
>
<DurationPart name="hours-value">{String(duration.totalHours).padStart(2, '0')}</DurationPart>
:<DurationPart name="minutes-value">{String(duration.minutes.value).padStart(2, '0')}</DurationPart>
:<DurationPart name="seconds-value">{String(duration.seconds.value).padStart(2, '0')}</DurationPart>
:<DurationPart name="seconds-value" key={`digital-${duration.seconds.value}`}>{String(duration.seconds.value).padStart(2, '0')}</DurationPart>
</time>
<time
className={`client-duration${durationMode === 'words' ? ' is-active' : ''}`}
@@ -531,12 +549,13 @@ export function ClientOverviewPage({
.filter(([name, part]) => part.value || name === 'seconds')
.map(([name, part]) => (
<span className="client-duration-unit" data-unit={name} key={name}>
<DurationPart name={`${name}-value`}>{part.value}</DurationPart>{' '}
<DurationPart name={`${name}-value`} key={`${name}-${name === 'seconds' ? part.value : 'value'}`}>{part.value}</DurationPart>{' '}
<DurationPart name={`${name}-label`}>{part.label}</DurationPart>
</span>
))}
</time>
</span>
<CloudTooltip>{durationMode === 'digital' ? 'Показать время словами' : 'Показать цифровой таймер'}</CloudTooltip>
</button>
) : (
<p key="hint">
@@ -651,22 +670,21 @@ export function ClientOverviewPage({
<div className="client-subscription-heading">
<span>Ваша подписка</span>
<button
className="client-subscription-refresh"
className="client-subscription-refresh client-tooltip-anchor"
type="button"
aria-label="Обновить подписку"
title="Обновить подписку"
disabled={refreshingInfo}
onClick={refreshSubscription}
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M21 12a9 9 0 0 0-15.2-6.5L3 8m0-5v5h5M3 12a9 9 0 0 0 15.2 6.5L21 16m0 5v-5h-5" />
</svg>
<CloudTooltip>Обновить подписку</CloudTooltip>
</button>
<button
className="client-subscription-delete"
className="client-subscription-delete client-tooltip-anchor"
type="button"
aria-label="Удалить подписку"
title="Удалить подписку"
disabled={busy}
onClick={() => setConfirmingDelete(true)}
>
@@ -674,6 +692,7 @@ export function ClientOverviewPage({
<path className="client-trash-lid" d="M4 7h16M9 7V4h6v3" />
<path d="m6 7 1 13h10l1-13M10 11v5M14 11v5" />
</svg>
<CloudTooltip>Удалить подписку</CloudTooltip>
</button>
</div>
<button