visual upgrade
ci / build-and-design (push) Failing after 13s

This commit is contained in:
Jason Stedwell
2026-07-23 04:23:19 -05:00
parent 9d7f593307
commit 822d8e41e6
53 changed files with 7757 additions and 285 deletions
+2 -2
View File
@@ -11,11 +11,11 @@ const buttonVariants = cva(
{
variants: {
variant: {
primary: "bg-brand text-bg hover:bg-brand-bright shadow-sm hover:shadow-glow",
primary: "bg-brand text-brand-foreground hover:bg-brand-bright shadow-sm hover:shadow-glow",
secondary: "bg-surface-2 text-text hover:bg-surface-2/70 shadow-sm",
outline: "border border-border bg-transparent text-text hover:bg-surface-2/60",
ghost: "bg-transparent text-text hover:bg-surface-2/60",
danger: "bg-danger text-bg hover:bg-danger/90 shadow-sm",
danger: "bg-danger text-danger-foreground hover:bg-danger/90 shadow-sm",
link: "text-brand underline-offset-4 hover:underline",
},
size: {
+27
View File
@@ -0,0 +1,27 @@
"use client";
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { Check } from "lucide-react";
import { cn } from "../lib/cn";
export const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer size-4 shrink-0 rounded-sm border border-border bg-bg/40 shadow-sm",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/60",
"disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-brand data-[state=checked]:bg-brand data-[state=checked]:text-brand-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator className="flex items-center justify-center">
<Check className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = "Checkbox";
+1 -1
View File
@@ -32,7 +32,7 @@ export const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-1/2 top-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2",
"fixed left-1/2 top-1/2 z-50 w-[calc(100%-2rem)] max-w-lg -translate-x-1/2 -translate-y-1/2",
"rounded-lg border border-border bg-surface p-6 shadow-lg",
"data-[state=open]:animate-scale-in",
className
+50
View File
@@ -0,0 +1,50 @@
import * as React from "react";
import { cn } from "../lib/cn";
import { Label } from "./label";
export interface FormFieldProps {
label: React.ReactNode;
children: React.ReactElement<{
id?: string;
"aria-describedby"?: string;
"aria-invalid"?: boolean;
}>;
id?: string;
description?: React.ReactNode;
error?: React.ReactNode;
required?: boolean;
className?: string;
}
/** Associates a control with its label, supporting text, and validation error. */
export function FormField({
label,
children,
id,
description,
error,
required,
className,
}: FormFieldProps) {
const generatedId = React.useId();
const controlId = id ?? children.props.id ?? `field-${generatedId}`;
const descriptionId = description ? `${controlId}-description` : undefined;
const errorId = error ? `${controlId}-error` : undefined;
const describedBy = [children.props["aria-describedby"], descriptionId, errorId].filter(Boolean).join(" ") || undefined;
return (
<div className={cn("space-y-1.5", className)}>
<Label htmlFor={controlId} className="mb-0 text-sm text-text">
{label}
{required && <span className="ml-1 text-danger" aria-hidden>*</span>}
</Label>
{React.cloneElement(children, {
id: controlId,
"aria-describedby": describedBy,
"aria-invalid": Boolean(error) || children.props["aria-invalid"],
})}
{description && !error && <p id={descriptionId} className="text-xs text-muted">{description}</p>}
{error && <p id={errorId} className="text-xs font-medium text-danger">{error}</p>}
</div>
);
}
+50
View File
@@ -0,0 +1,50 @@
"use client";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { Button } from "./button";
import { cn } from "../lib/cn";
export interface PaginationProps {
page: number;
pageCount: number;
onPageChange: (page: number) => void;
className?: string;
label?: string;
}
export function Pagination({
page,
pageCount,
onPageChange,
className,
label = "Pagination",
}: PaginationProps) {
const current = Math.min(Math.max(1, page), Math.max(1, pageCount));
return (
<nav aria-label={label} className={cn("flex items-center justify-between gap-3", className)}>
<Button
type="button"
variant="outline"
size="sm"
disabled={current <= 1}
onClick={() => onPageChange(current - 1)}
>
<ChevronLeft />
Previous
</Button>
<span className="text-sm text-muted" aria-live="polite">
Page <strong className="font-semibold text-text">{current}</strong> of {Math.max(1, pageCount)}
</span>
<Button
type="button"
variant="outline"
size="sm"
disabled={current >= pageCount}
onClick={() => onPageChange(current + 1)}
>
Next
<ChevronRight />
</Button>
</nav>
);
}
Executable → Regular
+102 -63
View File
@@ -1,23 +1,11 @@
"use client";
import * as React from "react";
import * as RS from "@radix-ui/react-select";
import { Check, ChevronDown } from "lucide-react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "../lib/cn";
/**
* Themed Select — a drop-in for a native <select>, but the open menu is a fully
* styleable popover (dark/gold), unlike the OS-drawn native dropdown.
*
* Keeps the native API on purpose so it swaps 1:1 with existing code:
* <Select value={x} onChange={(e) => setX(e.target.value)}>
* <option value="">None</option>
* <option value="a">A</option>
* </Select>
*
* Radix forbids empty-string item values, but apps use <option value=""> for
* real, selectable choices (e.g. "No procedure"). We map "" to a sentinel
* internally and translate back on change, so empty options stay selectable.
*/
const selectVariants = cva(
"inline-flex w-full items-center justify-between gap-2 rounded border border-border bg-bg/40 text-text " +
"transition-shadow duration-base ease-out focus:outline-none focus-visible:ring-2 focus-visible:ring-ring/60 " +
@@ -29,61 +17,101 @@ const selectVariants = cva(
);
const EMPTY = "__ui_select_empty__";
const toRadix = (v?: string) => (v === "" ? EMPTY : v);
const fromRadix = (v: string) => (v === EMPTY ? "" : v);
type OptionData = { value: string; label: React.ReactNode; disabled?: boolean };
type OptionGroup = { label?: React.ReactNode; options: OptionData[] };
function collectOptions(children: React.ReactNode): OptionData[] {
const out: OptionData[] = [];
const walk = (nodes: React.ReactNode) =>
React.Children.forEach(nodes, (child) => {
if (!React.isValidElement(child)) return;
if (child.type === "option") {
const p = child.props as { value?: string | number; children?: React.ReactNode; disabled?: boolean };
out.push({ value: String(p.value ?? ""), label: p.children, disabled: p.disabled });
} else if (child.type === "optgroup") {
walk((child.props as { children?: React.ReactNode }).children);
}
});
walk(children);
return out;
function collectOptions(children: React.ReactNode): OptionGroup[] {
const groups: OptionGroup[] = [];
const ungrouped: OptionData[] = [];
React.Children.forEach(children, (child) => {
if (!React.isValidElement(child)) return;
if (child.type === "option") {
const props = child.props as { value?: string | number; children?: React.ReactNode; disabled?: boolean };
ungrouped.push({ value: String(props.value ?? ""), label: props.children, disabled: props.disabled });
return;
}
if (child.type === "optgroup") {
const props = child.props as { label?: React.ReactNode; children?: React.ReactNode };
const options: OptionData[] = [];
React.Children.forEach(props.children, (option) => {
if (!React.isValidElement(option) || option.type !== "option") return;
const optionProps = option.props as { value?: string | number; children?: React.ReactNode; disabled?: boolean };
options.push({
value: String(optionProps.value ?? ""),
label: optionProps.children,
disabled: optionProps.disabled,
});
});
groups.push({ label: props.label, options });
}
});
return ungrouped.length ? [{ options: ungrouped }, ...groups] : groups;
}
export interface SelectProps extends VariantProps<typeof selectVariants> {
type RootProps = React.ComponentPropsWithoutRef<typeof RS.Root>;
type TriggerProps = React.ComponentPropsWithoutRef<typeof RS.Trigger>;
export interface SelectProps
extends VariantProps<typeof selectVariants>,
Omit<RootProps, "children" | "value" | "defaultValue" | "onValueChange"> {
value?: string;
defaultValue?: string;
onChange?: (e: { target: { value: string } }) => void;
onValueChange?: (value: string) => void;
disabled?: boolean;
name?: string;
placeholder?: string;
/** Compatibility callback for native-select migrations. Prefer onValueChange. */
onChange?: (event: { target: { value: string } }) => void;
placeholder?: React.ReactNode;
className?: string;
contentClassName?: string;
triggerProps?: Omit<TriggerProps, "children" | "className">;
children?: React.ReactNode;
"aria-label"?: string;
id?: string;
}
export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(
(
{ value, defaultValue, onChange, onValueChange, disabled, name, placeholder, size, className, children, id, ...rest },
{
value,
defaultValue,
onChange,
onValueChange,
placeholder,
size,
className,
contentClassName,
triggerProps,
children,
id,
...rootProps
},
ref
) => {
const options = collectOptions(children);
const handle = (v: string) => {
const real = fromRadix(v);
onValueChange?.(real);
onChange?.({ target: { value: real } });
const groups = collectOptions(children);
const values = groups.flatMap((group) => group.options.map((option) => option.value));
const emptySentinel = values.includes(EMPTY) ? `${EMPTY}_fallback` : EMPTY;
const toRadix = (next?: string) => (next === "" ? emptySentinel : next);
const fromRadix = (next: string) => (next === emptySentinel ? "" : next);
const handleValueChange = (next: string) => {
const realValue = fromRadix(next);
onValueChange?.(realValue);
onChange?.({ target: { value: realValue } });
};
return (
<RS.Root
{...rootProps}
value={value === undefined ? undefined : toRadix(value)}
defaultValue={defaultValue === undefined ? undefined : toRadix(defaultValue)}
onValueChange={handle}
disabled={disabled}
name={name}
onValueChange={handleValueChange}
>
<RS.Trigger ref={ref} id={id} aria-label={rest["aria-label"]} className={cn(selectVariants({ size }), className)}>
<RS.Trigger
{...triggerProps}
ref={ref}
id={id}
className={cn(selectVariants({ size }), className)}
>
<span className="min-w-0 truncate text-left">
<RS.Value placeholder={placeholder} />
</span>
@@ -97,26 +125,36 @@ export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(
sideOffset={6}
className={cn(
"z-50 max-h-[min(24rem,var(--radix-select-content-available-height))] min-w-[var(--radix-select-trigger-width)]",
"overflow-hidden rounded-md border border-border bg-surface shadow-lg data-[state=open]:animate-scale-in"
"overflow-hidden rounded-md border border-border bg-surface shadow-lg data-[state=open]:animate-scale-in",
contentClassName
)}
>
<RS.Viewport className="p-1">
{options.map((o, i) => (
<RS.Item
key={`${o.value}-${i}`}
value={toRadix(o.value)!}
disabled={o.disabled}
className={cn(
"relative flex cursor-pointer select-none items-center rounded px-2 py-1.5 pr-8 text-sm text-text outline-none",
"data-[highlighted]:bg-brand/15 data-[highlighted]:text-brand",
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
{groups.map((group, groupIndex) => (
<RS.Group key={groupIndex}>
{group.label && (
<RS.Label className="px-2 py-1.5 text-xs font-semibold uppercase tracking-wide text-muted">
{group.label}
</RS.Label>
)}
>
<RS.ItemText>{o.label}</RS.ItemText>
<RS.ItemIndicator className="absolute right-2 inline-flex">
<Check className="size-4" />
</RS.ItemIndicator>
</RS.Item>
{group.options.map((option, optionIndex) => (
<RS.Item
key={`${option.value}-${optionIndex}`}
value={toRadix(option.value)!}
disabled={option.disabled}
className={cn(
"relative flex cursor-pointer select-none items-center rounded px-2 py-1.5 pr-8 text-sm text-text outline-none",
"data-[highlighted]:bg-brand/15 data-[highlighted]:text-brand",
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
)}
>
<RS.ItemText>{option.label}</RS.ItemText>
<RS.ItemIndicator className="absolute right-2 inline-flex">
<Check className="size-4" />
</RS.ItemIndicator>
</RS.Item>
))}
</RS.Group>
))}
</RS.Viewport>
</RS.Content>
@@ -126,4 +164,5 @@ export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(
}
);
Select.displayName = "Select";
export { selectVariants };
+16
View File
@@ -0,0 +1,16 @@
import * as React from "react";
import { cn } from "../lib/cn";
export function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
aria-hidden
className={cn(
"animate-pulse rounded bg-surface-2",
"motion-reduce:animate-none",
className
)}
{...props}
/>
);
}
+24
View File
@@ -0,0 +1,24 @@
"use client";
import * as React from "react";
import * as SwitchPrimitive from "@radix-ui/react-switch";
import { cn } from "../lib/cn";
export const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitive.Root
ref={ref}
className={cn(
"inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border border-border bg-surface-2 p-0.5 transition-colors",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/60 disabled:cursor-not-allowed disabled:opacity-50",
"data-[state=checked]:border-brand data-[state=checked]:bg-brand",
className
)}
{...props}
>
<SwitchPrimitive.Thumb className="block size-5 rounded-full bg-text shadow-sm transition-transform data-[state=checked]:translate-x-5 data-[state=checked]:bg-brand-foreground" />
</SwitchPrimitive.Root>
));
Switch.displayName = "Switch";
+40
View File
@@ -0,0 +1,40 @@
"use client";
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "../lib/cn";
export const Tabs = TabsPrimitive.Root;
export const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List ref={ref} className={cn("inline-flex min-h-10 items-center rounded bg-surface-2 p-1", className)} {...props} />
));
TabsList.displayName = "TabsList";
export const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex min-h-8 items-center justify-center rounded-sm px-3 text-sm font-medium text-muted transition-colors",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/60 disabled:pointer-events-none disabled:opacity-50",
"data-[state=active]:bg-surface data-[state=active]:text-text data-[state=active]:shadow-sm",
className
)}
{...props}
/>
));
TabsTrigger.displayName = "TabsTrigger";
export const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content ref={ref} className={cn("mt-4 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/60", className)} {...props} />
));
TabsContent.displayName = "TabsContent";
+21 -15
View File
@@ -1,20 +1,26 @@
import { Toaster as Sonner, toast } from "sonner";
import { useOptionalTheme } from "../lib/theme";
/** Pre-themed sonner Toaster. Drop <Toaster /> once near your app root. */
export const Toaster = (props: React.ComponentProps<typeof Sonner>) => (
<Sonner
theme="dark"
position="bottom-right"
toastOptions={{
classNames: {
toast: "!bg-surface !border-border !text-text !rounded-lg !shadow-lg",
description: "!text-muted",
actionButton: "!bg-brand !text-bg",
cancelButton: "!bg-surface-2 !text-muted",
},
}}
{...props}
/>
);
export const Toaster = ({ toastOptions, ...props }: React.ComponentProps<typeof Sonner>) => {
const theme = useOptionalTheme();
return (
<Sonner
theme={theme?.resolvedTheme ?? "system"}
position="bottom-right"
toastOptions={{
...toastOptions,
classNames: {
toast: "!bg-surface !border-border !text-text !rounded-lg !shadow-lg",
description: "!text-muted",
actionButton: "!bg-brand !text-brand-foreground",
cancelButton: "!bg-surface-2 !text-muted",
...toastOptions?.classNames,
},
}}
{...props}
/>
);
};
export { toast };
+28
View File
@@ -0,0 +1,28 @@
"use client";
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "../lib/cn";
export const TooltipProvider = TooltipPrimitive.Provider;
export const Tooltip = TooltipPrimitive.Root;
export const TooltipTrigger = TooltipPrimitive.Trigger;
export const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 6, ...props }, ref) => (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 max-w-xs rounded bg-text px-2.5 py-1.5 text-xs font-medium text-bg shadow-md",
"data-[state=delayed-open]:animate-fade-in",
className
)}
{...props}
/>
</TooltipPrimitive.Portal>
));
TooltipContent.displayName = "TooltipContent";