feat: integrate @number-flow/react for enhanced number formatting in stats page

- Added @number-flow/react as a dependency to improve number display.
- Replaced the custom formatCount function with NumberFlow for projectsScaffolded and modulesInstalled stats, ensuring better formatting and handling of loading states.
- Updated LastRefreshed component's styling for improved readability.
This commit is contained in:
2026-05-27 17:49:07 -04:00
parent 2de4b24fc9
commit f59cc4c195
5 changed files with 38 additions and 103 deletions
+1
View File
@@ -15,6 +15,7 @@
"@base-ui/react": "^1.5.0",
"@fontsource-variable/geist": "^5.2.9",
"@fontsource-variable/geist-mono": "^5.2.8",
"@number-flow/react": "^0.6.0",
"@orama/orama": "^3.1.18",
"@pierre/trees": "1.0.0-beta.4",
"@stanza/registry": "workspace:*",
+1 -1
View File
@@ -3,7 +3,7 @@ import type { ReactNode } from "react";
export function Section({ title, children }: { title: string; children: ReactNode }) {
return (
<section>
<h3 className="mb-2 text-xs font-medium tracking-tight text-muted-foreground">{title}</h3>
<h3 className="mb-2 text-[13px] font-medium tracking-tight text-muted-foreground">{title}</h3>
{children}
</section>
);
+1 -1
View File
@@ -84,7 +84,7 @@ function TooltipContent({
<Popup
data-slot="tooltip-content"
className={cn(
"relative z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 overflow-visible rounded-none bg-foreground px-2 py-1.5 text-xs text-background shadow-md selection:bg-background selection:text-foreground has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-none data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
"relative z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 overflow-visible rounded-none bg-foreground px-2 py-1 text-xs text-background shadow-md selection:bg-background selection:text-foreground has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-none data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className,
)}
{...props}
+5 -9
View File
@@ -1,3 +1,4 @@
import NumberFlow from "@number-flow/react";
import type { CategoryId, ModuleMetadata } from "@stanza/registry";
import { categoryLabel, KNOWN_CATEGORIES } from "@stanza/registry";
import { createFileRoute, Link, useLoaderData } from "@tanstack/react-router";
@@ -30,11 +31,6 @@ const percentFormatter = new Intl.NumberFormat("en-US", {
maximumFractionDigits: 0,
});
function formatCount(value: number): string {
if (value <= 0) return "—";
return numberFormatter.format(value);
}
function formatGeneratedAt(iso: string): string {
const date = new Date(iso);
if (Number.isNaN(date.getTime())) return iso;
@@ -48,14 +44,14 @@ function formatGeneratedAt(iso: string): string {
function LastRefreshed({ iso }: { iso: string }) {
const ago = useTimeAgo(iso);
return (
<p>
<p className="text-xs leading-4">
<Tooltip>
<TooltipTrigger
nativeButton={false}
render={
<time
dateTime={iso}
className="cursor-help font-mono text-xs text-muted-foreground/70 tabular-nums"
className="cursor-help font-mono text-muted-foreground/70 tabular-nums"
/>
}
>
@@ -122,7 +118,7 @@ function StatsPage() {
</CardHeader>
<CardContent>
<p className="font-mono text-4xl font-medium tracking-tight tabular-nums">
{formatCount(stats?.projectsScaffolded ?? 0)}
{isLoading ? "—" : <NumberFlow value={stats.projectsScaffolded} locales="en-US" />}
</p>
</CardContent>
</Card>
@@ -134,7 +130,7 @@ function StatsPage() {
</CardHeader>
<CardContent>
<p className="font-mono text-4xl font-medium tracking-tight tabular-nums">
{formatCount(stats?.modulesInstalled ?? 0)}
{isLoading ? "—" : <NumberFlow value={stats.modulesInstalled} locales="en-US" />}
</p>
</CardContent>
</Card>
+30 -92
View File
@@ -98,6 +98,9 @@ importers:
'@fontsource-variable/geist-mono':
specifier: ^5.2.8
version: 5.2.8
'@number-flow/react':
specifier: ^0.6.0
version: 0.6.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@orama/orama':
specifier: ^3.1.18
version: 3.1.18
@@ -242,10 +245,10 @@ importers:
version: '@voidzero-dev/vite-plus-core@0.1.22(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(typescript@6.0.3)'
vite-plus:
specifier: 'catalog:'
version: 0.1.22(@types/node@25.9.1)(@voidzero-dev/vite-plus-core@0.1.22(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(typescript@6.0.3))(esbuild@0.28.0)(jiti@2.7.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(typescript@6.0.3)
version: 0.1.22(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(typescript@6.0.3)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))
vitest:
specifier: npm:@voidzero-dev/vite-plus-test@^0.1.22
version: '@voidzero-dev/vite-plus-test@0.1.22(@types/node@25.9.1)(@voidzero-dev/vite-plus-core@0.1.22(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(typescript@6.0.3))(esbuild@0.28.0)(jiti@2.7.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(typescript@6.0.3)'
version: '@voidzero-dev/vite-plus-test@0.1.22(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(typescript@6.0.3)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))'
packages/codemods:
dependencies:
@@ -1154,6 +1157,12 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
'@number-flow/react@0.6.0':
resolution: {integrity: sha512-77Yfc9+zkV2UDSP8phhZzxJGuwxi/Tt1TikmipL+1r3e9GFKEYDZ1XwInj67NoSt3OnOB0KLvvcl3lfPZgBHVQ==}
peerDependencies:
react: ^18 || ^19
react-dom: ^18 || ^19
'@oozcitak/dom@2.0.2':
resolution: {integrity: sha512-GjpKhkSYC3Mj4+lfwEyI1dqnsKTgwGy48ytZEhm4A/xnH/8z9M3ZVXKr/YGQi3uCLs1AEBS+x5T2JPiueEDW8w==}
engines: {node: '>=20.0'}
@@ -3240,6 +3249,9 @@ packages:
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
engines: {node: '>=12'}
esm-env@1.2.2:
resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==}
esprima@4.0.1:
resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
engines: {node: '>=4'}
@@ -4333,6 +4345,9 @@ packages:
nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
number-flow@0.6.0:
resolution: {integrity: sha512-K8flNq2Wqus53vjp/btVo3qXFkagF8dIdYavreBfE7hlvFFG/b1HMGEH6nZL+mlrJ+4lbLP9OmPv3t2rmRkpSQ==}
object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
@@ -6243,6 +6258,13 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.20.1
'@number-flow/react@0.6.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
dependencies:
esm-env: 1.2.2
number-flow: 0.6.0
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
'@oozcitak/dom@2.0.2':
dependencies:
'@oozcitak/infra': 2.0.2
@@ -7338,47 +7360,6 @@ snapshots:
'@voidzero-dev/vite-plus-linux-x64-musl@0.1.22':
optional: true
'@voidzero-dev/vite-plus-test@0.1.22(@types/node@25.9.1)(@voidzero-dev/vite-plus-core@0.1.22(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(typescript@6.0.3))(esbuild@0.28.0)(jiti@2.7.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(typescript@6.0.3)':
dependencies:
'@standard-schema/spec': 1.1.0
'@types/chai': 5.2.3
'@voidzero-dev/vite-plus-core': 0.1.22(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(typescript@6.0.3)
es-module-lexer: 1.7.0
obug: 2.1.1
pixelmatch: 7.2.0
pngjs: 7.0.0
sirv: 3.0.2
std-env: 4.1.0
tinybench: 2.9.0
tinyexec: 1.2.2
tinyglobby: 0.2.16
vite: '@voidzero-dev/vite-plus-core@0.1.22(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(typescript@6.0.3)'
ws: 8.21.0
optionalDependencies:
'@types/node': 25.9.1
jsdom: 29.1.1(@noble/hashes@1.8.0)
transitivePeerDependencies:
- '@arethetypeswrong/core'
- '@tsdown/css'
- '@tsdown/exe'
- '@vitejs/devtools'
- bufferutil
- esbuild
- jiti
- less
- publint
- sass
- sass-embedded
- stylus
- sugarss
- terser
- tsx
- typescript
- unplugin-unused
- unrun
- utf-8-validate
- yaml
'@voidzero-dev/vite-plus-test@0.1.22(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(typescript@6.0.3)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))':
dependencies:
'@standard-schema/spec': 1.1.0
@@ -7924,6 +7905,8 @@ snapshots:
escape-string-regexp@5.0.0: {}
esm-env@1.2.2: {}
esprima@4.0.1: {}
estree-util-attach-comments@3.0.0:
@@ -9298,6 +9281,10 @@ snapshots:
dependencies:
boolbase: 1.0.0
number-flow@0.6.0:
dependencies:
esm-env: 1.2.2
object-assign@4.1.1: {}
object-inspect@1.13.4: {}
@@ -10374,55 +10361,6 @@ snapshots:
d3-time: 3.1.0
d3-timer: 3.0.1
vite-plus@0.1.22(@types/node@25.9.1)(@voidzero-dev/vite-plus-core@0.1.22(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(typescript@6.0.3))(esbuild@0.28.0)(jiti@2.7.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(typescript@6.0.3):
dependencies:
'@oxc-project/types': 0.129.0
'@oxlint/plugins': 1.61.0
'@voidzero-dev/vite-plus-core': 0.1.22(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(typescript@6.0.3)
'@voidzero-dev/vite-plus-test': 0.1.22(@types/node@25.9.1)(@voidzero-dev/vite-plus-core@0.1.22(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(typescript@6.0.3))(esbuild@0.28.0)(jiti@2.7.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(typescript@6.0.3)
oxfmt: 0.48.0
oxlint: 1.63.0(oxlint-tsgolint@0.22.1)
oxlint-tsgolint: 0.22.1
optionalDependencies:
'@voidzero-dev/vite-plus-darwin-arm64': 0.1.22
'@voidzero-dev/vite-plus-darwin-x64': 0.1.22
'@voidzero-dev/vite-plus-linux-arm64-gnu': 0.1.22
'@voidzero-dev/vite-plus-linux-arm64-musl': 0.1.22
'@voidzero-dev/vite-plus-linux-x64-gnu': 0.1.22
'@voidzero-dev/vite-plus-linux-x64-musl': 0.1.22
'@voidzero-dev/vite-plus-win32-arm64-msvc': 0.1.22
'@voidzero-dev/vite-plus-win32-x64-msvc': 0.1.22
transitivePeerDependencies:
- '@arethetypeswrong/core'
- '@edge-runtime/vm'
- '@opentelemetry/api'
- '@tsdown/css'
- '@tsdown/exe'
- '@types/node'
- '@vitejs/devtools'
- '@vitest/coverage-istanbul'
- '@vitest/coverage-v8'
- '@vitest/ui'
- bufferutil
- esbuild
- happy-dom
- jiti
- jsdom
- less
- publint
- sass
- sass-embedded
- stylus
- sugarss
- terser
- tsx
- typescript
- unplugin-unused
- unrun
- utf-8-validate
- vite
- yaml
vite-plus@0.1.22(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(typescript@6.0.3)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)):
dependencies:
'@oxc-project/types': 0.129.0