Refine root UI around shadcn primitives
CI / quality (push) Waiting to run

This commit is contained in:
MOH
2026-03-07 22:28:52 +01:00
parent 985fbe1745
commit a18370d003
36 changed files with 1344 additions and 597 deletions
+6 -6
View File
@@ -5,14 +5,14 @@ import { Card } from "@/components/ui/card";
import { cn } from "@/lib/utils";
const appCardVariants = cva(
"rounded-surface border transition-all duration-200",
"rounded-lg transition-colors",
{
variants: {
level: {
1: "border-border bg-surface-1 text-foreground shadow-card",
2: "border-border bg-surface-2 text-foreground shadow-sm",
3: "border-border/90 bg-surface-3 text-foreground shadow-panel",
inverse: "border-transparent bg-surface-inverse text-surface-inverse-foreground shadow-lg",
1: "border bg-card text-card-foreground shadow-sm",
2: "border bg-card text-card-foreground shadow-sm",
3: "border bg-card text-card-foreground shadow-md",
inverse: "border-transparent bg-foreground text-background shadow-sm",
},
padding: {
none: "",
@@ -21,7 +21,7 @@ const appCardVariants = cva(
lg: "p-8",
},
interactive: {
true: "hover:-translate-y-0.5 hover:border-border-strong hover:shadow-md",
true: "hover:bg-accent/30 hover:text-accent-foreground",
false: "",
},
},
+1 -1
View File
@@ -10,7 +10,7 @@ const badgeVariants = cva(
variant: {
default: "border-transparent bg-primary text-primary-foreground",
secondary: "border-transparent bg-secondary text-secondary-foreground",
outline: "border-border bg-surface-1 text-foreground",
outline: "border-input bg-background text-foreground",
success: "border-transparent bg-status-success-soft text-status-success",
warning: "border-transparent bg-status-warning-soft text-status-warning",
},
+5 -5
View File
@@ -5,16 +5,16 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-nested text-sm font-medium ring-offset-background transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground shadow-xs hover:brightness-95",
secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
outline: "border border-border bg-surface-1 text-foreground shadow-xs hover:border-border-strong hover:bg-surface-2",
default: "bg-primary text-primary-foreground hover:bg-primary/90",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
ghost: "text-muted-foreground hover:bg-muted hover:text-foreground",
link: "rounded-none text-primary underline-offset-4 hover:underline",
destructive: "bg-destructive text-destructive-foreground shadow-xs hover:brightness-95",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
},
size: {
default: "h-10 px-4 py-2",
+1 -1
View File
@@ -7,7 +7,7 @@ const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElemen
<div
ref={ref}
className={cn(
"rounded-surface border border-border bg-surface-1 text-card-foreground shadow-card",
"rounded-lg border bg-card text-card-foreground shadow-sm",
className,
)}
{...props}
+50
View File
@@ -0,0 +1,50 @@
"use client";
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { Check } from "lucide-react";
import { cn } from "@/lib/utils";
type CheckboxProps = React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> & {
name?: string;
};
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
CheckboxProps
>(({ className, name, checked, defaultChecked, onCheckedChange, ...props }, ref) => {
const [internalChecked, setInternalChecked] = React.useState(defaultChecked === true);
const isControlled = checked !== undefined;
const currentChecked = isControlled ? checked === true : internalChecked;
return (
<>
{name ? <input type="hidden" name={name} value={currentChecked ? "on" : "false"} /> : null}
<CheckboxPrimitive.Root
ref={ref}
checked={checked}
defaultChecked={defaultChecked}
onCheckedChange={(nextChecked) => {
if (!isControlled) {
setInternalChecked(nextChecked === true);
}
onCheckedChange?.(nextChecked);
}}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator className="flex items-center justify-center text-current">
<Check className="h-3.5 w-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
</>
);
});
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox };
+1 -1
View File
@@ -7,7 +7,7 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
<input
type={type}
className={cn(
"flex h-10 w-full rounded-nested border border-input bg-surface-1 px-3 py-2 text-sm text-foreground ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base text-foreground ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className,
)}
ref={ref}
+195
View File
@@ -0,0 +1,195 @@
"use client";
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { Check, ChevronDown, ChevronUp } from "lucide-react";
import { cn } from "@/lib/utils";
type SelectProps = React.ComponentPropsWithoutRef<typeof SelectPrimitive.Root> & {
name?: string;
required?: boolean;
};
function Select({
name,
required,
value,
defaultValue,
onValueChange,
children,
...props
}: SelectProps) {
const [internalValue, setInternalValue] = React.useState(defaultValue ?? "");
const isControlled = value !== undefined;
const currentValue = (isControlled ? value : internalValue) as string;
return (
<>
{name ? <input type="hidden" name={name} value={currentValue} required={required} /> : null}
<SelectPrimitive.Root
value={value}
defaultValue={defaultValue}
onValueChange={(nextValue) => {
if (!isControlled) {
setInternalValue(nextValue);
}
onValueChange?.(nextValue);
}}
{...props}
>
{children}
</SelectPrimitive.Root>
</>
);
}
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}
>
<ChevronUp className="h-4 w-4 rotate-180" />
</SelectPrimitive.ScrollDownButton>
));
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-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",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
};
+3 -3
View File
@@ -44,7 +44,7 @@ function TabsList({
return (
<div
className={cn(
"inline-flex h-auto flex-wrap items-center gap-2 rounded-surface border border-border bg-surface-2 p-1.5",
"inline-flex h-auto flex-wrap items-center gap-2 rounded-lg border bg-muted/40 p-1.5",
className,
)}
{...props}
@@ -69,8 +69,8 @@ function TabsTrigger({
<button
type="button"
className={cn(
"inline-flex items-center justify-center rounded-nested px-3 py-2 text-sm font-medium text-muted-foreground transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
isActive && "bg-surface-1 text-foreground shadow-xs",
"inline-flex items-center justify-center rounded-md px-3 py-2 text-sm font-medium text-muted-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
isActive && "bg-background text-foreground shadow-sm",
className,
)}
onClick={(event) => {
+1 -1
View File
@@ -6,7 +6,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, React.ComponentProps<"tex
({ className, ...props }, ref) => (
<textarea
className={cn(
"flex min-h-[112px] w-full rounded-nested border border-input bg-surface-1 px-3 py-2 text-sm text-foreground ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base text-foreground ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className,
)}
ref={ref}
+6 -6
View File
@@ -224,7 +224,7 @@ export function UiKitShowcase({ localeKey }: UiKitShowcaseProps) {
<div className="flex flex-col gap-section">
<AppCard level={3}>
<CardContent className="flex flex-col gap-4 p-6 lg:p-10">
<div className="inline-flex w-fit items-center gap-2 rounded-pill border border-border bg-surface-1 px-3 py-1 text-xs font-semibold uppercase tracking-[0.14em] text-foreground/80">
<div className="inline-flex w-fit items-center gap-2 rounded-full border border-input bg-background px-3 py-1 text-xs font-semibold uppercase tracking-[0.14em] text-foreground/80">
<Layers3 className="h-3.5 w-3.5 text-brand-secondary" />
{copy.title}
</div>
@@ -292,10 +292,10 @@ export function UiKitShowcase({ localeKey }: UiKitShowcaseProps) {
</AppCard>
<AppCard level="inverse">
<CardContent className="p-5">
<p className="text-sm font-medium text-surface-inverse-foreground/80">
<p className="text-sm font-medium text-background/80">
{copy.inverseOuterCard}
</p>
<p className="mt-2 text-sm text-surface-inverse-foreground/88">
<p className="mt-2 text-sm text-background/88">
{copy.inverseOuterCardText}
</p>
</CardContent>
@@ -452,8 +452,8 @@ export function UiKitShowcase({ localeKey }: UiKitShowcaseProps) {
</AppCard>
<AppCard level="inverse">
<CardContent className="p-5">
<p className="font-medium text-surface-inverse-foreground">{copy.inverse}</p>
<p className="mt-2 text-sm text-surface-inverse-foreground/88">{copy.inverseText}</p>
<p className="font-medium text-background">{copy.inverse}</p>
<p className="mt-2 text-sm text-background/88">{copy.inverseText}</p>
</CardContent>
</AppCard>
</div>
@@ -492,7 +492,7 @@ export function UiKitShowcase({ localeKey }: UiKitShowcaseProps) {
<div className="grid gap-6">
<AppCard level={3}>
<CardContent className="space-y-3 p-6">
<div className="inline-flex w-fit items-center gap-2 rounded-pill border border-border bg-surface-1 px-3 py-1 text-xs font-semibold uppercase tracking-[0.14em] text-foreground/80">
<div className="inline-flex w-fit items-center gap-2 rounded-full border border-input bg-background px-3 py-1 text-xs font-semibold uppercase tracking-[0.14em] text-foreground/80">
<Type className="h-3.5 w-3.5 text-brand-primary" />
{copy.pageHeader}
</div>