mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-07 00:44:49 +00:00
The reset countdown floored to one unit, so a weekly window read "resets 2d"
from 2d 23h down to 2d 0h and looked stuck for a day; the session window had
the same problem with hours. Countdowns now use two units ("2d 23h", "4h 59m"),
and every row also shows the clock time it resets ("7:20 AM", "Sat 8:00 AM",
"Oct 31, 3:00 PM"), so users can plan work around a reset without hovering.
Rows now put the window name and percent on one line, the bar under it, and the
reset on its own full-width line, so the exact time fits at phone width in
longer languages. The pinned-row background is gone; the pin glyph, drawn in
the foreground colour when pinned, is the only pinned signal. Bars are 6px.
190 lines
5.3 KiB
TypeScript
190 lines
5.3 KiB
TypeScript
import { Pin } from "lucide-react-native";
|
|
import { useMemo } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { Pressable, Text, View } from "react-native";
|
|
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
|
import {
|
|
formatDisplayPct,
|
|
formatProviderUsageLabel,
|
|
formatResetLabel,
|
|
formatResetTime,
|
|
formatRunsOutLabel,
|
|
} from "./format";
|
|
import { UsageMeter } from "./meter";
|
|
import { displayPercent, usageWindowRowLabel } from "./model";
|
|
import { UsageResetLine } from "./reset-line";
|
|
import type { UsageDisplayAs } from "./preferences";
|
|
import { windowTone } from "./tone";
|
|
import type { UsageTone, UsageWindow } from "./types";
|
|
import { useRelativeTimeTick } from "./use-relative-time-tick";
|
|
|
|
export function UsageWindowBar({
|
|
window,
|
|
displayAs,
|
|
pinnable,
|
|
pinned,
|
|
onTogglePin,
|
|
pinLabel,
|
|
pinTestID,
|
|
retained = false,
|
|
}: {
|
|
retained?: boolean;
|
|
window: UsageWindow;
|
|
displayAs: UsageDisplayAs;
|
|
/** Whether the row pins the window to the sidebar. When false the row is a plain bar. */
|
|
pinnable: boolean;
|
|
pinned: boolean;
|
|
onTogglePin: () => void;
|
|
/** What the row pins, naming the source and window: "Pin Claude Session". */
|
|
pinLabel: string;
|
|
pinTestID: string;
|
|
}) {
|
|
const { t, i18n } = useTranslation();
|
|
const shownPct = displayPercent(window, displayAs);
|
|
const tone = windowTone(window);
|
|
|
|
const now = useRelativeTimeTick({ enabled: window.runsOutAt != null || window.resetsAt != null });
|
|
const isAtRisk =
|
|
window.runsOutAt != null &&
|
|
window.shortfallPct != null &&
|
|
(!retained || Date.parse(window.runsOutAt) > now);
|
|
const countdown = isAtRisk
|
|
? formatRunsOutLabel(window.runsOutAt, now, t)
|
|
: formatResetLabel(window.resetsAt, retained, now, t);
|
|
// The exact time belongs to the reset; beside a runs-out warning it would read as the moment
|
|
// the quota runs out.
|
|
const exactTime = isAtRisk ? null : formatResetTime(window.resetsAt, now, t);
|
|
|
|
const value =
|
|
shownPct != null ? formatDisplayPct(shownPct, displayAs, i18n.resolvedLanguage, t) : "—";
|
|
const accessibilityState = useMemo(() => ({ checked: pinned }), [pinned]);
|
|
const content = {
|
|
label: formatProviderUsageLabel(window.id, window.label, t),
|
|
value,
|
|
countdown,
|
|
exactTime,
|
|
isAtRisk,
|
|
percent: shownPct ?? 0,
|
|
tone,
|
|
pinned,
|
|
};
|
|
|
|
if (!pinnable) {
|
|
return (
|
|
<View style={styles.row}>
|
|
<WindowRowContent {...content} pinnable={false} />
|
|
</View>
|
|
);
|
|
}
|
|
|
|
// The whole row pins the window to the sidebar Usage item; the pin glyph is the only sign of it.
|
|
return (
|
|
<Pressable
|
|
onPress={onTogglePin}
|
|
accessibilityRole="checkbox"
|
|
accessibilityLabel={usageWindowRowLabel({
|
|
pinLabel,
|
|
value,
|
|
trailing: [countdown, exactTime].filter(Boolean).join(" · "),
|
|
})}
|
|
accessibilityState={accessibilityState}
|
|
aria-checked={pinned}
|
|
style={styles.row}
|
|
testID={pinTestID}
|
|
>
|
|
<WindowRowContent {...content} pinnable />
|
|
</Pressable>
|
|
);
|
|
}
|
|
|
|
function WindowRowContent({
|
|
label,
|
|
value,
|
|
countdown,
|
|
exactTime,
|
|
isAtRisk,
|
|
percent,
|
|
tone,
|
|
pinnable,
|
|
pinned,
|
|
}: {
|
|
label: string;
|
|
value: string;
|
|
countdown: string | null;
|
|
exactTime: string | null;
|
|
isAtRisk: boolean;
|
|
percent: number;
|
|
tone: UsageTone;
|
|
pinnable: boolean;
|
|
pinned: boolean;
|
|
}) {
|
|
return (
|
|
<>
|
|
<View style={styles.labelRow}>
|
|
<Text style={styles.label} numberOfLines={1}>
|
|
{label}
|
|
</Text>
|
|
<Text style={styles.value} numberOfLines={1}>
|
|
{value}
|
|
</Text>
|
|
{pinnable ? <UsagePinGlyph pinned={pinned} /> : null}
|
|
</View>
|
|
<UsageMeter percent={percent} tone={tone} />
|
|
<UsageResetLine countdown={countdown} exactTime={exactTime} atRisk={isAtRisk} />
|
|
</>
|
|
);
|
|
}
|
|
|
|
const ThemedPin = withUnistyles(Pin);
|
|
|
|
function UsagePinGlyph({ pinned }: { pinned: boolean }) {
|
|
const { t } = useTranslation();
|
|
const iconMapping = useMemo(
|
|
() => (theme: { colors: { foreground: string; foregroundMuted: string } }) => ({
|
|
color: pinned ? theme.colors.foreground : theme.colors.foregroundMuted,
|
|
fill: pinned ? theme.colors.foreground : "none",
|
|
}),
|
|
[pinned],
|
|
);
|
|
return (
|
|
<Tooltip delayDuration={300} enabledOnDesktop enabledOnMobile={false}>
|
|
<TooltipTrigger asChild>
|
|
<View
|
|
style={styles.pin}
|
|
testID={pinned ? "usage-pin-glyph-pinned" : "usage-pin-glyph-unpinned"}
|
|
>
|
|
<ThemedPin size={12} uniProps={iconMapping} />
|
|
</View>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="top">
|
|
<Text style={styles.tooltipText}>
|
|
{pinned ? t("providerUsage.unpin") : t("providerUsage.pin")}
|
|
</Text>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create((theme) => ({
|
|
row: { gap: 3, paddingVertical: theme.spacing[1.5] },
|
|
pin: { width: 12, alignItems: "center" },
|
|
tooltipText: { color: theme.colors.popoverForeground, fontSize: theme.fontSize.sm },
|
|
labelRow: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: theme.spacing[2],
|
|
},
|
|
label: {
|
|
flex: 1,
|
|
color: theme.colors.foreground,
|
|
fontSize: theme.fontSize.sm,
|
|
fontWeight: theme.fontWeight.medium,
|
|
},
|
|
value: {
|
|
flexShrink: 0,
|
|
color: theme.colors.foreground,
|
|
fontSize: theme.fontSize.sm,
|
|
},
|
|
}));
|