feat(web): unify console + docs on @unom/ui; host OpenAPI via Scalar

Move the management console (web/) off shadcn/ui to the shared @unom/ui
design system the marketing site + docs are built on, on the punktfunk
violet brand over dark chrome:

- Add @unom/ui/@unom/style/motion/radix-ui/zod + Geist; web/.npmrc maps the
  @unom scope (packages are public-read, so CI needs no npm auth).
- styles.css: one dark-violet palette (#141019/#1c1530, brand #6c5bf3 ->
  #a79ff8) exposed under BOTH the shadcn token names the routes use and
  @unom/ui's contract, so routes + components both resolve; pulls in
  @unom/ui's material gloss + easings.
- components/ui/* now back onto @unom/ui (AnimatedButton/InputText/Label/
  AnimatedCard); brand-mark/wordmark/logo replace the generic Radio icon in
  the shell + login.
- MaterialProvider (specular gloss) at the root. No UI sounds, like the site.

docs-site: new /api route renders the host management REST API as an
interactive Scalar reference (reads public/openapi.json, a snapshot of
docs/api/openapi.json), branded violet and linked from the top nav, the
docs sidebar, the landing page, and host-cli.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-21 12:00:46 +00:00
parent 43b480ada3
commit 56f724815e
26 changed files with 3409 additions and 155 deletions
+9 -38
View File
@@ -1,41 +1,12 @@
import * as React from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
import type { ComponentProps } from 'react'
import { AnimatedButton, buttonVariants } from '@unom/ui/button'
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 cursor-pointer",
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
destructive:
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline:
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9',
},
},
defaultVariants: { variant: 'default', size: 'default' },
},
)
// 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 interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
export const Button = AnimatedButton
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, ...props }, ref) => (
<button ref={ref} className={cn(buttonVariants({ variant, size, className }))} {...props} />
),
)
Button.displayName = 'Button'
export { Button, buttonVariants }
export { buttonVariants }
+23 -9
View File
@@ -1,14 +1,24 @@
import * as React from 'react'
import type { ComponentProps } from 'react'
import { AnimatedCard } from '@unom/ui/card'
import { cn } from '@/lib/utils'
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('rounded-xl border bg-card text-card-foreground shadow', className)}
{...props}
/>
),
// 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
// (enabled via UnomProviders). We keep the composed shadcn-style sub-component
// 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>
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'
@@ -21,7 +31,11 @@ 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} />
<div
ref={ref}
className={cn('font-semibold leading-none tracking-tight', className)}
{...props}
/>
),
)
CardTitle.displayName = 'CardTitle'
+3 -19
View File
@@ -1,19 +1,3 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
({ className, type, ...props }, ref) => (
<input
type={type}
ref={ref}
className={cn(
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
{...props}
/>
),
)
Input.displayName = 'Input'
export { Input }
// 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'
+2 -18
View File
@@ -1,18 +1,2 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
const Label = React.forwardRef<HTMLLabelElement, React.LabelHTMLAttributes<HTMLLabelElement>>(
({ className, ...props }, ref) => (
<label
ref={ref}
className={cn(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
className,
)}
{...props}
/>
),
)
Label.displayName = 'Label'
export { Label }
// The console's Label IS @unom/ui's form label (radix-backed, text-main).
export { Label } from '@unom/ui/form/label'