Polish client duration and tooltip interactions
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user