Fix the usage page's tables, range tabs and day bars
- Both tables lay their heading and rows on one shared grid, so every figure sits under its own heading (twui's Row wrapped the cells). - The selected range reads as pressed: ink-filled in a hairline track. - Each day's bar opens a small summary card on hover or tap. - twui 0.1.7 places popovers before paint, so opening one no longer makes the layout flicker. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
"@modelcontextprotocol/sdk": "^1.32.1",
|
||||
"@moduletrace/bun-sqlite": "^1.3.0",
|
||||
"@moduletrace/bunext": "^1.3.3",
|
||||
"@moduletrace/twui": "^0.1.6",
|
||||
"@moduletrace/twui": "^0.1.7",
|
||||
"@tiptap/extension-bubble-menu": "3.32.0",
|
||||
"@tiptap/extension-mention": "3.32.0",
|
||||
"@tiptap/extension-placeholder": "3.32.0",
|
||||
@@ -255,7 +255,7 @@
|
||||
|
||||
"@moduletrace/bunext": ["@moduletrace/[email protected]", "https://git.tben.me/api/packages/Moduletrace/npm/%40moduletrace%2Fbunext/-/1.3.3/bunext-1.3.3.tgz", { "dependencies": { "@tailwindcss/postcss": "^4.3.3", "@types/bun": "1.3.14", "@types/node": "^24.10.0", "@types/react": "^19.3.0", "@types/react-dom": "^19.3.0", "bun-plugin-tailwind": "^0.1.2", "chalk": "^5.6.2", "chokidar": "^5.0.0", "commander": "^14.0.2", "esbuild": "^0.28.2", "lightningcss-wasm": "^1.32.0", "lodash": "^4.18.1", "ora": "^9.0.0", "postcss": "^8.5.30", "react": "^19.3.0", "react-dom": "^19.3.0", "tailwindcss": "^4.3.3", "typescript": "^5.0.0", "uuid": "^14.0.2" }, "bin": { "bunext": "dist/commands/index.js" } }, "sha512-bbUtQaigsDPUQf6vtDOSCo8GYR7Gnw2ny1Fi3JtUnKsYsYQEcEadzmqntAgf7rLcKc12dadDUgFLxru+jiA92g=="],
|
||||
|
||||
"@moduletrace/twui": ["@moduletrace/[email protected].6", "https://git.tben.me/api/packages/Moduletrace/npm/%40moduletrace%2Ftwui/-/0.1.6/twui-0.1.6.tgz", { "dependencies": { "lodash": "^4.18.1", "tailwind-merge": "^3.7.0" }, "peerDependencies": { "ace-builds": "^1.40.0", "lucide-react": ">=0.577.0", "next-mdx-remote": "^6.0.0", "openai": ">=6.0.0", "react": ">=18", "react-dom": ">=18", "react-markdown": ">=9.0.0", "rehype-prism-plus": "^2.0.0", "remark-gfm": "^4.0.0", "tailwindcss": "^4" }, "optionalPeers": ["ace-builds", "next-mdx-remote", "openai", "react-markdown", "rehype-prism-plus", "remark-gfm"] }, "sha512-BFNIV1itkIYXmhq1rBJA8E8MjwN9AP7MESbOnzM0yUb6xbsOFIBbmlKfUu6kTr7QCl1TWNxWTGFPVmQ2S54cDA=="],
|
||||
"@moduletrace/twui": ["@moduletrace/[email protected].7", "https://git.tben.me/api/packages/Moduletrace/npm/%40moduletrace%2Ftwui/-/0.1.7/twui-0.1.7.tgz", { "dependencies": { "lodash": "^4.18.1", "tailwind-merge": "^3.7.0" }, "peerDependencies": { "ace-builds": "^1.40.0", "lucide-react": ">=0.577.0", "next-mdx-remote": "^6.0.0", "openai": ">=6.0.0", "react": ">=18", "react-dom": ">=18", "react-markdown": ">=9.0.0", "rehype-prism-plus": "^2.0.0", "remark-gfm": "^4.0.0", "tailwindcss": "^4" }, "optionalPeers": ["ace-builds", "next-mdx-remote", "openai", "react-markdown", "rehype-prism-plus", "remark-gfm"] }, "sha512-/Y9QyhtQsoEAJO+ahlGYQaYMx3gUWS+DlSkaBVjAIY5V9OQ6i7qhhqMNcmnfOi9m3eyAze1uW00jox+RDUqLpA=="],
|
||||
|
||||
"@next/mdx": ["@next/[email protected]", "", { "dependencies": { "source-map": "^0.7.0" }, "peerDependencies": { "@mdx-js/loader": ">=0.15.0", "@mdx-js/react": ">=0.15.0" }, "optionalPeers": ["@mdx-js/loader", "@mdx-js/react"] }, "sha512-Vpfuf928ACvx4AOdDXWx/cj13Ak6xlvMn3QCkjvfQMmLnQWCesQTeY01CPDbIyZvjY+NOwP7bvbvJ2ghkj/3GA=="],
|
||||
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@
|
||||
"@modelcontextprotocol/sdk": "^1.32.1",
|
||||
"@moduletrace/bun-sqlite": "^1.3.0",
|
||||
"@moduletrace/bunext": "^1.3.3",
|
||||
"@moduletrace/twui": "^0.1.6",
|
||||
"@moduletrace/twui": "^0.1.7",
|
||||
"@tiptap/extension-bubble-menu": "3.32.0",
|
||||
"@tiptap/extension-mention": "3.32.0",
|
||||
"@tiptap/extension-placeholder": "3.32.0",
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* The column tracks of the models table, shared by its heading and every
|
||||
* row, so a figure always sits under its own heading whatever its width.
|
||||
* The table never gets narrower than its columns: it scrolls instead.
|
||||
*/
|
||||
export const UsageModelColumns =
|
||||
"grid grid-cols-[minmax(160px,1fr)_48px_56px_56px_64px_64px_64px] min-w-[620px]";
|
||||
|
||||
/**
|
||||
* The column tracks of the breakdown by kind of call, the same way.
|
||||
*/
|
||||
export const UsageHarnessColumns = "grid grid-cols-[minmax(0,1fr)_40px_72px_64px]";
|
||||
@@ -1,39 +1,44 @@
|
||||
import Popover from "@moduletrace/twui/elements/Popover";
|
||||
import Stack from "@moduletrace/twui/layout/Stack";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import dayjs from "dayjs";
|
||||
import type { UsageOverview } from "@/src/types";
|
||||
import formatTokens from "@/src/utils/format-tokens";
|
||||
import { usageBarHeight } from "../(data)/usage-bar-heights";
|
||||
import UsageDaySummary from "./usage-day-summary";
|
||||
|
||||
type Props = {
|
||||
day: UsageOverview["days"][number];
|
||||
max: number;
|
||||
/**
|
||||
* Every day's tokens in the period, for the day's share.
|
||||
*/
|
||||
total_tokens: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* One day's bar in the chart: its tokens above nothing, the bar itself,
|
||||
* and the title spells the day out with what it spent. Hovering it shows
|
||||
* that title.
|
||||
* One day's bar in the chart. Hovering it (or tapping it) opens a small
|
||||
* card above it summing the day up.
|
||||
*/
|
||||
export default function UsageDayBar({ day, max }: Props) {
|
||||
const title = `${dayjs(day.at).format("ddd, MMM D")}: ${formatTokens({ tokens: day.total_tokens })} tokens, ${daysCalls({ calls: day.calls })}`;
|
||||
|
||||
export default function UsageDayBar({ day, max, total_tokens }: Props) {
|
||||
return (
|
||||
<Stack
|
||||
title={title}
|
||||
className="grow basis-[3px] min-w-[3px] max-w-10 h-24 justify-end items-stretch gap-0!"
|
||||
>
|
||||
<Popover
|
||||
trigger="hover"
|
||||
position="top"
|
||||
debounce={80}
|
||||
targetWrapperProps={{
|
||||
className: "grow basis-[3px] min-w-[3px] max-w-10 h-24 flex flex-col justify-end cursor-pointer group",
|
||||
}}
|
||||
target={
|
||||
<Stack
|
||||
className={twMerge(
|
||||
"w-full rounded-t-[2px]",
|
||||
day.total_tokens ? "bg-ink" : "bg-hairline",
|
||||
"w-full gap-0! rounded-t-[2px] transition-opacity",
|
||||
day.total_tokens ? "bg-ink group-hover:opacity-70" : "bg-hairline group-hover:bg-hairline-strong",
|
||||
usageBarHeight({ value: day.total_tokens, max }),
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
className="w-56 p-3 gap-2! bg-surface dark:bg-surface border-hairline dark:border-hairline rounded-default shadow-popover"
|
||||
>
|
||||
<UsageDaySummary day={day} total_tokens={total_tokens} />
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
function daysCalls({ calls }: { calls: number }) {
|
||||
return `${calls} ${calls == 1 ? "call" : "calls"}`;
|
||||
}
|
||||
|
||||
@@ -13,10 +13,12 @@ type Props = {
|
||||
|
||||
/**
|
||||
* The period day by day, one bar a day, so a spike or a quiet stretch is
|
||||
* read at a glance. The busiest day's total labels the top.
|
||||
* read at a glance. The busiest day's total labels the top, and each bar
|
||||
* opens a small summary of its day.
|
||||
*/
|
||||
export default function UsageDayChart({ days }: Props) {
|
||||
const max = days.reduce((highest, day) => Math.max(highest, day.total_tokens), 0);
|
||||
const total_tokens = days.reduce((total, day) => total + day.total_tokens, 0);
|
||||
const first = days[0];
|
||||
const last = days[days.length - 1];
|
||||
|
||||
@@ -30,9 +32,9 @@ export default function UsageDayChart({ days }: Props) {
|
||||
}
|
||||
>
|
||||
<Stack className="w-full gap-2!">
|
||||
<Row className="w-full gap-1! items-end">
|
||||
<Row className="w-full gap-1! flex-nowrap items-end">
|
||||
{days.map((day) => (
|
||||
<UsageDayBar day={day} max={max} key={day.at} />
|
||||
<UsageDayBar day={day} max={max} total_tokens={total_tokens} key={day.at} />
|
||||
))}
|
||||
</Row>
|
||||
{first && last ? (
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import Stack from "@moduletrace/twui/layout/Stack";
|
||||
import dayjs from "dayjs";
|
||||
import type { UsageOverview } from "@/src/types";
|
||||
import formatTokens from "@/src/utils/format-tokens";
|
||||
import UsageSummaryLine from "./usage-summary-line";
|
||||
|
||||
type Props = {
|
||||
day: UsageOverview["days"][number];
|
||||
/**
|
||||
* Every day's tokens in the period, for the day's share.
|
||||
*/
|
||||
total_tokens: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* What one day spent, in the card its bar opens: the day spelled out, then
|
||||
* its tokens, how they split, its calls, its cost and its share of the
|
||||
* period.
|
||||
*/
|
||||
export default function UsageDaySummary({ day, total_tokens }: Props) {
|
||||
const share = total_tokens ? Math.round((day.total_tokens / total_tokens) * 100) : 0;
|
||||
|
||||
const lines = [
|
||||
{ label: "Tokens", value: formatTokens({ tokens: day.total_tokens }) },
|
||||
{ label: "In", value: formatTokens({ tokens: day.prompt_tokens }) },
|
||||
{ label: "Out", value: formatTokens({ tokens: day.completion_tokens }) },
|
||||
{ label: "Model calls", value: formatTokens({ tokens: day.calls }) },
|
||||
{ label: "Cost", value: day.cost ? `$${day.cost.toFixed(2)}` : "-" },
|
||||
{ label: "Share of period", value: `${share}%` },
|
||||
];
|
||||
|
||||
return (
|
||||
<Stack className="w-full gap-2! items-stretch">
|
||||
<Span className="text-[13px] leading-5 font-bold text-ink">
|
||||
{dayjs(day.at).format("dddd, MMM D")}
|
||||
</Span>
|
||||
{day.calls ? (
|
||||
<Stack className="w-full gap-1! items-stretch">
|
||||
{lines.map((line) => (
|
||||
<UsageSummaryLine {...line} key={line.label} />
|
||||
))}
|
||||
</Stack>
|
||||
) : (
|
||||
<Span className="text-[12px] leading-4 text-ink-secondary">No model calls.</Span>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,9 @@ import Row from "@moduletrace/twui/layout/Row";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import Stack from "@moduletrace/twui/layout/Stack";
|
||||
import type { UsageOverview } from "@/src/types";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import SettingsGroup from "@/src/pages/admin/settings/(partials)/settings-group";
|
||||
import { UsageHarnessColumns } from "../(data)/usage-table-columns";
|
||||
import sumTokens from "../(functions)/sum-tokens";
|
||||
import UsageHarnessRow from "./usage-harness-row";
|
||||
|
||||
@@ -32,17 +34,22 @@ export default function UsageHarnessBreakdown({ rows }: Props) {
|
||||
return (
|
||||
<SettingsGroup title="By kind of call" description="Where the period's tokens went.">
|
||||
<Stack className="w-full gap-0!">
|
||||
<Row className="w-full pb-2 gap-3! items-center border-0 border-b border-solid border-hairline">
|
||||
<Span className="text-[11px] uppercase tracking-wide text-ink-secondary grow">
|
||||
<Row
|
||||
className={twMerge(
|
||||
UsageHarnessColumns,
|
||||
"w-full pb-2 gap-3! items-center border-0 border-b border-solid border-hairline",
|
||||
)}
|
||||
>
|
||||
<Span className="text-[11px] uppercase tracking-wide text-ink-secondary">
|
||||
Kind
|
||||
</Span>
|
||||
<Span className="text-[11px] uppercase tracking-wide text-ink-secondary w-10 text-right">
|
||||
<Span className="text-[11px] uppercase tracking-wide text-ink-secondary text-right">
|
||||
Share
|
||||
</Span>
|
||||
<Span className="text-[11px] uppercase tracking-wide text-ink-secondary w-20 text-right">
|
||||
<Span className="text-[11px] uppercase tracking-wide text-ink-secondary text-right">
|
||||
Tokens
|
||||
</Span>
|
||||
<Span className="text-[11px] uppercase tracking-wide text-ink-secondary w-16 text-right">
|
||||
<Span className="text-[11px] uppercase tracking-wide text-ink-secondary text-right">
|
||||
Cost
|
||||
</Span>
|
||||
</Row>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import Row from "@moduletrace/twui/layout/Row";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import type { UsageOverview } from "@/src/types";
|
||||
import formatTokens from "@/src/utils/format-tokens";
|
||||
import { UsageHarnessTitles } from "../(data)/usage-harness-titles";
|
||||
import { UsageHarnessColumns } from "../(data)/usage-table-columns";
|
||||
|
||||
type Props = {
|
||||
row: UsageOverview["by_harness"][number];
|
||||
@@ -20,17 +22,22 @@ export default function UsageHarnessRow({ row, total_tokens }: Props) {
|
||||
const share = total_tokens ? Math.round((row.total_tokens / total_tokens) * 100) : 0;
|
||||
|
||||
return (
|
||||
<Row className="w-full py-2 gap-3! items-center border-0 border-b border-solid border-hairline last:border-b-0">
|
||||
<Span className="text-[13px] leading-5 text-ink grow min-w-0 truncate">
|
||||
<Row
|
||||
className={twMerge(
|
||||
UsageHarnessColumns,
|
||||
"w-full py-2 gap-3! items-center border-0 border-b border-solid border-hairline last:border-b-0",
|
||||
)}
|
||||
>
|
||||
<Span className="text-[13px] leading-5 text-ink min-w-0 truncate">
|
||||
{UsageHarnessTitles[row.harness] || row.harness}
|
||||
</Span>
|
||||
<Span className="text-[12px] leading-4 text-ink-secondary tabular-nums w-10 text-right">
|
||||
<Span className="text-[12px] leading-4 text-ink-secondary tabular-nums text-right truncate">
|
||||
{share}%
|
||||
</Span>
|
||||
<Span className="text-[13px] leading-5 text-ink tabular-nums w-20 text-right">
|
||||
<Span className="text-[13px] leading-5 text-ink tabular-nums text-right truncate">
|
||||
{formatTokens({ tokens: row.total_tokens })}
|
||||
</Span>
|
||||
<Span className="text-[12px] leading-4 text-ink-secondary tabular-nums w-16 text-right">
|
||||
<Span className="text-[12px] leading-4 text-ink-secondary tabular-nums text-right truncate">
|
||||
{row.cost ? `$${row.cost.toFixed(2)}` : "-"}
|
||||
</Span>
|
||||
</Row>
|
||||
|
||||
@@ -13,7 +13,7 @@ export default function UsageModelHeadCell({ title, className }: Props) {
|
||||
return (
|
||||
<Span
|
||||
className={twMerge(
|
||||
"text-[11px] uppercase tracking-wide text-ink-secondary text-right shrink-0",
|
||||
"text-[11px] uppercase tracking-wide text-ink-secondary text-right truncate",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import Row from "@moduletrace/twui/layout/Row";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import Stack from "@moduletrace/twui/layout/Stack";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import type { UsageOverview } from "@/src/types";
|
||||
import formatTokens from "@/src/utils/format-tokens";
|
||||
import { UsageHarnessTitles } from "../(data)/usage-harness-titles";
|
||||
import { UsageModelColumns } from "../(data)/usage-table-columns";
|
||||
|
||||
type Props = {
|
||||
row: UsageOverview["models"][number];
|
||||
@@ -20,8 +22,13 @@ export default function UsageModelRow({ row }: Props) {
|
||||
: "-";
|
||||
|
||||
return (
|
||||
<Row className="w-full py-2.5 gap-3! items-center border-0 border-b border-solid border-hairline last:border-b-0">
|
||||
<Stack className="grow min-w-0 gap-0!">
|
||||
<Row
|
||||
className={twMerge(
|
||||
UsageModelColumns,
|
||||
"w-full py-2.5 gap-3! items-center border-0 border-b border-solid border-hairline last:border-b-0",
|
||||
)}
|
||||
>
|
||||
<Stack className="min-w-0 gap-0! items-stretch">
|
||||
<Span className="text-[13px] leading-5 text-ink truncate" title={row.model_slug}>
|
||||
{row.model_name || row.model_slug}
|
||||
</Span>
|
||||
@@ -29,22 +36,22 @@ export default function UsageModelRow({ row }: Props) {
|
||||
{row.provider_name} - {UsageHarnessTitles[row.harness] || row.harness}
|
||||
</Span>
|
||||
</Stack>
|
||||
<Span className="text-[12px] leading-4 text-ink-secondary tabular-nums w-14 text-right">
|
||||
<Span className="text-[12px] leading-4 text-ink-secondary tabular-nums text-right truncate">
|
||||
{formatTokens({ tokens: row.calls })}
|
||||
</Span>
|
||||
<Span className="text-[12px] leading-4 text-ink-secondary tabular-nums w-16 text-right">
|
||||
<Span className="text-[12px] leading-4 text-ink-secondary tabular-nums text-right truncate">
|
||||
{formatTokens({ tokens: row.prompt_tokens })}
|
||||
</Span>
|
||||
<Span className="text-[12px] leading-4 text-ink-secondary tabular-nums w-16 text-right">
|
||||
<Span className="text-[12px] leading-4 text-ink-secondary tabular-nums text-right truncate">
|
||||
{formatTokens({ tokens: row.completion_tokens })}
|
||||
</Span>
|
||||
<Span className="text-[13px] leading-5 text-ink tabular-nums w-16 text-right">
|
||||
<Span className="text-[13px] leading-5 text-ink tabular-nums text-right truncate">
|
||||
{formatTokens({ tokens: row.total_tokens })}
|
||||
</Span>
|
||||
<Span className="text-[12px] leading-4 text-ink-secondary tabular-nums w-16 text-right">
|
||||
<Span className="text-[12px] leading-4 text-ink-secondary tabular-nums text-right truncate">
|
||||
{per_thousand}
|
||||
</Span>
|
||||
<Span className="text-[12px] leading-4 text-ink-secondary tabular-nums w-14 text-right">
|
||||
<Span className="text-[12px] leading-4 text-ink-secondary tabular-nums text-right truncate">
|
||||
{row.cost ? `$${row.cost.toFixed(2)}` : "-"}
|
||||
</Span>
|
||||
</Row>
|
||||
|
||||
@@ -3,7 +3,9 @@ import Row from "@moduletrace/twui/layout/Row";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import Stack from "@moduletrace/twui/layout/Stack";
|
||||
import type { UsageOverview } from "@/src/types";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import SettingsGroup from "@/src/pages/admin/settings/(partials)/settings-group";
|
||||
import { UsageModelColumns } from "../(data)/usage-table-columns";
|
||||
import UsageModelHeadCell from "./usage-model-head-cell";
|
||||
import UsageModelRow from "./usage-model-row";
|
||||
|
||||
@@ -34,14 +36,19 @@ export default function UsageModelsTable({ rows, days }: Props) {
|
||||
description={`The models this install ran in the last ${days} days, busiest first. The price per thousand tokens is read from what the provider reported.`}
|
||||
>
|
||||
<Stack className="w-full gap-0! overflow-x-auto">
|
||||
<Row className="w-full pb-2 gap-3! items-center border-0 border-b border-solid border-hairline min-w-[640px]">
|
||||
<UsageModelHeadCell className="grow text-left" title="Model" />
|
||||
<UsageModelHeadCell className="w-14" title="Calls" />
|
||||
<UsageModelHeadCell className="w-16" title="In" />
|
||||
<UsageModelHeadCell className="w-16" title="Out" />
|
||||
<UsageModelHeadCell className="w-16" title="Tokens" />
|
||||
<UsageModelHeadCell className="w-16" title="Per 1K" />
|
||||
<UsageModelHeadCell className="w-14" title="Cost" />
|
||||
<Row
|
||||
className={twMerge(
|
||||
UsageModelColumns,
|
||||
"w-full pb-2 gap-3! items-center border-0 border-b border-solid border-hairline",
|
||||
)}
|
||||
>
|
||||
<UsageModelHeadCell className="text-left" title="Model" />
|
||||
<UsageModelHeadCell title="Calls" />
|
||||
<UsageModelHeadCell title="In" />
|
||||
<UsageModelHeadCell title="Out" />
|
||||
<UsageModelHeadCell title="Tokens" />
|
||||
<UsageModelHeadCell title="Per 1K" />
|
||||
<UsageModelHeadCell title="Cost" />
|
||||
</Row>
|
||||
{rows.map((row) => (
|
||||
<UsageModelRow
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import Button from "@moduletrace/twui/layout/Button";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import type { UsageRange } from "../(data)/usage-ranges";
|
||||
|
||||
type Props = {
|
||||
range: UsageRange;
|
||||
selected: boolean;
|
||||
loading?: boolean;
|
||||
onChange: (days: number) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* One period of the range picker: ink-filled while it is the one showing,
|
||||
* quiet otherwise.
|
||||
*/
|
||||
export default function UsageRangeButton({ range, selected, loading, onChange }: Props) {
|
||||
return (
|
||||
<Button
|
||||
title={range.title}
|
||||
type="button"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
color="text"
|
||||
aria-pressed={selected}
|
||||
disabled={loading && !selected}
|
||||
onClick={() => {
|
||||
if (!selected) onChange(range.days);
|
||||
}}
|
||||
className={twMerge(
|
||||
"px-2.5! py-1! text-[12px]! rounded-default-xs",
|
||||
selected
|
||||
? "bg-ink dark:bg-ink hover:bg-ink dark:hover:bg-ink text-on-ink dark:text-on-ink cursor-default"
|
||||
: "text-ink-secondary dark:text-ink-secondary hover:bg-ink/5 dark:hover:bg-ink/5 hover:text-ink dark:hover:text-ink",
|
||||
)}
|
||||
>
|
||||
{range.title}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import Button from "@moduletrace/twui/layout/Button";
|
||||
import Row from "@moduletrace/twui/layout/Row";
|
||||
import { UsageRanges } from "../(data)/usage-ranges";
|
||||
import UsageRangeButton from "./usage-range-button";
|
||||
|
||||
type Props = {
|
||||
days: number;
|
||||
@@ -9,26 +9,20 @@ type Props = {
|
||||
};
|
||||
|
||||
/**
|
||||
* The periods the page totals, one button each. The one showing is the
|
||||
* one that reads as pressed.
|
||||
* The periods the page totals, one segment each in a hairline track. The
|
||||
* one showing is filled in ink, so it reads as pressed.
|
||||
*/
|
||||
export default function UsageRangePicker({ days, loading, onChange }: Props) {
|
||||
return (
|
||||
<Row className="gap-1! items-center">
|
||||
<Row className="gap-0.5! p-0.5 flex-nowrap items-center rounded-default border border-solid border-hairline">
|
||||
{UsageRanges.map((range) => (
|
||||
<Button
|
||||
<UsageRangeButton
|
||||
key={range.days}
|
||||
title={range.title}
|
||||
type="button"
|
||||
size="small"
|
||||
variant={range.days == days ? "outlined" : "ghost"}
|
||||
color="text"
|
||||
disabled={loading}
|
||||
onClick={() => onChange(range.days)}
|
||||
className="px-2.5! py-1! text-[12px]! rounded-default-xs"
|
||||
>
|
||||
{range.title}
|
||||
</Button>
|
||||
range={range}
|
||||
selected={range.days == days}
|
||||
loading={loading}
|
||||
onChange={onChange}
|
||||
/>
|
||||
))}
|
||||
</Row>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import Row from "@moduletrace/twui/layout/Row";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
|
||||
type Props = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* One figure of a day's summary card: what it is on the left, the figure
|
||||
* on the right.
|
||||
*/
|
||||
export default function UsageSummaryLine({ label, value }: Props) {
|
||||
return (
|
||||
<Row className="w-full gap-3! flex-nowrap items-center justify-between">
|
||||
<Span className="text-[12px] leading-4 text-ink-secondary">{label}</Span>
|
||||
<Span className="text-[12px] leading-4 text-ink tabular-nums">{value}</Span>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user