Files
vercel__workflow/docs/components/ui/gauge.tsx
christopherkindl 80595e6463 [home][docs] migrate to new geistdocs facelift (#1666)
* fix: migrate to new facelift

* add: max-w- for footer

* update

* DCO Remediation Commit for christopherkindl <53372002+christopherkindl@users.noreply.github.com>

I, christopherkindl <53372002+christopherkindl@users.noreply.github.com>, hereby add my Signed-off-by to this commit: ba7353f1fc
I, christopherkindl <53372002+christopherkindl@users.noreply.github.com>, hereby add my Signed-off-by to this commit: e83ca813b1
I, christopherkindl <53372002+christopherkindl@users.noreply.github.com>, hereby add my Signed-off-by to this commit: 8e69ff0d6b

Signed-off-by: christopherkindl <53372002+christopherkindl@users.noreply.github.com>

* Fix: Mobile menu's "close on route change" useEffect has empty dependency array [], making it a no-op that never fires on actual route changes.


This commit fixes the issue reported at docs/components/geistdocs/mobile-menu.tsx:80

## Bug Analysis

The `MobileMenu` component at `docs/components/geistdocs/mobile-menu.tsx` line 80 has a useEffect intended to close the menu on route changes:

```js
useEffect(() => {
  setShow(false);
}, []);
```

The empty dependency array `[]` means this effect runs exactly once — on initial mount — when `show` is already `false` (its initial state). This is a complete no-op. It never detects or responds to route changes.

**When this manifests:** If a user opens the mobile menu and then navigates via browser back/forward buttons, the menu remains open and `document.body.style.overflow` stays set to `'hidden'`, completely locking page scroll. While clicking the `NavLink` items within the menu closes it via their `onClick={close}` handlers, programmatic/browser-level navigation bypasses those handlers.

**Impact:** Users on mobile who use browser navigation while the menu is open will have a broken experience — the menu stays open overlaying the page and body scroll is locked.

## Fix

Replaced the empty-dependency useEffect with the correct pattern already demonstrated in the sibling `sidebar.tsx` component:

1.  Added `usePathname()` from `next/navigation` to track the current route.
2.  Added a `useRef` to store the previous pathname.
3.  Updated the useEffect to depend on `pathname`, comparing it to the previous value and closing the menu when a change is detected.

This ensures the menu closes on any route change, whether triggered by clicking a link, browser back/forward, or programmatic navigation.


Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: christopherkindl <christopher.kindl@gmail.com>

* fix(docs): use ds-gray-900 token for muted-foreground color

Replace hardcoded oklch values with var(--ds-gray-900) for both light
and dark mode to stay in sync with the design system.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: christopherkindl <53372002+christopherkindl@users.noreply.github.com>

* fix(docs): update world component colors to use 900-level tokens

Align icon and badge colors with design system by switching from 500/600
shades to 900-level variants across WorldDetailHero and
WorldTestingPerformance components.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: christopherkindl <53372002+christopherkindl@users.noreply.github.com>

* fix(docs): update world page colors and align hero layout with TOC grid

- Align hero quick links with TOC sidebar using matching grid layout
- Add shrink-0 to hero icons for consistent vertical alignment
- Update benchmark bar colors (green for fastest, blue for others)
- Remove extra px-4 padding from hero section

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: christopherkindl <53372002+christopherkindl@users.noreply.github.com>

* polish: /worlds page

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: christopherkindl <53372002+christopherkindl@users.noreply.github.com>

* feat(docs): add filterable world cards with section grouping

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: christopherkindl <53372002+christopherkindl@users.noreply.github.com>

* fix(docs): use radix checkbox with SSR placeholder to prevent layout shift

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: christopherkindl <53372002+christopherkindl@users.noreply.github.com>

* fix(docs): use badge tabs for world filters instead of checkboxes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: christopherkindl <53372002+christopherkindl@users.noreply.github.com>

* revert: restore .husky/pre-commit from main

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: christopherkindl <53372002+christopherkindl@users.noreply.github.com>

* fix(docs): improve benchmark history chart UI and API performance

Benchmark history dialog:
- Fix DialogContent accessibility warning (use DialogDescription)
- Fix 2MB unstable_cache error by using in-memory cache for snapshot map
- Add Geist-style underline variant to Tabs component
- Add skeleton loading state matching stat cards + chart layout
- Add Geist shimmer effect to Skeleton component (--ds-gray-100/200 gradient)
- Add --ds-shadow-tooltip token and apply to chart tooltip
- Use design tokens for all chart colors (--ds-blue-800, --ds-green-800, --ds-purple-900)
- Use linear interpolation, solid gridlines (--ds-gray-400), no animations
- Add 10% fill area under single-line charts, hide for multi-line
- Add white ring to chart dots, equidistant x-axis labels
- Stale-while-revalidate client cache for instant mode switching
- Stat cards refactored to use .map() over a data array
- Lock dialog height with min-h to prevent layout shift

API performance:
- Early exit on gh-pages pagination (cap at MAX_ITEMS * 3)
- Use snapshot timestamp for releases (eliminates 30 serial API calls)
- Increase fetch batch size from 10 to 30
- Remove unstable_cache, rely on in-memory + fetch-level caching

Other:
- Add recharts.d.ts type augmentation for React 19 compatibility

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: christopherkindl <53372002+christopherkindl@users.noreply.github.com>

* fix(docs): add missing geist tokens and utilities from homepage-visuals

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: christopherkindl <53372002+christopherkindl@users.noreply.github.com>

* [DO NOT MERGE][home] Adds vercel-com workflow visuals (#1674)

* add: visualizations

* DCO Remediation Commit for christopherkindl <53372002+christopherkindl@users.noreply.github.com>

I, christopherkindl <53372002+christopherkindl@users.noreply.github.com>, hereby add my Signed-off-by to this commit: eefbc9744a

Signed-off-by: christopherkindl <53372002+christopherkindl@users.noreply.github.com>

* fix: adjust bar border radius and add @container to feature cards

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: christopherkindl <53372002+christopherkindl@users.noreply.github.com>

* fix: use rounded-lg for bar border radius, add @container to wide card

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: christopherkindl <53372002+christopherkindl@users.noreply.github.com>

* revert: restore geistdocs.css from main

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: christopherkindl <53372002+christopherkindl@users.noreply.github.com>

---------

Signed-off-by: christopherkindl <53372002+christopherkindl@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(docs): add mobile docs bar to cookbook pages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: christopherkindl <53372002+christopherkindl@users.noreply.github.com>

* fix(docs): add mobile docs bar to cookbook pages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: christopherkindl <53372002+christopherkindl@users.noreply.github.com>

* fix(docs): increase hero section vertical spacing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: christopherkindl <53372002+christopherkindl@users.noreply.github.com>

* chore(docs): remove recharts React 19 type shim

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: christopherkindl <53372002+christopherkindl@users.noreply.github.com>

* fix(docs): hoist useTransform calls to top level of components

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: christopherkindl <53372002+christopherkindl@users.noreply.github.com>

* fix(docs): add optional chaining for world.features access

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: christopherkindl <53372002+christopherkindl@users.noreply.github.com>

* fix(docs): log warning when benchmark pagination cap is hit

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: christopherkindl <53372002+christopherkindl@users.noreply.github.com>

* fix(docs): make filter badges keyboard accessible

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: christopherkindl <53372002+christopherkindl@users.noreply.github.com>

* fix(docs): clean up setTimeout on unmount in globe path components

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: christopherkindl <53372002+christopherkindl@users.noreply.github.com>

* feat(docs): add full-width frameworks section to homepage

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: christopherkindl <53372002+christopherkindl@users.noreply.github.com>

* feat(docs): update navbar logo and add framework logos from geist symbols

- Replace LogoWorkflowSdk with Geist symbols wordmark (LogoWorkflow)
- Add currentColor framework logo icons from geistcn-assets
- Add full-width frameworks section to homepage with colored logos
- TanStack shown as "Coming soon" with analytics tracking

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: christopherkindl <53372002+christopherkindl@users.noreply.github.com>

* docs: add comment noting timestamp semantic change in benchmark API

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: christopherkindl <53372002+christopherkindl@users.noreply.github.com>

* fix(docs): move frameworks section after effortless setup on homepage

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: christopherkindl <53372002+christopherkindl@users.noreply.github.com>

---------

Signed-off-by: christopherkindl <53372002+christopherkindl@users.noreply.github.com>
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 13:36:44 +02:00

256 lines
7.5 KiB
TypeScript

import React, { useMemo, type JSX } from 'react';
import type { CSSProperties } from 'react';
import { cn } from '@/lib/utils';
type ColorObject =
| Record<number, string>
| {
primary: string;
secondary?: string;
};
const DEFAULT_COLORS: ColorObject = {
// Level: Poor
'0': 'var(--ds-red-800)',
// Level: Okay
'33': 'var(--ds-amber-700)',
// Level: Good
'66': 'var(--ds-green-700)',
};
export interface GaugeProps {
/** The value for the gauge. This needs to be a number between 0 and 100 representing 0% to 100%. */
value: number;
/** Determines whether the value is displayed as a number. */
showValue?: boolean;
/** Either color stops the gauge should be based on `value`, or static colors. */
colors?: ColorObject;
size: 'tiny' | 'small' | 'medium' | 'large';
/* Decides which arc should be prioritized. Use `equal` when displaying a ratio and `primary` when displaying a single percentage. `equal` leads to both arcs being the exact same size at 50. */
arcPriority?: 'equal' | 'primary';
placeholder?: boolean;
indeterminate?: boolean;
className?: string;
children?: React.ReactNode;
style?: CSSProperties;
}
const GaugeSizeMapping: Record<GaugeProps['size'], number> = {
tiny: 20,
small: 32,
medium: 64,
large: 128,
};
const GaugeGapMapping: Record<GaugeProps['size'], number> = {
tiny: 3,
small: 2,
medium: 1,
large: 1,
};
const FontConfigMapping: Record<
GaugeProps['size'],
{
size: number;
weight: number;
} | null
> = {
tiny: null,
small: {
size: 11,
weight: 500,
},
medium: {
size: 18,
weight: 500,
},
large: {
size: 32,
weight: 600,
},
};
const CIRCLE_SIZE = 100;
export function Gauge({
colors = DEFAULT_COLORS,
value,
placeholder: _placeholder,
indeterminate,
className,
arcPriority = 'primary',
style,
size: sizeLabel,
children,
showValue = false,
...props
}: GaugeProps): JSX.Element {
const placeholder = _placeholder || false;
const size = GaugeSizeMapping[sizeLabel];
const textConfig = FontConfigMapping[sizeLabel];
// Gets the color for the current value based on the color map.
const [primaryColor, secondaryColor] = useMemo<
Readonly<[string | undefined, string | undefined]>
>(() => {
if ('primary' in colors) {
return [colors.primary, colors.secondary] as const;
}
const stops = Object.keys(colors).map(Number);
if (stops.length === 0) return [undefined, undefined] as const;
const c = stops.filter((stop) => value >= stop).pop();
return [c !== undefined ? colors[c] : undefined, undefined] as const;
}, [colors, value]);
// Calculates arc & circle values based on the current size & value.
const arcValues = useMemo(() => {
const strokeWidth = size <= GaugeSizeMapping.tiny ? 15 : 10;
const circumference = 2 * Math.PI * (CIRCLE_SIZE / 2 - strokeWidth / 2);
const pxToPercent = CIRCLE_SIZE / circumference;
const circleProps: React.SVGProps<SVGCircleElement> = {
cx: CIRCLE_SIZE / 2,
cy: CIRCLE_SIZE / 2,
r: CIRCLE_SIZE / 2 - strokeWidth / 2,
strokeWidth,
strokeDashoffset: 0,
strokeLinecap: 'round',
strokeLinejoin: 'round',
};
const baseGap = Math.round(strokeWidth * pxToPercent);
let segmentGap = baseGap + GaugeGapMapping[sizeLabel];
if (value === 0 || value >= 100) {
segmentGap = 0;
}
const offsetFactor = arcPriority === 'equal' ? 0.5 : 0;
const primaryStrokePercent = value - segmentGap * 2 * offsetFactor;
const diffToOnePercent = Math.max(1 - primaryStrokePercent, 0);
const secondaryStrokePercent =
100 - value - segmentGap * 2 * (1 - offsetFactor) - diffToOnePercent;
return {
circleProps,
circumference,
segmentGap,
primaryStrokePercent,
secondaryStrokePercent,
offsetFactor,
};
}, [arcPriority, size, sizeLabel, value]);
const {
circleProps,
circumference,
offsetFactor,
segmentGap,
primaryStrokePercent,
secondaryStrokePercent,
} = arcValues;
return (
<div
aria-valuemax={100}
aria-valuemin={0}
aria-valuenow={indeterminate ? undefined : value}
className={cn(
'relative flex flex-col justify-center items-center [&_svg]:overflow-visible',
'[--transition-length:1s] [--transition-step:200ms] [--delay:0s] [--percent-to-deg:3.6deg] transform-gpu',
indeterminate ? '[&_circle]:!stroke-[var(--ds-gray-alpha-400)]' : '',
className
)}
data-geist-progress-circle=""
data-version="v1"
role="progressbar"
style={{
...style,
['--circle-size' as string]: `${CIRCLE_SIZE}px`,
['--circumference' as string]: circumference,
['--percent-to-px' as string]: `${circumference / 100}px`,
['--gap-percent' as string]: segmentGap,
['--offset-factor' as string]: offsetFactor,
}}
{...props}
>
<svg
aria-hidden
fill="none"
height={size}
strokeWidth="2"
viewBox={`0 0 ${CIRCLE_SIZE} ${CIRCLE_SIZE}`}
width={size}
>
<circle
style={{
opacity: secondaryStrokePercent < 0 || value === 100 ? 0 : 1,
['--stroke-percent' as string]: secondaryStrokePercent,
}}
{...circleProps}
className={cn(
!placeholder
? [
'[--offset-factor-secondary:calc(1-var(--offset-factor))]',
'[stroke-dasharray:calc(var(--stroke-percent)*var(--percent-to-px))_var(--circumference)]',
'[transform:rotate(calc(360deg-90deg-(var(--gap-percent)*var(--percent-to-deg)*var(--offset-factor-secondary))))_scaleY(-1)] [transform-origin:calc(var(--circle-size)/2)_calc(var(--circle-size)/2)]',
'[transition:all_var(--transition-length)_ease_var(--delay)]',
]
: '',
indeterminate ? '!opacity-0 [--stroke-percent:0_!important]' : ''
)}
stroke={secondaryColor || 'var(--ds-gray-alpha-400)'}
/>
<circle
{...circleProps}
className={cn(
!placeholder
? [
'[stroke-dasharray:calc(var(--stroke-percent)*var(--percent-to-px))_var(--circumference)]',
'[transition-property:stroke-dasharray,transform] [transition:var(--transition-length)_ease_var(--delay),stroke_var(--transition-length)_ease_var(--delay)]',
'[transform:rotate(calc(-90deg+var(--gap-percent)*var(--offset-factor)*var(--percent-to-deg)))] [transform-origin:calc(var(--circle-size)/2)_calc(var(--circle-size)/2)]',
]
: '',
indeterminate ? '[--stroke-percent:100_!important]' : ''
)}
data-geist-progress-circle-fg=""
stroke={primaryColor || 'var(--geist-foreground)'}
style={{
opacity: value === 0 ? 0 : 1,
['--stroke-percent' as string]: Math.min(
100 - segmentGap,
primaryStrokePercent
),
}}
/>
</svg>
{children || showValue ? (
<div aria-hidden className="flex absolute">
{children ||
(textConfig && (
<p
className="text-inherit"
style={{
fontSize: textConfig.size,
fontWeight: textConfig.weight,
}}
>
{value}
</p>
))}
</div>
) : null}
</div>
);
}