improve web ui

This commit is contained in:
2026-06-26 05:43:34 +00:00
parent 764c814483
commit 187738ed86
73 changed files with 3373 additions and 2847 deletions
+24 -21
View File
@@ -1,29 +1,32 @@
import * as React from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
import { cva, type VariantProps } from "class-variance-authority";
import type * as React from "react";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
'inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-medium transition-colors focus:outline-none',
{
variants: {
variant: {
default: 'border-transparent bg-primary text-primary-foreground',
secondary: 'border-transparent bg-secondary text-secondary-foreground',
destructive: 'border-transparent bg-destructive text-destructive-foreground',
success: 'border-transparent bg-[var(--success)] text-white',
outline: 'text-foreground',
},
},
defaultVariants: { variant: 'default' },
},
)
"inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-medium transition-colors focus:outline-none",
{
variants: {
variant: {
default: "border-transparent bg-primary text-primary-foreground",
secondary: "border-transparent bg-secondary text-secondary-foreground",
destructive:
"border-transparent bg-destructive text-destructive-foreground",
success: "border-transparent bg-[var(--success)] text-white",
outline: "text-foreground",
},
},
defaultVariants: { variant: "default" },
},
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
);
}
export { Badge, badgeVariants }
export { Badge, badgeVariants };
+5 -5
View File
@@ -1,12 +1,12 @@
import type { ComponentProps } from 'react'
import { AnimatedButton, buttonVariants } from '@unom/ui/button'
import { AnimatedButton, buttonVariants } from "@unom/ui/button";
import type { ComponentProps } from "react";
// The console's Button IS @unom/ui's animated button — pill shape, specular
// material gloss + UI click/hover sounds (enabled via UnomProviders), driven by
// the shared brand tokens. Same variant/size vocabulary the routes already use
// (default/destructive/outline/secondary/ghost/link + default/sm/lg/icon).
export type ButtonProps = ComponentProps<typeof AnimatedButton>
export type ButtonProps = ComponentProps<typeof AnimatedButton>;
export const Button = AnimatedButton
export const Button = AnimatedButton;
export { buttonVariants }
export { buttonVariants };
+79 -50
View File
@@ -1,7 +1,7 @@
import * as React from 'react'
import type { ComponentProps } from 'react'
import { AnimatedCard } from '@unom/ui/card'
import { cn } from '@/lib/utils'
import { AnimatedCard } from "@unom/ui/card";
import type { ComponentProps } from "react";
import * as React from "react";
import { cn } from "@/lib/utils";
// The console's Card IS @unom/ui's animated card — a `bg-neutral` (#1c1530)
// surface with a soft brand-violet ring, on-mount motion + material gloss
@@ -9,56 +9,85 @@ import { cn } from '@/lib/utils'
// API (CardHeader/Title/Description/Content/Footer own their own padding), so
// the card defaults to `padding={false}` to avoid doubling it, and soften the
// 2px ring to a subtle 1px brand tint.
type CardProps = ComponentProps<typeof AnimatedCard>
type CardProps = ComponentProps<typeof AnimatedCard>;
const Card = ({ className, padding = false, children, ...props }: CardProps) => (
<AnimatedCard
padding={padding}
className={cn('ring-1 ring-accent/40', className)}
{...props}
>
{children}
</AnimatedCard>
)
Card.displayName = 'Card'
const Card = ({
className,
padding = false,
children,
...props
}: CardProps) => (
<AnimatedCard
padding={padding}
className={cn("ring-1 ring-accent/40", className)}
{...props}
>
{children}
</AnimatedCard>
);
Card.displayName = "Card";
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
),
)
CardHeader.displayName = 'CardHeader'
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
));
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('font-semibold leading-none tracking-tight', className)}
{...props}
/>
),
)
CardTitle.displayName = 'CardTitle'
const CardTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
));
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
),
)
CardDescription.displayName = 'CardDescription'
const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
),
)
CardContent.displayName = 'CardContent'
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
));
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
),
)
CardFooter.displayName = 'CardFooter'
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
));
CardFooter.displayName = "CardFooter";
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
export {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
};
+1 -1
View File
@@ -1,3 +1,3 @@
// The console's Input IS @unom/ui's form input (shadcn-compatible tokens:
// border-input / muted-foreground / ring, material gloss via UnomProviders).
export { InputText as Input } from '@unom/ui/form/input-text'
export { InputText as Input } from "@unom/ui/form/input-text";
+1 -1
View File
@@ -1,2 +1,2 @@
// The console's Label IS @unom/ui's form label (radix-backed, text-main).
export { Label } from '@unom/ui/form/label'
export { Label } from "@unom/ui/form/label";
+77 -68
View File
@@ -1,6 +1,6 @@
import { useEffect, useRef } from 'react'
import { motion, useReducedMotion, useTime, useTransform } from 'motion/react'
import { cn } from '@/lib/utils'
import { motion, useReducedMotion, useTime, useTransform } from "motion/react";
import { useEffect, useRef } from "react";
import { cn } from "@/lib/utils";
// The punktfunk lens, alive. The two overlapping circles of the brand mark are
// recreated from divs and animated as if orbiting on a path whose long axis points
@@ -13,76 +13,85 @@ import { cn } from '@/lib/utils'
// both the scaling and the front/back swap. Honours prefers-reduced-motion.
// Size via className (e.g. `size-8`); geometry derives from the box.
const DURATION_MS = 1600
const R_DEPTH = 0.34 // depth amplitude (fraction of box) → the size change
const PERSP = 1.05 // perspective distance (fraction of box); smaller → stronger scaling
const R_PLANE_FIXED = 0.12 // constant in-plane offset → the two never fully eclipse
const R_PLANE_SWAY = 0.05 // small in-plane breathing
const DIAG: readonly [number, number] = [-Math.SQRT1_2, Math.SQRT1_2] // lens axis (↙ light / ↗ deep)
const LOBE_FRAC = 0.58 // circle diameter as a fraction of the box
const REST = 0 // reduced-motion: park flat (widest lens, no depth) = the brand mark
const DURATION_MS = 1600;
const R_DEPTH = 0.34; // depth amplitude (fraction of box) → the size change
const PERSP = 1.05; // perspective distance (fraction of box); smaller → stronger scaling
const R_PLANE_FIXED = 0.12; // constant in-plane offset → the two never fully eclipse
const R_PLANE_SWAY = 0.05; // small in-plane breathing
const DIAG: readonly [number, number] = [-Math.SQRT1_2, Math.SQRT1_2]; // lens axis (↙ light / ↗ deep)
const LOBE_FRAC = 0.58; // circle diameter as a fraction of the box
const REST = 0; // reduced-motion: park flat (widest lens, no depth) = the brand mark
export function Spinner({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
const reduce = useReducedMotion()
const ref = useRef<HTMLDivElement>(null)
const sizeRef = useRef(0)
const time = useTime()
export function Spinner({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
const reduce = useReducedMotion();
const ref = useRef<HTMLDivElement>(null);
const sizeRef = useRef(0);
const time = useTime();
useEffect(() => {
const el = ref.current
if (!el) return
sizeRef.current = el.clientWidth
const ro = new ResizeObserver((entries) => {
const w = entries[0]?.contentRect.width
if (w) sizeRef.current = w
})
ro.observe(el)
return () => ro.disconnect()
}, [])
useEffect(() => {
const el = ref.current;
if (!el) return;
sizeRef.current = el.clientWidth;
const ro = new ResizeObserver((entries) => {
const w = entries[0]?.contentRect.width;
if (w) sizeRef.current = w;
});
ro.observe(el);
return () => ro.disconnect();
}, []);
const angleAt = (t: number) => (reduce ? REST : (t / DURATION_MS) * Math.PI * 2)
const depthAt = (t: number, side: number) => side * Math.sin(angleAt(t)) * R_DEPTH
const angleAt = (t: number) =>
reduce ? REST : (t / DURATION_MS) * Math.PI * 2;
const depthAt = (t: number, side: number) =>
side * Math.sin(angleAt(t)) * R_DEPTH;
const transformAt = (t: number, side: number) => {
const s = sizeRef.current
const angle = angleAt(t)
const z = side * Math.sin(angle) * R_DEPTH // world depth (toward viewer = +)
const p = PERSP / (PERSP - z) // perspective: nearer → bigger, farther → smaller
const mag = (R_PLANE_FIXED + R_PLANE_SWAY * Math.cos(angle)) * side
const x = mag * DIAG[0] * p * s
const y = mag * DIAG[1] * p * s
return `translate(-50%, -50%) translate(${x}px, ${y}px) scale(${p})`
}
const transformAt = (t: number, side: number) => {
const s = sizeRef.current;
const angle = angleAt(t);
const z = side * Math.sin(angle) * R_DEPTH; // world depth (toward viewer = +)
const p = PERSP / (PERSP - z); // perspective: nearer → bigger, farther → smaller
const mag = (R_PLANE_FIXED + R_PLANE_SWAY * Math.cos(angle)) * side;
const x = mag * DIAG[0] * p * s;
const y = mag * DIAG[1] * p * s;
return `translate(-50%, -50%) translate(${x}px, ${y}px) scale(${p})`;
};
const tLight = useTransform(time, (t) => transformAt(t, 1))
const tDeep = useTransform(time, (t) => transformAt(t, -1))
// z-index follows depth, so whichever circle is nearer is painted on top.
const zLight = useTransform(time, (t) => Math.round(depthAt(t, 1) * 1000))
const zDeep = useTransform(time, (t) => Math.round(depthAt(t, -1) * 1000))
const tLight = useTransform(time, (t) => transformAt(t, 1));
const tDeep = useTransform(time, (t) => transformAt(t, -1));
// z-index follows depth, so whichever circle is nearer is painted on top.
const zLight = useTransform(time, (t) => Math.round(depthAt(t, 1) * 1000));
const zDeep = useTransform(time, (t) => Math.round(depthAt(t, -1) * 1000));
const lobe = (color: string): React.CSSProperties => ({
width: `${LOBE_FRAC * 100}%`,
height: `${LOBE_FRAC * 100}%`,
backgroundColor: color,
mixBlendMode: 'screen',
})
const lobe = (color: string): React.CSSProperties => ({
width: `${LOBE_FRAC * 100}%`,
height: `${LOBE_FRAC * 100}%`,
backgroundColor: color,
mixBlendMode: "screen",
});
return (
<div
ref={ref}
role="status"
aria-label="Loading"
className={cn('relative inline-block size-6 isolate', className)}
{...props}
>
<motion.div
className="absolute left-1/2 top-1/2 rounded-full"
style={{ ...lobe('var(--pf-brand-light)'), transform: tLight, zIndex: zLight }}
/>
<motion.div
className="absolute left-1/2 top-1/2 rounded-full"
style={{ ...lobe('var(--pf-brand)'), transform: tDeep, zIndex: zDeep }}
/>
</div>
)
return (
<div
ref={ref}
role="status"
aria-label="Loading"
className={cn("relative inline-block size-6 isolate", className)}
{...props}
>
<motion.div
className="absolute left-1/2 top-1/2 rounded-full"
style={{
...lobe("var(--pf-brand-light)"),
transform: tLight,
zIndex: zLight,
}}
/>
<motion.div
className="absolute left-1/2 top-1/2 rounded-full"
style={{ ...lobe("var(--pf-brand)"), transform: tDeep, zIndex: zDeep }}
/>
</div>
);
}
+65 -55
View File
@@ -1,70 +1,80 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
import * as React from "react";
import { cn } from "@/lib/utils";
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table ref={ref} className={cn('w-full caption-bottom text-sm', className)} {...props} />
</div>
),
)
Table.displayName = 'Table'
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
));
Table.displayName = "Table";
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
))
TableHeader.displayName = 'TableHeader'
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
));
TableHeader.displayName = "TableHeader";
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody ref={ref} className={cn('[&_tr:last-child]:border-0', className)} {...props} />
))
TableBody.displayName = 'TableBody'
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
));
TableBody.displayName = "TableBody";
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
'border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',
className,
)}
{...props}
/>
),
)
TableRow.displayName = 'TableRow'
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className,
)}
{...props}
/>
));
TableRow.displayName = "TableRow";
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
'h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0',
className,
)}
{...props}
/>
))
TableHead.displayName = 'TableHead'
<th
ref={ref}
className={cn(
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className,
)}
{...props}
/>
));
TableHead.displayName = "TableHead";
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn('p-2 align-middle [&:has([role=checkbox])]:pr-0', className)}
{...props}
/>
))
TableCell.displayName = 'TableCell'
<td
ref={ref}
className={cn("p-2 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
));
TableCell.displayName = "TableCell";
export { Table, TableHeader, TableBody, TableHead, TableRow, TableCell }
export { Table, TableBody, TableCell, TableHead, TableHeader, TableRow };