fix: respect time format preference in UI
Add shared time formatting helpers that apply the Appearance time format preference consistently. Update visible time labels across chat, quota usage, scheduled tasks, tunnels, context details, git views, PR metadata, and passkey settings to use the selected 12-hour, 24-hour, or automatic format. Leave date-only and non-UI formatting untouched so unrelated behavior does not change.
This commit is contained in:
@@ -1038,6 +1038,7 @@ const AssistantMessageBody = React.memo(({
|
|||||||
const collapsibleThinkingBlocks = useUIStore((state) => state.collapsibleThinkingBlocks);
|
const collapsibleThinkingBlocks = useUIStore((state) => state.collapsibleThinkingBlocks);
|
||||||
const groupReasoningBlocks = useUIStore((state) => state.groupReasoningBlocks);
|
const groupReasoningBlocks = useUIStore((state) => state.groupReasoningBlocks);
|
||||||
const showSplitAssistantMessageActions = useUIStore((state) => state.showSplitAssistantMessageActions);
|
const showSplitAssistantMessageActions = useUIStore((state) => state.showSplitAssistantMessageActions);
|
||||||
|
const timeFormatPreference = useUIStore((state) => state.timeFormatPreference);
|
||||||
const vscodeApi = useRuntimeAPIs().vscode;
|
const vscodeApi = useRuntimeAPIs().vscode;
|
||||||
const isSortedRenderMode = chatRenderMode === 'sorted';
|
const isSortedRenderMode = chatRenderMode === 'sorted';
|
||||||
const collapsedPreviewCount = 7;
|
const collapsedPreviewCount = 7;
|
||||||
@@ -1757,9 +1758,9 @@ const AssistantMessageBody = React.memo(({
|
|||||||
: (typeof messageCreatedAt === 'number' && messageCreatedAt > 0 ? messageCreatedAt : null);
|
: (typeof messageCreatedAt === 'number' && messageCreatedAt > 0 ? messageCreatedAt : null);
|
||||||
if (timestamp === null) return null;
|
if (timestamp === null) return null;
|
||||||
|
|
||||||
const formatted = formatTimestampForDisplay(timestamp);
|
const formatted = formatTimestampForDisplay(timestamp, timeFormatPreference);
|
||||||
return formatted.length > 0 ? formatted : null;
|
return formatted.length > 0 ? formatted : null;
|
||||||
}, [messageCompletedAt, messageCreatedAt]);
|
}, [messageCompletedAt, messageCreatedAt, timeFormatPreference]);
|
||||||
|
|
||||||
const footerTimestampClassName = 'text-sm text-muted-foreground/60 tabular-nums flex items-center gap-1';
|
const footerTimestampClassName = 'text-sm text-muted-foreground/60 tabular-nums flex items-center gap-1';
|
||||||
const canOpenMessagePreview = !isMiniChatSurface && !isMobile && !isVSCode;
|
const canOpenMessagePreview = !isMiniChatSurface && !isMobile && !isVSCode;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const pad2 = (value: number): string => String(value).padStart(2, '0');
|
import { formatTimeForPreference } from '@/lib/timeFormat';
|
||||||
|
import type { TimeFormatPreference } from '@/stores/useUIStore';
|
||||||
|
|
||||||
const isSameDay = (left: Date, right: Date): boolean => {
|
const isSameDay = (left: Date, right: Date): boolean => {
|
||||||
return (
|
return (
|
||||||
@@ -18,7 +19,7 @@ const isValidTimestamp = (timestamp: number): boolean => {
|
|||||||
return Number.isFinite(timestamp) && !Number.isNaN(new Date(timestamp).getTime());
|
return Number.isFinite(timestamp) && !Number.isNaN(new Date(timestamp).getTime());
|
||||||
};
|
};
|
||||||
|
|
||||||
export const formatTimestampForDisplay = (timestamp: number): string => {
|
export const formatTimestampForDisplay = (timestamp: number, timeFormatPreference: TimeFormatPreference): string => {
|
||||||
if (!isValidTimestamp(timestamp)) {
|
if (!isValidTimestamp(timestamp)) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -26,7 +27,7 @@ export const formatTimestampForDisplay = (timestamp: number): string => {
|
|||||||
const date = new Date(timestamp);
|
const date = new Date(timestamp);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
const timePart = `${pad2(date.getHours())}:${pad2(date.getMinutes())}`;
|
const timePart = formatTimeForPreference(date, timeFormatPreference);
|
||||||
|
|
||||||
if (isSameDay(date, now)) {
|
if (isSameDay(date, now)) {
|
||||||
return timePart;
|
return timePart;
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import { useSessionUIStore } from '@/sync/session-ui-store';
|
|||||||
import { useSessions, useSessionMessageRecords } from '@/sync/sync-context';
|
import { useSessions, useSessionMessageRecords } from '@/sync/sync-context';
|
||||||
import { copyTextToClipboard } from '@/lib/clipboard';
|
import { copyTextToClipboard } from '@/lib/clipboard';
|
||||||
import { useI18n } from '@/lib/i18n';
|
import { useI18n } from '@/lib/i18n';
|
||||||
|
import { formatDateTimeForPreference } from '@/lib/timeFormat';
|
||||||
|
import { useUIStore, type TimeFormatPreference } from '@/stores/useUIStore';
|
||||||
|
|
||||||
type SessionMessage = { info: Message; parts: Part[] };
|
type SessionMessage = { info: Message; parts: Part[] };
|
||||||
|
|
||||||
@@ -229,9 +231,9 @@ const formatMoney = (value: number): string => {
|
|||||||
return `$${value.toFixed(2)}`;
|
return `$${value.toFixed(2)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatDateTime = (timestamp: number | null): string => {
|
const formatDateTime = (timestamp: number | null, timeFormatPreference: TimeFormatPreference): string => {
|
||||||
if (!timestamp || !Number.isFinite(timestamp)) return '-';
|
if (!timestamp || !Number.isFinite(timestamp)) return '-';
|
||||||
return new Date(timestamp).toLocaleString(undefined, {
|
return formatDateTimeForPreference(timestamp, timeFormatPreference, {
|
||||||
month: 'short',
|
month: 'short',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
@@ -240,9 +242,9 @@ const formatDateTime = (timestamp: number | null): string => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatMessageDateMeta = (timestamp: number | null): string => {
|
const formatMessageDateMeta = (timestamp: number | null, timeFormatPreference: TimeFormatPreference): string => {
|
||||||
if (!timestamp || !Number.isFinite(timestamp)) return '-';
|
if (!timestamp || !Number.isFinite(timestamp)) return '-';
|
||||||
return new Date(timestamp).toLocaleString(undefined, {
|
return formatDateTimeForPreference(timestamp, timeFormatPreference, {
|
||||||
month: 'short',
|
month: 'short',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
hour: 'numeric',
|
hour: 'numeric',
|
||||||
@@ -273,6 +275,7 @@ const resolveProviderAndModel = (
|
|||||||
export const ContextPanelContent: React.FC = () => {
|
export const ContextPanelContent: React.FC = () => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { currentTheme } = useThemeSystem();
|
const { currentTheme } = useThemeSystem();
|
||||||
|
const timeFormatPreference = useUIStore((state) => state.timeFormatPreference);
|
||||||
const syntaxTheme = React.useMemo(() => generateSyntaxTheme(currentTheme), [currentTheme]);
|
const syntaxTheme = React.useMemo(() => generateSyntaxTheme(currentTheme), [currentTheme]);
|
||||||
const [expandedRawMessages, setExpandedRawMessages] = React.useState<Record<string, boolean>>({});
|
const [expandedRawMessages, setExpandedRawMessages] = React.useState<Record<string, boolean>>({});
|
||||||
const [copiedRawMessageId, setCopiedRawMessageId] = React.useState<string | null>(null);
|
const [copiedRawMessageId, setCopiedRawMessageId] = React.useState<string | null>(null);
|
||||||
@@ -416,7 +419,7 @@ export const ContextPanelContent: React.FC = () => {
|
|||||||
{viewModel.createdAt && (
|
{viewModel.createdAt && (
|
||||||
<>
|
<>
|
||||||
<span>·</span>
|
<span>·</span>
|
||||||
<span>{formatDateTime(viewModel.createdAt)}</span>
|
<span>{formatDateTime(viewModel.createdAt, timeFormatPreference)}</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -547,7 +550,7 @@ export const ContextPanelContent: React.FC = () => {
|
|||||||
<span className="typography-ui-label text-foreground shrink-0">{capitalizeRole(role)}</span>
|
<span className="typography-ui-label text-foreground shrink-0">{capitalizeRole(role)}</span>
|
||||||
<span className="min-w-0 truncate typography-micro text-muted-foreground">{message.info.id}</span>
|
<span className="min-w-0 truncate typography-micro text-muted-foreground">{message.info.id}</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="typography-micro text-muted-foreground shrink-0">{formatMessageDateMeta(messageCreatedAt)}</span>
|
<span className="typography-micro text-muted-foreground shrink-0">{formatMessageDateMeta(messageCreatedAt, timeFormatPreference)}</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,9 @@ import { formatQuotaValueLabel, formatQuotaResetLabel, formatWindowLabel, QUOTA_
|
|||||||
import { UsageProgressBar } from '@/components/sections/usage/UsageProgressBar';
|
import { UsageProgressBar } from '@/components/sections/usage/UsageProgressBar';
|
||||||
import { PaceIndicator } from '@/components/sections/usage/PaceIndicator';
|
import { PaceIndicator } from '@/components/sections/usage/PaceIndicator';
|
||||||
import { updateDesktopSettings } from '@/lib/persistence';
|
import { updateDesktopSettings } from '@/lib/persistence';
|
||||||
|
import { formatTimeForPreference } from '@/lib/timeFormat';
|
||||||
import { eventMatchesShortcut, formatShortcutForDisplay, getEffectiveShortcutCombo } from '@/lib/shortcuts';
|
import { eventMatchesShortcut, formatShortcutForDisplay, getEffectiveShortcutCombo } from '@/lib/shortcuts';
|
||||||
|
import type { TimeFormatPreference } from '@/stores/useUIStore';
|
||||||
import {
|
import {
|
||||||
getAllModelFamilies,
|
getAllModelFamilies,
|
||||||
getDisplayModelName,
|
getDisplayModelName,
|
||||||
@@ -356,6 +358,7 @@ type DesktopServicesMenuProps = {
|
|||||||
remoteUpdateError: string | null;
|
remoteUpdateError: string | null;
|
||||||
onOpenRemoteUpdate: () => void;
|
onOpenRemoteUpdate: () => void;
|
||||||
showPredValues: boolean;
|
showPredValues: boolean;
|
||||||
|
timeFormatPreference: TimeFormatPreference;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DesktopServicesMenu = React.memo(function DesktopServicesMenu({
|
const DesktopServicesMenu = React.memo(function DesktopServicesMenu({
|
||||||
@@ -391,6 +394,7 @@ const DesktopServicesMenu = React.memo(function DesktopServicesMenu({
|
|||||||
remoteUpdateError,
|
remoteUpdateError,
|
||||||
onOpenRemoteUpdate,
|
onOpenRemoteUpdate,
|
||||||
showPredValues,
|
showPredValues,
|
||||||
|
timeFormatPreference,
|
||||||
}: DesktopServicesMenuProps) {
|
}: DesktopServicesMenuProps) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
return (
|
return (
|
||||||
@@ -511,7 +515,7 @@ const DesktopServicesMenu = React.memo(function DesktopServicesMenu({
|
|||||||
<div className="flex items-center justify-between gap-3 border-b border-[var(--interactive-border)] px-4 py-2.5">
|
<div className="flex items-center justify-between gap-3 border-b border-[var(--interactive-border)] px-4 py-2.5">
|
||||||
<div className="flex min-w-0 items-baseline gap-2">
|
<div className="flex min-w-0 items-baseline gap-2">
|
||||||
<span className="typography-ui-header font-semibold text-foreground">{t('header.services.rateLimits')}</span>
|
<span className="typography-ui-header font-semibold text-foreground">{t('header.services.rateLimits')}</span>
|
||||||
<span className="truncate typography-micro text-muted-foreground">{formatTime(quotaLastUpdated)}</span>
|
<span className="truncate typography-micro text-muted-foreground">{formatTime(quotaLastUpdated, timeFormatPreference)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<div className="h-7 w-[10.5rem]">
|
<div className="h-7 w-[10.5rem]">
|
||||||
@@ -572,7 +576,7 @@ const DesktopServicesMenu = React.memo(function DesktopServicesMenu({
|
|||||||
: calculateExpectedUsagePercent(paceInfo.elapsedRatio))
|
: calculateExpectedUsagePercent(paceInfo.elapsedRatio))
|
||||||
: null;
|
: null;
|
||||||
const metricLabel = formatQuotaValueLabel(window.valueLabel, displayPercent);
|
const metricLabel = formatQuotaValueLabel(window.valueLabel, displayPercent);
|
||||||
const resetLabel = formatQuotaResetLabel(window.resetAt, window.resetAfterFormatted ?? window.resetAtFormatted);
|
const resetLabel = formatQuotaResetLabel(window.resetAt, window.resetAfterFormatted ?? window.resetAtFormatted, timeFormatPreference);
|
||||||
return (
|
return (
|
||||||
<div key={`${group.providerId}-${label}`} className="flex flex-col gap-1.5">
|
<div key={`${group.providerId}-${label}`} className="flex flex-col gap-1.5">
|
||||||
<div className="flex min-w-0 items-center justify-between gap-3">
|
<div className="flex min-w-0 items-center justify-between gap-3">
|
||||||
@@ -714,13 +718,10 @@ const formatCompactHeaderLabel = (value: string): string => {
|
|||||||
return trimmed.length > 12 ? `${trimmed.slice(0, 9).trimEnd()}...` : trimmed;
|
return trimmed.length > 12 ? `${trimmed.slice(0, 9).trimEnd()}...` : trimmed;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatTime = (timestamp: number | null) => {
|
const formatTime = (timestamp: number | null, timeFormatPreference: 'auto' | '12h' | '24h') => {
|
||||||
if (!timestamp) return '-';
|
if (!timestamp) return '-';
|
||||||
try {
|
try {
|
||||||
return new Date(timestamp).toLocaleTimeString(undefined, {
|
return formatTimeForPreference(timestamp, timeFormatPreference, { fallback: '-' });
|
||||||
hour: 'numeric',
|
|
||||||
minute: '2-digit',
|
|
||||||
});
|
|
||||||
} catch {
|
} catch {
|
||||||
return '-';
|
return '-';
|
||||||
}
|
}
|
||||||
@@ -791,6 +792,7 @@ export const Header: React.FC<HeaderProps> = ({
|
|||||||
const activeMainTab = useUIStore((state) => state.activeMainTab);
|
const activeMainTab = useUIStore((state) => state.activeMainTab);
|
||||||
const setActiveMainTab = useUIStore((state) => state.setActiveMainTab);
|
const setActiveMainTab = useUIStore((state) => state.setActiveMainTab);
|
||||||
const shortcutOverrides = useUIStore((state) => state.shortcutOverrides);
|
const shortcutOverrides = useUIStore((state) => state.shortcutOverrides);
|
||||||
|
const timeFormatPreference = useUIStore((state) => state.timeFormatPreference);
|
||||||
|
|
||||||
const getCurrentModel = useConfigStore((state) => state.getCurrentModel);
|
const getCurrentModel = useConfigStore((state) => state.getCurrentModel);
|
||||||
const runtimeApis = useRuntimeAPIs();
|
const runtimeApis = useRuntimeAPIs();
|
||||||
@@ -2089,6 +2091,7 @@ export const Header: React.FC<HeaderProps> = ({
|
|||||||
remoteUpdateChecking={remoteUpdateChecking}
|
remoteUpdateChecking={remoteUpdateChecking}
|
||||||
remoteUpdateError={remoteUpdateError}
|
remoteUpdateError={remoteUpdateError}
|
||||||
onOpenRemoteUpdate={openRemoteInstanceUpdate}
|
onOpenRemoteUpdate={openRemoteInstanceUpdate}
|
||||||
|
timeFormatPreference={timeFormatPreference}
|
||||||
/>
|
/>
|
||||||
<HeaderIconActionButton
|
<HeaderIconActionButton
|
||||||
title={t('header.actions.terminalPanelWithShortcut', { shortcut: shortcutLabel('toggle_terminal') })}
|
title={t('header.actions.terminalPanelWithShortcut', { shortcut: shortcutLabel('toggle_terminal') })}
|
||||||
@@ -2445,7 +2448,7 @@ export const Header: React.FC<HeaderProps> = ({
|
|||||||
<div className="flex flex-col min-w-0 gap-0.5">
|
<div className="flex flex-col min-w-0 gap-0.5">
|
||||||
<span className="typography-ui-header font-semibold text-foreground">{t('header.services.rateLimits')}</span>
|
<span className="typography-ui-header font-semibold text-foreground">{t('header.services.rateLimits')}</span>
|
||||||
<span className="truncate typography-micro text-muted-foreground">
|
<span className="truncate typography-micro text-muted-foreground">
|
||||||
{formatTime(quotaLastUpdated)}
|
{formatTime(quotaLastUpdated, timeFormatPreference)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 shrink-0">
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
@@ -2533,7 +2536,7 @@ export const Header: React.FC<HeaderProps> = ({
|
|||||||
: calculateExpectedUsagePercent(paceInfo.elapsedRatio))
|
: calculateExpectedUsagePercent(paceInfo.elapsedRatio))
|
||||||
: null;
|
: null;
|
||||||
const metricLabel = formatQuotaValueLabel(window.valueLabel, displayPercent);
|
const metricLabel = formatQuotaValueLabel(window.valueLabel, displayPercent);
|
||||||
const resetLabel = formatQuotaResetLabel(window.resetAt, window.resetAfterFormatted ?? window.resetAtFormatted);
|
const resetLabel = formatQuotaResetLabel(window.resetAt, window.resetAfterFormatted ?? window.resetAtFormatted, timeFormatPreference);
|
||||||
return (
|
return (
|
||||||
<div key={`${group.providerId}-${label}`} className="flex flex-col gap-1.5">
|
<div key={`${group.providerId}-${label}`} className="flex flex-col gap-1.5">
|
||||||
<div className="flex min-w-0 items-center justify-between gap-3">
|
<div className="flex min-w-0 items-center justify-between gap-3">
|
||||||
|
|||||||
@@ -30,19 +30,18 @@ import { formatQuotaValueLabel, formatQuotaResetLabel, formatWindowLabel, QUOTA_
|
|||||||
import { useQuotaAutoRefresh, useQuotaStore } from '@/stores/useQuotaStore';
|
import { useQuotaAutoRefresh, useQuotaStore } from '@/stores/useQuotaStore';
|
||||||
import { useUpdateStore } from '@/stores/useUpdateStore';
|
import { useUpdateStore } from '@/stores/useUpdateStore';
|
||||||
import { updateDesktopSettings } from '@/lib/persistence';
|
import { updateDesktopSettings } from '@/lib/persistence';
|
||||||
|
import { formatTimeForPreference } from '@/lib/timeFormat';
|
||||||
import { lazyWithChunkRecovery } from '@/lib/chunkLoadRecovery';
|
import { lazyWithChunkRecovery } from '@/lib/chunkLoadRecovery';
|
||||||
import type { UsageWindow } from '@/types';
|
import type { UsageWindow } from '@/types';
|
||||||
import type { SessionContextUsage } from '@/stores/types/sessionTypes';
|
import type { SessionContextUsage } from '@/stores/types/sessionTypes';
|
||||||
|
import { useUIStore, type TimeFormatPreference } from '@/stores/useUIStore';
|
||||||
|
|
||||||
const SettingsView = lazyWithChunkRecovery(() => import('@/components/views/SettingsView').then(m => ({ default: m.SettingsView })));
|
const SettingsView = lazyWithChunkRecovery(() => import('@/components/views/SettingsView').then(m => ({ default: m.SettingsView })));
|
||||||
|
|
||||||
const formatTime = (timestamp: number | null) => {
|
const formatTime = (timestamp: number | null, timeFormatPreference: TimeFormatPreference) => {
|
||||||
if (!timestamp) return '-';
|
if (!timestamp) return '-';
|
||||||
try {
|
try {
|
||||||
return new Date(timestamp).toLocaleTimeString(undefined, {
|
return formatTimeForPreference(timestamp, timeFormatPreference, { fallback: '-' });
|
||||||
hour: 'numeric',
|
|
||||||
minute: '2-digit',
|
|
||||||
});
|
|
||||||
} catch {
|
} catch {
|
||||||
return '-';
|
return '-';
|
||||||
}
|
}
|
||||||
@@ -583,6 +582,7 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
|
|||||||
const quotaLastUpdated = useQuotaStore((state) => state.lastUpdated);
|
const quotaLastUpdated = useQuotaStore((state) => state.lastUpdated);
|
||||||
const quotaDisplayMode = useQuotaStore((state) => state.displayMode);
|
const quotaDisplayMode = useQuotaStore((state) => state.displayMode);
|
||||||
const showPredValues = useQuotaStore((state) => state.showPredValues);
|
const showPredValues = useQuotaStore((state) => state.showPredValues);
|
||||||
|
const timeFormatPreference = useUIStore((state) => state.timeFormatPreference);
|
||||||
const dropdownProviderIds = useQuotaStore((state) => state.dropdownProviderIds);
|
const dropdownProviderIds = useQuotaStore((state) => state.dropdownProviderIds);
|
||||||
const loadQuotaSettings = useQuotaStore((state) => state.loadSettings);
|
const loadQuotaSettings = useQuotaStore((state) => state.loadSettings);
|
||||||
const setQuotaDisplayMode = useQuotaStore((state) => state.setDisplayMode);
|
const setQuotaDisplayMode = useQuotaStore((state) => state.setDisplayMode);
|
||||||
@@ -839,7 +839,7 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
|
|||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-b border-[var(--interactive-border)] px-2 pb-2 typography-micro text-muted-foreground text-[10px]">
|
<div className="border-b border-[var(--interactive-border)] px-2 pb-2 typography-micro text-muted-foreground text-[10px]">
|
||||||
{t('vscodeLayout.quota.lastUpdated', { time: formatTime(quotaLastUpdated) })}
|
{t('vscodeLayout.quota.lastUpdated', { time: formatTime(quotaLastUpdated, timeFormatPreference) })}
|
||||||
</div>
|
</div>
|
||||||
{!hasRateLimits && (
|
{!hasRateLimits && (
|
||||||
<DropdownMenuItem className="cursor-default" closeOnClick={false}>
|
<DropdownMenuItem className="cursor-default" closeOnClick={false}>
|
||||||
@@ -899,7 +899,7 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<span className="flex items-center justify-between typography-micro text-muted-foreground text-[10px]">
|
<span className="flex items-center justify-between typography-micro text-muted-foreground text-[10px]">
|
||||||
<span>{formatQuotaResetLabel(window.resetAt, window.resetAfterFormatted ?? window.resetAtFormatted)}</span>
|
<span>{formatQuotaResetLabel(window.resetAt, window.resetAfterFormatted ?? window.resetAtFormatted, timeFormatPreference)}</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|||||||
@@ -15,8 +15,9 @@ import {
|
|||||||
type StoredPasskey,
|
type StoredPasskey,
|
||||||
} from '@/lib/passkeys';
|
} from '@/lib/passkeys';
|
||||||
import { useI18n } from '@/lib/i18n';
|
import { useI18n } from '@/lib/i18n';
|
||||||
|
import { useUIStore, type TimeFormatPreference } from '@/stores/useUIStore';
|
||||||
|
|
||||||
const formatTimestamp = (timestamp: number | null, neverUsedText: string) => {
|
const formatTimestamp = (timestamp: number | null, neverUsedText: string, timeFormatPreference: TimeFormatPreference) => {
|
||||||
if (!timestamp || !Number.isFinite(timestamp)) {
|
if (!timestamp || !Number.isFinite(timestamp)) {
|
||||||
return neverUsedText;
|
return neverUsedText;
|
||||||
}
|
}
|
||||||
@@ -24,11 +25,13 @@ const formatTimestamp = (timestamp: number | null, neverUsedText: string) => {
|
|||||||
return new Intl.DateTimeFormat(undefined, {
|
return new Intl.DateTimeFormat(undefined, {
|
||||||
dateStyle: 'medium',
|
dateStyle: 'medium',
|
||||||
timeStyle: 'short',
|
timeStyle: 'short',
|
||||||
|
hour12: timeFormatPreference === 'auto' ? undefined : timeFormatPreference === '12h',
|
||||||
}).format(timestamp);
|
}).format(timestamp);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PasskeySettings: React.FC = () => {
|
export const PasskeySettings: React.FC = () => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const timeFormatPreference = useUIStore((state) => state.timeFormatPreference);
|
||||||
const [supportsPasskeys, setSupportsPasskeys] = React.useState(false);
|
const [supportsPasskeys, setSupportsPasskeys] = React.useState(false);
|
||||||
const [isLoading, setIsLoading] = React.useState(true);
|
const [isLoading, setIsLoading] = React.useState(true);
|
||||||
const [isRegistering, setIsRegistering] = React.useState(false);
|
const [isRegistering, setIsRegistering] = React.useState(false);
|
||||||
@@ -230,10 +233,10 @@ export const PasskeySettings: React.FC = () => {
|
|||||||
<span className="typography-meta text-muted-foreground truncate">
|
<span className="typography-meta text-muted-foreground truncate">
|
||||||
{passkey.lastUsedAt
|
{passkey.lastUsedAt
|
||||||
? t('settings.openchamber.passkeys.item.lastUsed', {
|
? t('settings.openchamber.passkeys.item.lastUsed', {
|
||||||
time: formatTimestamp(passkey.lastUsedAt, t('settings.openchamber.passkeys.time.neverUsed')),
|
time: formatTimestamp(passkey.lastUsedAt, t('settings.openchamber.passkeys.time.neverUsed'), timeFormatPreference),
|
||||||
})
|
})
|
||||||
: t('settings.openchamber.passkeys.item.added', {
|
: t('settings.openchamber.passkeys.item.added', {
|
||||||
time: formatTimestamp(passkey.createdAt, t('settings.openchamber.passkeys.time.neverUsed')),
|
time: formatTimestamp(passkey.createdAt, t('settings.openchamber.passkeys.time.neverUsed'), timeFormatPreference),
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import { useI18n } from '@/lib/i18n';
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { openExternalUrl } from '@/lib/url';
|
import { openExternalUrl } from '@/lib/url';
|
||||||
import { getRuntimeApiBaseUrl } from '@/lib/runtime-switch';
|
import { getRuntimeApiBaseUrl } from '@/lib/runtime-switch';
|
||||||
|
import { formatTimeForPreference } from '@/lib/timeFormat';
|
||||||
|
import { useUIStore, type TimeFormatPreference } from '@/stores/useUIStore';
|
||||||
|
|
||||||
type TunnelState =
|
type TunnelState =
|
||||||
| 'checking'
|
| 'checking'
|
||||||
@@ -209,8 +211,8 @@ const formatRemaining = (remainingMs: number): string => {
|
|||||||
return `${seconds}s`;
|
return `${seconds}s`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatAbsoluteTime = (timestamp: number): string => {
|
const formatAbsoluteTime = (timestamp: number, timeFormatPreference: TimeFormatPreference): string => {
|
||||||
return new Date(timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
return formatTimeForPreference(timestamp, timeFormatPreference, { hour: '2-digit', precision: 'second' });
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizePresetHostname = (value: string): string => {
|
const normalizePresetHostname = (value: string): string => {
|
||||||
@@ -267,6 +269,7 @@ const createPresetId = (): string => {
|
|||||||
|
|
||||||
export const TunnelSettings: React.FC = () => {
|
export const TunnelSettings: React.FC = () => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const timeFormatPreference = useUIStore((state) => state.timeFormatPreference);
|
||||||
const tUnsafe = React.useCallback((key: string) => t(key as Parameters<typeof t>[0]), [t]);
|
const tUnsafe = React.useCallback((key: string) => t(key as Parameters<typeof t>[0]), [t]);
|
||||||
const [state, setState] = React.useState<TunnelState>('checking');
|
const [state, setState] = React.useState<TunnelState>('checking');
|
||||||
const [tunnelInfo, setTunnelInfo] = React.useState<TunnelInfo | null>(null);
|
const [tunnelInfo, setTunnelInfo] = React.useState<TunnelInfo | null>(null);
|
||||||
@@ -1167,7 +1170,7 @@ export const TunnelSettings: React.FC = () => {
|
|||||||
{modeLabel}
|
{modeLabel}
|
||||||
</span>
|
</span>
|
||||||
<span className="typography-meta text-muted-foreground/80">
|
<span className="typography-meta text-muted-foreground/80">
|
||||||
{t('settings.openchamber.tunnel.session.redeemedAt', { time: formatAbsoluteTime(record.createdAt) })}
|
{t('settings.openchamber.tunnel.session.redeemedAt', { time: formatAbsoluteTime(record.createdAt, timeFormatPreference) })}
|
||||||
</span>
|
</span>
|
||||||
<span className="typography-meta text-foreground">
|
<span className="typography-meta text-foreground">
|
||||||
{record.isActive
|
{record.isActive
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { UsageProgressBar } from './UsageProgressBar';
|
|||||||
import { PaceIndicator } from './PaceIndicator';
|
import { PaceIndicator } from './PaceIndicator';
|
||||||
import { useQuotaStore } from '@/stores/useQuotaStore';
|
import { useQuotaStore } from '@/stores/useQuotaStore';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
|
|
||||||
interface UsageCardProps {
|
interface UsageCardProps {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -25,10 +26,11 @@ export const UsageCard: React.FC<UsageCardProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const displayMode = useQuotaStore((state) => state.displayMode);
|
const displayMode = useQuotaStore((state) => state.displayMode);
|
||||||
const showPredValues = useQuotaStore((state) => state.showPredValues);
|
const showPredValues = useQuotaStore((state) => state.showPredValues);
|
||||||
|
const timeFormatPreference = useUIStore((state) => state.timeFormatPreference);
|
||||||
const displayPercent = displayMode === 'remaining' ? window.remainingPercent : window.usedPercent;
|
const displayPercent = displayMode === 'remaining' ? window.remainingPercent : window.usedPercent;
|
||||||
const barLabel = displayMode === 'remaining' ? 'remaining' : 'used';
|
const barLabel = displayMode === 'remaining' ? 'remaining' : 'used';
|
||||||
const percentLabel = formatQuotaValueLabel(window.valueLabel, displayPercent);
|
const percentLabel = formatQuotaValueLabel(window.valueLabel, displayPercent);
|
||||||
const resetLabel = formatQuotaResetLabel(window.resetAt, window.resetAfterFormatted ?? window.resetAtFormatted);
|
const resetLabel = formatQuotaResetLabel(window.resetAt, window.resetAfterFormatted ?? window.resetAtFormatted, timeFormatPreference);
|
||||||
const windowLabel = formatWindowLabel(title);
|
const windowLabel = formatWindowLabel(title);
|
||||||
|
|
||||||
const paceInfo = React.useMemo(() => {
|
const paceInfo = React.useMemo(() => {
|
||||||
|
|||||||
@@ -16,14 +16,13 @@ import { getAllModelFamilies, getDisplayModelName, sortModelFamilies, groupModel
|
|||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import { Icon } from "@/components/icon/Icon";
|
import { Icon } from "@/components/icon/Icon";
|
||||||
import { useI18n } from '@/lib/i18n';
|
import { useI18n } from '@/lib/i18n';
|
||||||
|
import { formatTimeForPreference } from '@/lib/timeFormat';
|
||||||
|
import { useUIStore, type TimeFormatPreference } from '@/stores/useUIStore';
|
||||||
|
|
||||||
const formatTime = (timestamp: number | null) => {
|
const formatTime = (timestamp: number | null, timeFormatPreference: TimeFormatPreference) => {
|
||||||
if (!timestamp) return '-';
|
if (!timestamp) return '-';
|
||||||
try {
|
try {
|
||||||
return new Date(timestamp).toLocaleTimeString(undefined, {
|
return formatTimeForPreference(timestamp, timeFormatPreference, { fallback: '-' });
|
||||||
hour: 'numeric',
|
|
||||||
minute: '2-digit'
|
|
||||||
});
|
|
||||||
} catch {
|
} catch {
|
||||||
return '-';
|
return '-';
|
||||||
}
|
}
|
||||||
@@ -36,6 +35,7 @@ interface ModelInfo {
|
|||||||
|
|
||||||
export const UsagePage: React.FC = () => {
|
export const UsagePage: React.FC = () => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const timeFormatPreference = useUIStore((state) => state.timeFormatPreference);
|
||||||
const results = useQuotaStore((state) => state.results);
|
const results = useQuotaStore((state) => state.results);
|
||||||
const selectedProviderId = useQuotaStore((state) => state.selectedProviderId);
|
const selectedProviderId = useQuotaStore((state) => state.selectedProviderId);
|
||||||
const setSelectedProvider = useQuotaStore((state) => state.setSelectedProvider);
|
const setSelectedProvider = useQuotaStore((state) => state.setSelectedProvider);
|
||||||
@@ -163,7 +163,7 @@ export const UsagePage: React.FC = () => {
|
|||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<span className="animate-pulse">{t('settings.usage.page.header.refreshing')}</span>
|
<span className="animate-pulse">{t('settings.usage.page.header.refreshing')}</span>
|
||||||
) : (
|
) : (
|
||||||
t('settings.usage.page.header.lastUpdated', { time: formatTime(lastUpdated) })
|
t('settings.usage.page.header.lastUpdated', { time: formatTime(lastUpdated, timeFormatPreference) })
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import { toast } from '@/components/ui';
|
|||||||
import { Icon } from "@/components/icon/Icon";
|
import { Icon } from "@/components/icon/Icon";
|
||||||
import type { IconName } from "@/components/icon/icons";
|
import type { IconName } from "@/components/icon/icons";
|
||||||
import { useUIStore } from '@/stores/useUIStore';
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
|
import { formatTimeForPreference } from '@/lib/timeFormat';
|
||||||
|
import type { TimeFormatPreference } from '@/stores/useUIStore';
|
||||||
import { useProjectsStore } from '@/stores/useProjectsStore';
|
import { useProjectsStore } from '@/stores/useProjectsStore';
|
||||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||||
import { refreshGlobalSessions } from '@/stores/useGlobalSessionsStore';
|
import { refreshGlobalSessions } from '@/stores/useGlobalSessionsStore';
|
||||||
@@ -100,11 +102,11 @@ const formatSchedule = (task: ScheduledTask, t: ReturnType<typeof useI18n>['t'])
|
|||||||
return t('sessions.scheduledTasks.dialog.schedule.cron', { cron: task.schedule.cron || '' });
|
return t('sessions.scheduledTasks.dialog.schedule.cron', { cron: task.schedule.cron || '' });
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatClockTime = (value?: number): string => {
|
const formatClockTime = (value: number | undefined, timeFormatPreference: TimeFormatPreference): string => {
|
||||||
if (!value || !Number.isFinite(value)) {
|
if (!value || !Number.isFinite(value)) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
return new Date(value).toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' });
|
return formatTimeForPreference(value, timeFormatPreference);
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatRelativeTime = (value: number | undefined, t: ReturnType<typeof useI18n>['t']): string => {
|
const formatRelativeTime = (value: number | undefined, t: ReturnType<typeof useI18n>['t']): string => {
|
||||||
@@ -174,6 +176,7 @@ export function ScheduledTasksDialog() {
|
|||||||
const open = useUIStore((state) => state.isScheduledTasksDialogOpen);
|
const open = useUIStore((state) => state.isScheduledTasksDialogOpen);
|
||||||
const setOpen = useUIStore((state) => state.setScheduledTasksDialogOpen);
|
const setOpen = useUIStore((state) => state.setScheduledTasksDialogOpen);
|
||||||
const isMobile = useUIStore((state) => state.isMobile);
|
const isMobile = useUIStore((state) => state.isMobile);
|
||||||
|
const timeFormatPreference = useUIStore((state) => state.timeFormatPreference);
|
||||||
const projects = useProjectsStore((state) => state.projects);
|
const projects = useProjectsStore((state) => state.projects);
|
||||||
const activeProject = useProjectsStore((state) => state.getActiveProject());
|
const activeProject = useProjectsStore((state) => state.getActiveProject());
|
||||||
const homeDirectory = useDirectoryStore((state) => state.homeDirectory);
|
const homeDirectory = useDirectoryStore((state) => state.homeDirectory);
|
||||||
@@ -471,7 +474,7 @@ export function ScheduledTasksDialog() {
|
|||||||
<>
|
<>
|
||||||
<span className="text-foreground">{formatRelativeTime(nextAt, t)}</span>
|
<span className="text-foreground">{formatRelativeTime(nextAt, t)}</span>
|
||||||
<span className="text-muted-foreground/50">·</span>
|
<span className="text-muted-foreground/50">·</span>
|
||||||
<span>{formatClockTime(nextAt)}</span>
|
<span>{formatClockTime(nextAt, timeFormatPreference)}</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<span>—</span>
|
<span>—</span>
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import type { LanedCommit } from './gitGraph';
|
|||||||
import { GitGraphSegment } from './GitGraphSegment';
|
import { GitGraphSegment } from './GitGraphSegment';
|
||||||
import * as git from '@/lib/gitApi';
|
import * as git from '@/lib/gitApi';
|
||||||
import { toast } from '@/components/ui/toast';
|
import { toast } from '@/components/ui/toast';
|
||||||
|
import { formatDateTimeForPreference } from '@/lib/timeFormat';
|
||||||
|
import { useUIStore, type TimeFormatPreference } from '@/stores/useUIStore';
|
||||||
|
|
||||||
const HISTORY_DIFF_REQUEST_TIMEOUT_MS = 15000;
|
const HISTORY_DIFF_REQUEST_TIMEOUT_MS = 15000;
|
||||||
const HISTORY_DIFF_LARGE_CHANGED_LINES = 500;
|
const HISTORY_DIFF_LARGE_CHANGED_LINES = 500;
|
||||||
@@ -77,14 +79,13 @@ interface HistoryCommitRowProps {
|
|||||||
onActionSuccess?: () => void;
|
onActionSuccess?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatCommitDate(date: string) {
|
function formatCommitDate(date: string, timeFormatPreference: TimeFormatPreference) {
|
||||||
const value = new Date(date);
|
const value = new Date(date);
|
||||||
if (Number.isNaN(value.getTime())) {
|
if (Number.isNaN(value.getTime())) {
|
||||||
return date;
|
return date;
|
||||||
}
|
}
|
||||||
|
|
||||||
return value.toLocaleString(undefined, {
|
return formatDateTimeForPreference(value, timeFormatPreference, {
|
||||||
hour12: false,
|
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'short',
|
month: 'short',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
@@ -146,6 +147,7 @@ export const HistoryCommitRow = React.memo(({
|
|||||||
onActionSuccess,
|
onActionSuccess,
|
||||||
}: HistoryCommitRowProps) => {
|
}: HistoryCommitRowProps) => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const timeFormatPreference = useUIStore((state) => state.timeFormatPreference);
|
||||||
const isGraphMode = mode === 'graph';
|
const isGraphMode = mode === 'graph';
|
||||||
type PendingAction =
|
type PendingAction =
|
||||||
| 'checkout' | 'cherryPick' | 'revert'
|
| 'checkout' | 'cherryPick' | 'revert'
|
||||||
@@ -397,8 +399,8 @@ export const HistoryCommitRow = React.memo(({
|
|||||||
{entry.author_name}
|
{entry.author_name}
|
||||||
</span>
|
</span>
|
||||||
<span className="shrink-0">·</span>
|
<span className="shrink-0">·</span>
|
||||||
<span className="truncate min-w-0" title={formatCommitDate(entry.date)}>
|
<span className="truncate min-w-0" title={formatCommitDate(entry.date, timeFormatPreference)}>
|
||||||
{formatCommitDate(entry.date)}
|
{formatCommitDate(entry.date, timeFormatPreference)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="shrink-0">·</span>
|
<span className="shrink-0">·</span>
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
|
|||||||
import { SimpleMarkdownRenderer } from '@/components/chat/MarkdownRenderer';
|
import { SimpleMarkdownRenderer } from '@/components/chat/MarkdownRenderer';
|
||||||
import { Icon } from "@/components/icon/Icon";
|
import { Icon } from "@/components/icon/Icon";
|
||||||
import { useUIStore } from '@/stores/useUIStore';
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
|
import { formatDateTimeForPreference } from '@/lib/timeFormat';
|
||||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||||
import { useSelectionStore } from '@/sync/selection-store';
|
import { useSelectionStore } from '@/sync/selection-store';
|
||||||
import { useConfigStore } from '@/stores/useConfigStore';
|
import { useConfigStore } from '@/stores/useConfigStore';
|
||||||
@@ -309,6 +310,7 @@ export const PullRequestSection: React.FC<{
|
|||||||
onGeneratedDescription?: () => void;
|
onGeneratedDescription?: () => void;
|
||||||
}> = ({ directory, branch, baseBranch, trackingBranch, remotes = [], remoteBranches = [], onGeneratedDescription }) => {
|
}> = ({ directory, branch, baseBranch, trackingBranch, remotes = [], remoteBranches = [], onGeneratedDescription }) => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const timeFormatPreference = useUIStore((state) => state.timeFormatPreference);
|
||||||
const { github } = useRuntimeAPIs();
|
const { github } = useRuntimeAPIs();
|
||||||
const githubAuthStatus = useGitHubAuthStore((state) => state.status);
|
const githubAuthStatus = useGitHubAuthStore((state) => state.status);
|
||||||
const githubAuthChecked = useGitHubAuthStore((state) => state.hasChecked);
|
const githubAuthChecked = useGitHubAuthStore((state) => state.hasChecked);
|
||||||
@@ -627,8 +629,14 @@ export const PullRequestSection: React.FC<{
|
|||||||
if (!Number.isFinite(ts)) {
|
if (!Number.isFinite(ts)) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
return new Date(ts).toLocaleString();
|
return formatDateTimeForPreference(ts, timeFormatPreference, {
|
||||||
}, []);
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: '2-digit',
|
||||||
|
});
|
||||||
|
}, [timeFormatPreference]);
|
||||||
|
|
||||||
const connectedGitHubLogin = React.useMemo(() => {
|
const connectedGitHubLogin = React.useMemo(() => {
|
||||||
const login = githubAuthStatus?.user?.login;
|
const login = githubAuthStatus?.user?.login;
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import { formatDateTimeForPreference, formatTimeForPreference } from '@/lib/timeFormat';
|
||||||
|
import type { TimeFormatPreference } from '@/stores/useUIStore';
|
||||||
|
|
||||||
export const clampPercent = (value: number | null): number | null => {
|
export const clampPercent = (value: number | null): number | null => {
|
||||||
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -22,6 +25,7 @@ export const formatQuotaValueLabel = (
|
|||||||
export const formatQuotaResetLabel = (
|
export const formatQuotaResetLabel = (
|
||||||
resetAt: number | null,
|
resetAt: number | null,
|
||||||
fallback?: string | null,
|
fallback?: string | null,
|
||||||
|
timeFormatPreference: TimeFormatPreference = 'auto',
|
||||||
): string => {
|
): string => {
|
||||||
if (!resetAt) {
|
if (!resetAt) {
|
||||||
return fallback ?? '';
|
return fallback ?? '';
|
||||||
@@ -37,13 +41,10 @@ export const formatQuotaResetLabel = (
|
|||||||
const isToday = resetDate.toDateString() === now.toDateString();
|
const isToday = resetDate.toDateString() === now.toDateString();
|
||||||
|
|
||||||
if (isToday) {
|
if (isToday) {
|
||||||
return resetDate.toLocaleTimeString(undefined, {
|
return formatTimeForPreference(resetDate, timeFormatPreference, { fallback: fallback ?? '' });
|
||||||
hour: 'numeric',
|
|
||||||
minute: '2-digit',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return resetDate.toLocaleString(undefined, {
|
return formatDateTimeForPreference(resetDate, timeFormatPreference, {
|
||||||
month: 'short',
|
month: 'short',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
weekday: 'short',
|
weekday: 'short',
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import type { TimeFormatPreference } from '@/stores/useUIStore';
|
||||||
|
|
||||||
|
type TimePrecision = 'minute' | 'second';
|
||||||
|
|
||||||
|
const getHour12Option = (preference: TimeFormatPreference): boolean | undefined => {
|
||||||
|
if (preference === '12h') return true;
|
||||||
|
if (preference === '24h') return false;
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUses24HourForPreference = (preference: TimeFormatPreference, locale: string): boolean => {
|
||||||
|
if (preference === '24h') return true;
|
||||||
|
if (preference === '12h') return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const options = new Intl.DateTimeFormat(locale, { hour: 'numeric' }).resolvedOptions();
|
||||||
|
if (typeof options.hour12 === 'boolean') {
|
||||||
|
return !options.hour12;
|
||||||
|
}
|
||||||
|
return options.hourCycle === 'h23' || options.hourCycle === 'h24';
|
||||||
|
} catch {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatTimeForPreference = (
|
||||||
|
timestamp: number | Date,
|
||||||
|
preference: TimeFormatPreference,
|
||||||
|
options: { precision?: TimePrecision; hour?: 'numeric' | '2-digit'; fallback?: string } = {},
|
||||||
|
): string => {
|
||||||
|
const date = timestamp instanceof Date ? timestamp : new Date(timestamp);
|
||||||
|
if (!Number.isFinite(date.getTime())) {
|
||||||
|
return options.fallback ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return date.toLocaleTimeString(undefined, {
|
||||||
|
hour: options.hour ?? 'numeric',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: options.precision === 'second' ? '2-digit' : undefined,
|
||||||
|
hour12: getHour12Option(preference),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatDateTimeForPreference = (
|
||||||
|
timestamp: number | Date,
|
||||||
|
preference: TimeFormatPreference,
|
||||||
|
options: Intl.DateTimeFormatOptions,
|
||||||
|
): string => {
|
||||||
|
const date = timestamp instanceof Date ? timestamp : new Date(timestamp);
|
||||||
|
if (!Number.isFinite(date.getTime())) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return date.toLocaleString(undefined, {
|
||||||
|
...options,
|
||||||
|
hour12: options.hour ? getHour12Option(preference) : options.hour12,
|
||||||
|
});
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user