mirror of
https://github.com/vercel/workflow.git
synced 2026-09-14 19:59:43 +08:00
80595e6463
* 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:ba7353f1fcI, christopherkindl <53372002+christopherkindl@users.noreply.github.com>, hereby add my Signed-off-by to this commit:e83ca813b1I, christopherkindl <53372002+christopherkindl@users.noreply.github.com>, hereby add my Signed-off-by to this commit:8e69ff0d6bSigned-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:eefbc9744aSigned-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>
256 lines
7.5 KiB
TypeScript
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>
|
|
);
|
|
}
|