SnowLuma v2.4.2 source snapshot
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run

This commit is contained in:
TanTan committed 2026-11-13 09:02:00 +08:00
commit fa9c73363d
1367 files changed
+355735

No files matched your search

+89
View File
@@ -0,0 +1,89 @@
{
"name": "@snowluma/ui",
"author": {
"name": "MotricSeven",
"email": "motricseven@gmail.com"
},
"version": "2.4.2",
"description": "Shared SnowLuma UI primitives — Tailwind 4 theme tokens + shadcn-style atomic components",
"keywords": [
"snowluma",
"ui",
"shadcn",
"tailwind",
"react"
],
"homepage": "https://github.com/SnowLuma/SnowLuma#readme",
"bugs": {
"url": "https://github.com/SnowLuma/SnowLuma/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/SnowLuma/SnowLuma.git",
"directory": "packages/ui"
},
"license": "SEE LICENSE IN LICENSE",
"type": "module",
"sideEffects": [
"**/*.css"
],
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./styles/theme.css": "./src/styles/theme.css",
"./package.json": "./package.json"
},
"files": [
"dist",
"src/styles"
],
"scripts": {
"clean": "node -e \"const fs = require('node:fs'); fs.rmSync('dist', { recursive: true, force: true })\"",
"prebuild": "pnpm run clean",
"build": "tsup",
"dev": "tsup --watch",
"prepack": "pnpm run build",
"typecheck": "tsc --noEmit"
},
"publishConfig": {
"access": "public"
},
"engines": {
"node": "^24.22.1 || >=26.12.1"
},
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"dependencies": {
"@radix-ui/react-alert-dialog": "^1.1.7",
"@radix-ui/react-checkbox": "^1.1.5",
"@radix-ui/react-dialog": "^1.1.7",
"@radix-ui/react-label": "^2.1.3",
"@radix-ui/react-popover": "^1.1.7",
"@radix-ui/react-scroll-area": "^1.2.4",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.3",
"@radix-ui/react-slot": "^1.1.3",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.1.9",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.469.0",
"sonner": "^2.0.6",
"tailwind-merge": "^3.5.0"
},
"devDependencies": {
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"tsup": "^8.5.0",
"typescript": "catalog:"
}
}
+113
View File
@@ -0,0 +1,113 @@
import * as React from 'react';
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
import { cn } from '../lib/cn';
import { buttonVariants } from './button';
const AlertDialog = AlertDialogPrimitive.Root;
const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
const AlertDialogPortal = AlertDialogPrimitive.Portal;
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
ref={ref}
className={cn(
'fixed inset-0 z-50 bg-background/60 backdrop-blur-sm',
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className,
)}
{...props}
/>
));
AlertDialogOverlay.displayName = 'AlertDialogOverlay';
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-1/2 top-1/2 z-50 grid w-[calc(100%-2rem)] max-w-lg grid-cols-[minmax(0,1fr)] -translate-x-1/2 -translate-y-1/2 gap-5 rounded-2xl border border-border bg-popover p-6 text-popover-foreground shadow-xl ring-1 ring-foreground/5',
'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',
className,
)}
{...props}
/>
</AlertDialogPortal>
));
AlertDialogContent.displayName = 'AlertDialogContent';
const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn('flex flex-col gap-1.5 text-left', className)} {...props} />
);
const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
{...props}
/>
);
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn('text-base font-semibold leading-tight tracking-tight', className)}
{...props}
/>
));
AlertDialogTitle.displayName = 'AlertDialogTitle';
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn('text-sm leading-relaxed text-muted-foreground', className)}
{...props}
/>
));
AlertDialogDescription.displayName = 'AlertDialogDescription';
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
));
AlertDialogAction.displayName = 'AlertDialogAction';
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(buttonVariants({ variant: 'outline' }), 'mt-0', className)}
{...props}
/>
));
AlertDialogCancel.displayName = 'AlertDialogCancel';
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
};
+50
View File
@@ -0,0 +1,50 @@
import * as React from 'react';
import { cn } from '../lib/cn';
interface AvatarProps extends React.HTMLAttributes<HTMLDivElement> {
size?: number;
}
const Avatar = React.forwardRef<HTMLDivElement, AvatarProps>(
({ className, size = 36, style, ...props }, ref) => (
<div
ref={ref}
className={cn(
'relative inline-flex shrink-0 overflow-hidden rounded-full bg-muted',
className,
)}
style={{ width: size, height: size, ...style }}
{...props}
/>
),
);
Avatar.displayName = 'Avatar';
const AvatarImage = React.forwardRef<HTMLImageElement, React.ImgHTMLAttributes<HTMLImageElement>>(
({ className, alt = '', ...props }, ref) => (
<img
ref={ref}
alt={alt}
className={cn('aspect-square size-full object-cover', className)}
referrerPolicy="no-referrer"
{...props}
/>
),
);
AvatarImage.displayName = 'AvatarImage';
const AvatarFallback = React.forwardRef<HTMLSpanElement, React.HTMLAttributes<HTMLSpanElement>>(
({ className, ...props }, ref) => (
<span
ref={ref}
className={cn(
'flex size-full items-center justify-center bg-muted text-xs font-medium text-muted-foreground',
className,
)}
{...props}
/>
),
);
AvatarFallback.displayName = 'AvatarFallback';
export { Avatar, AvatarImage, AvatarFallback };
+34
View File
@@ -0,0 +1,34 @@
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '../lib/cn';
export const badgeVariants = cva(
'inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[11px] font-medium leading-none transition-colors',
{
variants: {
variant: {
default: 'border-transparent bg-primary/15 text-primary',
secondary: 'border-transparent bg-secondary text-secondary-foreground',
destructive: 'border-transparent bg-destructive/15 text-destructive',
success: 'border-transparent bg-success/15 text-success',
warning: 'border-transparent bg-warning/15 text-warning',
info: 'border-transparent bg-info/15 text-info',
outline: 'border-border bg-card text-foreground',
soft: 'border-transparent bg-muted text-muted-foreground',
},
size: {
default: 'px-2 py-0.5 text-[11px]',
sm: 'px-1.5 py-px text-[10px]',
lg: 'px-2.5 py-1 text-xs',
},
},
defaultVariants: { variant: 'default', size: 'default' },
},
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLSpanElement>, VariantProps<typeof badgeVariants> {}
export function Badge({ className, variant, size, ...props }: BadgeProps) {
return <span className={cn(badgeVariants({ variant, size }), className)} {...props} />;
}
+60
View File
@@ -0,0 +1,60 @@
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '../lib/cn';
export const buttonVariants = cva(
[
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium',
'transition-[color,background-color,border-color,box-shadow,transform] duration-150 ease-out',
'disabled:pointer-events-none disabled:opacity-50',
"[&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0",
'outline-none focus-visible:ring-[3px] focus-visible:ring-ring/40 focus-visible:border-ring',
'active:translate-y-px cursor-pointer',
].join(' '),
{
variants: {
variant: {
default:
'bg-primary text-primary-foreground shadow-sm hover:bg-primary/90 active:bg-primary/95',
destructive:
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90 active:bg-destructive/95',
outline:
'border border-border bg-card text-foreground shadow-xs hover:bg-accent hover:text-accent-foreground hover:border-border/80',
secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
ghost: 'text-foreground hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline shadow-none',
success: 'bg-success text-success-foreground shadow-sm hover:bg-success/90',
warning: 'bg-warning text-warning-foreground shadow-sm hover:bg-warning/90',
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-lg px-6',
icon: 'size-9 rounded-lg',
'icon-sm': 'size-8 rounded-md',
'icon-xs': 'size-7 rounded-md',
},
},
defaultVariants: { variant: 'default', size: 'default' },
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
className={cn(buttonVariants({ variant, size }), className)}
ref={ref}
{...props}
/>
);
},
);
Button.displayName = 'Button';
+61
View File
@@ -0,0 +1,61 @@
import * as React from 'react';
import { cn } from '../lib/cn';
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
'rounded-xl border border-border bg-card text-card-foreground shadow-sm',
className,
)}
{...props}
/>
),
);
Card.displayName = 'Card';
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex flex-col gap-1.5 p-5', className)} {...props} />
),
);
CardHeader.displayName = 'CardHeader';
const CardTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('text-base font-semibold leading-tight 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 leading-relaxed 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-5 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 gap-2 p-5 pt-0', className)} {...props} />
),
);
CardFooter.displayName = 'CardFooter';
export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter };
+34
View File
@@ -0,0 +1,34 @@
import * as React from 'react';
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
import { Check, Minus } from 'lucide-react';
import { cn } from '../lib/cn';
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-[5px] border border-border bg-card shadow-xs transition-colors',
'hover:border-border/80',
'focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/40',
'disabled:cursor-not-allowed disabled:opacity-50',
'data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary',
'data-[state=indeterminate]:bg-primary data-[state=indeterminate]:text-primary-foreground data-[state=indeterminate]:border-primary',
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator className="flex items-center justify-center text-current">
{props.checked === 'indeterminate' ? (
<Minus className="size-3" />
) : (
<Check className="size-3" />
)}
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = 'Checkbox';
export { Checkbox };
+105
View File
@@ -0,0 +1,105 @@
import * as React from 'react';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { X } from 'lucide-react';
import { cn } from '../lib/cn';
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
'fixed inset-0 z-50 bg-background/60 backdrop-blur-sm',
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className,
)}
{...props}
/>
));
DialogOverlay.displayName = 'DialogOverlay';
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & { showClose?: boolean }
>(({ className, children, showClose = true, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
// grid-cols-[minmax(0,1fr)] keeps wide children (long select text,
// file paths, etc.) from blowing past the dialog's max-w-lg by
// forcing the implicit grid track's `min-width: auto` to 0.
'fixed left-1/2 top-1/2 z-50 grid w-[calc(100%-2rem)] max-w-lg grid-cols-[minmax(0,1fr)] -translate-x-1/2 -translate-y-1/2 gap-5 rounded-2xl border border-border bg-popover p-6 text-popover-foreground shadow-xl ring-1 ring-foreground/5',
'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-[state=open]:slide-in-from-top-2',
className,
)}
{...props}
>
{children}
{showClose && (
<DialogPrimitive.Close className="absolute right-4 top-4 inline-flex size-7 items-center justify-center rounded-md text-muted-foreground opacity-80 transition hover:bg-muted hover:opacity-100 focus:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/40">
<X className="size-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = 'DialogContent';
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn('flex flex-col gap-1.5 text-left', className)} {...props} />
);
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
{...props}
/>
);
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn('text-base font-semibold leading-tight tracking-tight', className)}
{...props}
/>
));
DialogTitle.displayName = 'DialogTitle';
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn('text-sm leading-relaxed text-muted-foreground', className)}
{...props}
/>
));
DialogDescription.displayName = 'DialogDescription';
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};
@@ -0,0 +1,45 @@
import * as React from 'react';
import { cn } from '../lib/cn';
interface EmptyStateProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {
icon?: React.ReactNode;
title: React.ReactNode;
description?: React.ReactNode;
action?: React.ReactNode;
}
/**
* Generic "nothing here yet" placeholder. Used by Bot list, search-with-no-
* results, log filters that filtered everything out, etc.
*/
export function EmptyState({
icon,
title,
description,
action,
className,
...props
}: EmptyStateProps) {
return (
<div
className={cn(
'flex flex-col items-center justify-center gap-3 rounded-xl border border-dashed border-border bg-card/40 px-6 py-12 text-center',
className,
)}
{...props}
>
{icon && (
<div className="flex size-12 items-center justify-center rounded-full bg-muted text-muted-foreground">
{icon}
</div>
)}
<div className="space-y-1">
<h3 className="text-sm font-medium text-foreground">{title}</h3>
{description && (
<p className="text-xs leading-relaxed text-muted-foreground">{description}</p>
)}
</div>
{action && <div className="pt-1">{action}</div>}
</div>
);
}
+25
View File
@@ -0,0 +1,25 @@
import * as React from 'react';
import { cn } from '../lib/cn';
const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
({ className, type, ...props }, ref) => (
<input
type={type}
ref={ref}
className={cn(
'flex h-9 w-full min-w-0 rounded-lg border border-border bg-card px-3 py-1 text-sm text-foreground shadow-xs transition-[color,box-shadow,border-color,background-color] outline-none',
'file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium',
'placeholder:text-muted-foreground',
'hover:border-border/80',
'focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/40',
'disabled:cursor-not-allowed disabled:opacity-50',
'aria-invalid:border-destructive aria-invalid:ring-destructive/20',
className,
)}
{...props}
/>
),
);
Input.displayName = 'Input';
export { Input };
+17
View File
@@ -0,0 +1,17 @@
import * as React from 'react';
import { cn } from '../lib/cn';
/** Small inline keyboard-key label, e.g. `<Kbd>⌘K</Kbd>`. */
export const Kbd = React.forwardRef<HTMLElement, React.HTMLAttributes<HTMLElement>>(
({ className, ...props }, ref) => (
<kbd
ref={ref}
className={cn(
'inline-flex h-5 min-w-[1.25rem] items-center justify-center rounded border border-border bg-muted px-1 font-mono text-[10px] font-medium text-muted-foreground',
className,
)}
{...props}
/>
),
);
Kbd.displayName = 'Kbd';
+20
View File
@@ -0,0 +1,20 @@
import * as React from 'react';
import * as LabelPrimitive from '@radix-ui/react-label';
import { cn } from '../lib/cn';
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(
'text-xs font-medium leading-none text-muted-foreground select-none',
className,
)}
{...props}
/>
));
Label.displayName = 'Label';
export { Label };
+31
View File
@@ -0,0 +1,31 @@
import * as React from 'react';
import * as PopoverPrimitive from '@radix-ui/react-popover';
import { cn } from '../lib/cn';
const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverAnchor = PopoverPrimitive.Anchor;
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = 'center', sideOffset = 6, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
'z-50 w-72 rounded-xl border border-border bg-popover p-4 text-popover-foreground shadow-xl ring-1 ring-foreground/5 outline-none',
'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',
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
));
PopoverContent.displayName = 'PopoverContent';
export { Popover, PopoverTrigger, PopoverAnchor, PopoverContent };
+54
View File
@@ -0,0 +1,54 @@
import * as React from 'react';
import { cn } from '../lib/cn';
interface ProgressProps extends React.HTMLAttributes<HTMLDivElement> {
value?: number;
indicatorClassName?: string;
/**
* When true, the bar shows an animated marquee instead of a fixed
* width — useful when we know a job is running but don't yet know
* the total size (e.g. a download whose Content-Length is unknown).
*/
indeterminate?: boolean;
}
const Progress = React.forwardRef<HTMLDivElement, ProgressProps>(
({ className, value = 0, indicatorClassName, indeterminate = false, ...props }, ref) => {
const clamped = Math.max(0, Math.min(100, value));
return (
<div
ref={ref}
role="progressbar"
aria-valuenow={indeterminate ? undefined : clamped}
aria-valuemin={0}
aria-valuemax={100}
className={cn(
'relative h-2 w-full overflow-hidden rounded-full bg-muted',
className,
)}
{...props}
>
{indeterminate ? (
<div
className={cn(
'absolute inset-y-0 left-0 w-1/3 rounded-full bg-primary',
'animate-[progress-marquee_1.4s_ease-in-out_infinite]',
indicatorClassName,
)}
/>
) : (
<div
className={cn(
'h-full rounded-full bg-primary transition-[width] duration-200 ease-out',
indicatorClassName,
)}
style={{ width: `${clamped}%` }}
/>
)}
</div>
);
},
);
Progress.displayName = 'Progress';
export { Progress };
@@ -0,0 +1,51 @@
import * as React from 'react';
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
import { cn } from '../lib/cn';
interface ScrollAreaProps extends React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> {
viewportClassName?: string;
}
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
ScrollAreaProps
>(({ className, children, viewportClassName, type = 'hover', ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
type={type}
className={cn('relative overflow-hidden', className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
className={cn('size-full rounded-[inherit] [&>div]:!block', viewportClassName)}
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollBar orientation="horizontal" />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
));
ScrollArea.displayName = 'ScrollArea';
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = 'vertical', ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
'flex touch-none select-none p-0.5 transition-colors',
orientation === 'vertical' && 'h-full w-2.5 border-l border-l-transparent',
orientation === 'horizontal' && 'h-2.5 w-full flex-col border-t border-t-transparent',
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border transition-colors hover:bg-muted-foreground/50" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
));
ScrollBar.displayName = 'ScrollBar';
export { ScrollArea, ScrollBar };
+171
View File
@@ -0,0 +1,171 @@
import * as React from 'react';
import * as SelectPrimitive from '@radix-ui/react-select';
import { Check, ChevronDown, ChevronUp } from 'lucide-react';
import { cn } from '../lib/cn';
const Select = 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(
'group flex h-9 w-full min-w-0 items-center justify-between gap-2 rounded-lg border border-border bg-card px-3 py-1 text-sm shadow-xs outline-none transition-[color,box-shadow,background-color,border-color]',
'hover:border-border/80 hover:bg-card/70',
'placeholder:text-muted-foreground',
'focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/40',
'data-[state=open]:border-ring data-[state=open]:ring-[3px] data-[state=open]:ring-ring/40',
'disabled:cursor-not-allowed disabled:opacity-50',
'data-[placeholder]:text-muted-foreground',
// Selected text mustn't push the chevron out of the trigger.
'[&>span:not([data-radix-select-icon])]:flex-1 [&>span:not([data-radix-select-icon])]:min-w-0 [&>span:not([data-radix-select-icon])]:truncate [&>span:not([data-radix-select-icon])]:text-left',
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild data-radix-select-icon>
<ChevronDown className="size-4 shrink-0 opacity-60 transition-transform group-data-[state=open]:rotate-180" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = 'SelectTrigger';
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 text-muted-foreground',
className,
)}
{...props}
>
<ChevronUp className="size-4" />
</SelectPrimitive.ScrollUpButton>
));
SelectScrollUpButton.displayName = 'SelectScrollUpButton';
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 text-muted-foreground',
className,
)}
{...props}
>
<ChevronDown className="size-4" />
</SelectPrimitive.ScrollDownButton>
));
SelectScrollDownButton.displayName = 'SelectScrollDownButton';
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}
position={position}
sideOffset={4}
className={cn(
// Solid popover surface — explicit bg + ring for visibility against any background.
'relative z-50 max-h-(--radix-select-content-available-height) min-w-(--radix-select-trigger-width) overflow-hidden rounded-xl border border-border bg-popover text-popover-foreground shadow-xl ring-1 ring-foreground/5',
'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',
position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=top]:-translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1',
className,
)}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
'p-1',
position === 'popper' &&
'h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width)',
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = 'SelectContent';
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn(
'px-2 py-1.5 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground',
className,
)}
{...props}
/>
));
SelectLabel.displayName = 'SelectLabel';
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 gap-2 rounded-md py-1.5 pl-2 pr-8 text-sm outline-none transition-colors',
'focus:bg-accent focus:text-accent-foreground',
'data-[state=checked]:font-medium',
'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className,
)}
{...props}
>
<span className="absolute right-2 flex size-3.5 items-center justify-center text-primary">
<SelectPrimitive.ItemIndicator>
<Check className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = 'SelectItem';
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-border', className)}
{...props}
/>
));
SelectSeparator.displayName = 'SelectSeparator';
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
};
+23
View File
@@ -0,0 +1,23 @@
import * as React from 'react';
import * as SeparatorPrimitive from '@radix-ui/react-separator';
import { cn } from '../lib/cn';
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
'shrink-0 bg-border',
orientation === 'horizontal' ? 'h-px w-full' : 'h-full w-px',
className,
)}
{...props}
/>
));
Separator.displayName = 'Separator';
export { Separator };
+72
View File
@@ -0,0 +1,72 @@
import * as React from 'react';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { X } from 'lucide-react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '../lib/cn';
const Sheet = DialogPrimitive.Root;
const SheetTrigger = DialogPrimitive.Trigger;
const SheetClose = DialogPrimitive.Close;
const SheetPortal = DialogPrimitive.Portal;
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
'fixed inset-0 z-50 bg-black/50 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className,
)}
{...props}
/>
));
SheetOverlay.displayName = 'SheetOverlay';
const sheetVariants = cva(
'fixed z-50 gap-4 bg-card p-0 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500 outline-none',
{
variants: {
side: {
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
bottom: 'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
left: 'inset-y-0 left-0 h-full w-3/4 max-w-xs border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
right: 'inset-y-0 right-0 h-full w-3/4 max-w-xs border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
},
},
defaultVariants: { side: 'right' },
},
);
interface SheetContentProps
extends
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>,
VariantProps<typeof sheetVariants> {
showClose?: boolean;
}
const SheetContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
SheetContentProps
>(({ side = 'right', className, children, showClose = true, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
{children}
{showClose && (
<DialogPrimitive.Close className="absolute right-4 top-4 inline-flex size-7 items-center justify-center rounded-md opacity-70 transition-opacity hover:opacity-100 outline-none focus-visible:ring-[3px] focus-visible:ring-ring/40 cursor-pointer">
<X className="size-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</SheetPortal>
));
SheetContent.displayName = 'SheetContent';
export { Sheet, SheetTrigger, SheetClose, SheetContent, SheetPortal, SheetOverlay };
+6
View File
@@ -0,0 +1,6 @@
import * as React from 'react';
import { cn } from '../lib/cn';
export function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn('animate-pulse rounded-md bg-muted', className)} {...props} />;
}
+49
View File
@@ -0,0 +1,49 @@
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '../lib/cn';
/**
* A semantic colored status dot — drop-in replacement for ad-hoc emoji
* (🟢 / 🟡 / 🔴) so we keep visual rhythm with the rest of the Lucide
* iconography. `pulse` adds a soft halo for the "active" state.
*/
const dotVariants = cva('inline-block shrink-0 rounded-full', {
variants: {
tone: {
success: 'bg-success',
warning: 'bg-warning',
destructive: 'bg-destructive',
info: 'bg-info',
muted: 'bg-muted-foreground/50',
primary: 'bg-primary',
},
size: {
xs: 'size-1.5',
sm: 'size-2',
md: 'size-2.5',
lg: 'size-3',
},
},
defaultVariants: { tone: 'muted', size: 'sm' },
});
export interface StatusDotProps
extends React.HTMLAttributes<HTMLSpanElement>, VariantProps<typeof dotVariants> {
pulse?: boolean;
}
export function StatusDot({ className, tone, size, pulse, ...props }: StatusDotProps) {
return (
<span
className={cn(
'relative inline-flex items-center justify-center',
pulse &&
'before:absolute before:inset-0 before:rounded-full before:bg-current before:opacity-30 before:animate-ping',
className,
)}
{...props}
>
<span className={cn(dotVariants({ tone, size }), 'relative')} aria-hidden />
</span>
);
}
+35
View File
@@ -0,0 +1,35 @@
import * as React from 'react';
import * as SwitchPrimitive from '@radix-ui/react-switch';
import { cn } from '../lib/cn';
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitive.Root
ref={ref}
className={cn(
'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-border shadow-inner transition-colors',
'focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/40',
'disabled:cursor-not-allowed disabled:opacity-50',
// Checked: filled with primary
'data-[state=checked]:bg-primary data-[state=checked]:border-primary',
// Unchecked: visible track that contrasts with both surface modes
'data-[state=unchecked]:bg-input dark:data-[state=unchecked]:bg-muted-foreground/25',
className,
)}
{...props}
>
<SwitchPrimitive.Thumb
className={cn(
'pointer-events-none block size-4 rounded-full shadow-md ring-0 transition-transform',
// Thumb always uses card surface so it pops against the track in both states.
'bg-card dark:bg-foreground/95',
'data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0',
)}
/>
</SwitchPrimitive.Root>
));
Switch.displayName = 'Switch';
export { Switch };
+48
View File
@@ -0,0 +1,48 @@
import * as React from 'react';
import * as TabsPrimitive from '@radix-ui/react-tabs';
import { cn } from '../lib/cn';
const Tabs = TabsPrimitive.Root;
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 h-9 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground',
className,
)}
{...props}
/>
));
TabsList.displayName = 'TabsList';
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 items-center justify-center whitespace-nowrap rounded-sm px-3 py-1 text-sm font-medium transition-all',
'focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/40',
'disabled:pointer-events-none disabled:opacity-50',
'data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm',
className,
)}
{...props}
/>
));
TabsTrigger.displayName = 'TabsTrigger';
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content ref={ref} className={cn('mt-2 outline-none', className)} {...props} />
));
TabsContent.displayName = 'TabsContent';
export { Tabs, TabsList, TabsTrigger, TabsContent };
+30
View File
@@ -0,0 +1,30 @@
import { Toaster as Sonner, toast, type ToasterProps } from 'sonner';
/**
* SnowLuma-themed toaster. Uses the Tailwind `--*-foreground / --background`
* tokens defined in `@snowluma/ui/styles/theme.css` so toasts match the
* surrounding chrome in both light and dark mode.
*/
export function Toaster(props: ToasterProps) {
return (
<Sonner
position="bottom-right"
richColors
closeButton
theme="system"
toastOptions={{
classNames: {
toast: 'group toast group-[.toaster]:bg-card group-[.toaster]:text-card-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
description: 'group-[.toast]:text-muted-foreground',
actionButton:
'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
cancelButton: 'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',
},
}}
{...props}
/>
);
}
export { toast };
export type { ToasterProps };
+29
View File
@@ -0,0 +1,29 @@
import * as React from 'react';
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
import { cn } from '../lib/cn';
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger;
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 overflow-hidden rounded-md border border-border bg-popover px-2.5 py-1.5 text-xs text-popover-foreground shadow-lg ring-1 ring-foreground/5',
'data-[state=delayed-open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=delayed-open]:fade-in-0',
'data-[state=delayed-open]:zoom-in-95 data-[state=closed]:zoom-out-95',
className,
)}
{...props}
/>
</TooltipPrimitive.Portal>
));
TooltipContent.displayName = 'TooltipContent';
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
+72
View File
@@ -0,0 +1,72 @@
export { cn } from './lib/cn';
export { Button, buttonVariants, type ButtonProps } from './components/button';
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
} from './components/alert-dialog';
export { Avatar, AvatarImage, AvatarFallback } from './components/avatar';
export { Badge, badgeVariants, type BadgeProps } from './components/badge';
export {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
CardFooter,
} from './components/card';
export { Checkbox } from './components/checkbox';
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
} from './components/dialog';
export { Input } from './components/input';
export { Label } from './components/label';
export { Progress } from './components/progress';
export { ScrollArea, ScrollBar } from './components/scroll-area';
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
} from './components/select';
export { Separator } from './components/separator';
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetPortal,
SheetOverlay,
} from './components/sheet';
export { Skeleton } from './components/skeleton';
export { Switch } from './components/switch';
export { Tabs, TabsList, TabsTrigger, TabsContent } from './components/tabs';
export { Toaster, toast, type ToasterProps } from './components/toaster';
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from './components/tooltip';
export { Popover, PopoverTrigger, PopoverAnchor, PopoverContent } from './components/popover';
export { Kbd } from './components/kbd';
export { StatusDot, type StatusDotProps } from './components/status-dot';
export { EmptyState } from './components/empty-state';
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]): string {
return twMerge(clsx(inputs));
}
+259
View File
@@ -0,0 +1,259 @@
/**
* @snowluma/ui — Tailwind 4 theme tokens.
*
* Consumer setup:
* @import "tailwindcss";
* @import "tw-animate-css";
* @import "@snowluma/ui/styles/theme.css";
* @source "../node_modules/@snowluma/ui/dist/**\/*.js";
*/
@custom-variant dark (&:where(.dark, .dark *));
:root {
/* Surface ramp — light mode: near-white surfaces over a faint blue base. */
--radius: 0.625rem;
--background: oklch(99% 0.004 240);
--foreground: oklch(20% 0.03 250);
--card: oklch(100% 0 0);
--card-foreground: oklch(20% 0.03 250);
--popover: oklch(100% 0 0);
--popover-foreground: oklch(20% 0.03 250);
/* sky accent ramp */
--primary: oklch(64% 0.16 230);
--primary-foreground: oklch(99% 0.005 240);
--secondary: oklch(96.5% 0.012 240);
--secondary-foreground: oklch(22% 0.03 250);
--muted: oklch(96.5% 0.012 240);
--muted-foreground: oklch(48% 0.02 250);
--accent: oklch(94% 0.04 230);
--accent-foreground: oklch(28% 0.08 230);
--destructive: oklch(58% 0.22 27);
--destructive-foreground: oklch(99% 0.005 240);
--success: oklch(64% 0.16 155);
--success-foreground: oklch(99% 0.005 240);
--warning: oklch(76% 0.16 75);
--warning-foreground: oklch(20% 0.05 75);
--info: oklch(64% 0.16 230);
--info-foreground: oklch(99% 0.005 240);
--border: oklch(91% 0.012 240);
--input: oklch(94% 0.012 240);
--ring: oklch(64% 0.16 230);
--sidebar: oklch(98% 0.006 240);
--sidebar-foreground: oklch(25% 0.03 250);
--sidebar-primary: oklch(64% 0.16 230);
--sidebar-primary-foreground: oklch(99% 0.005 240);
--sidebar-accent: oklch(94% 0.03 230);
--sidebar-accent-foreground: oklch(28% 0.08 230);
--sidebar-border: oklch(91% 0.012 240);
--sidebar-ring: oklch(64% 0.16 230);
--chrome-height: 3.25rem; /* 52px — uniform top-bar height */
--titlebar-height: 2rem; /* 32px — Windows native titlebar height */
--shadow-color: 240 6% 10%;
--shadow-xs: 0 1px 2px 0 hsl(var(--shadow-color) / 0.05);
--shadow-sm: 0 1px 3px 0 hsl(var(--shadow-color) / 0.08), 0 1px 2px -1px hsl(var(--shadow-color) / 0.06);
--shadow-md: 0 4px 6px -1px hsl(var(--shadow-color) / 0.08), 0 2px 4px -2px hsl(var(--shadow-color) / 0.04);
--shadow-lg: 0 10px 16px -3px hsl(var(--shadow-color) / 0.10), 0 4px 6px -4px hsl(var(--shadow-color) / 0.05);
--shadow-xl: 0 20px 28px -8px hsl(var(--shadow-color) / 0.14);
}
.dark {
--background: oklch(13% 0.02 250);
--foreground: oklch(96% 0.012 240);
--card: oklch(17% 0.022 250);
--card-foreground: oklch(96% 0.012 240);
--popover: oklch(20% 0.025 250);
--popover-foreground: oklch(96% 0.012 240);
--primary: oklch(72% 0.16 230);
--primary-foreground: oklch(12% 0.025 250);
--secondary: oklch(22% 0.025 250);
--secondary-foreground: oklch(94% 0.012 240);
--muted: oklch(20% 0.022 250);
--muted-foreground: oklch(72% 0.02 240);
--accent: oklch(30% 0.06 230);
--accent-foreground: oklch(88% 0.12 230);
--destructive: oklch(66% 0.22 27);
--destructive-foreground: oklch(98% 0.005 240);
--success: oklch(70% 0.16 155);
--success-foreground: oklch(12% 0.025 250);
--warning: oklch(80% 0.16 75);
--warning-foreground: oklch(20% 0.05 75);
--info: oklch(72% 0.16 230);
--info-foreground: oklch(12% 0.025 250);
--border: oklch(28% 0.025 250);
--input: oklch(26% 0.025 250);
--ring: oklch(72% 0.16 230);
--sidebar: oklch(15% 0.022 250);
--sidebar-foreground: oklch(92% 0.012 240);
--sidebar-primary: oklch(72% 0.16 230);
--sidebar-primary-foreground: oklch(12% 0.025 250);
--sidebar-accent: oklch(28% 0.06 230);
--sidebar-accent-foreground: oklch(88% 0.12 230);
--sidebar-border: oklch(24% 0.025 250);
--sidebar-ring: oklch(72% 0.16 230);
--shadow-color: 240 14% 4%;
--shadow-xs: 0 1px 2px 0 hsl(var(--shadow-color) / 0.30);
--shadow-sm: 0 1px 3px 0 hsl(var(--shadow-color) / 0.36), 0 1px 2px -1px hsl(var(--shadow-color) / 0.28);
--shadow-md: 0 4px 6px -1px hsl(var(--shadow-color) / 0.40), 0 2px 4px -2px hsl(var(--shadow-color) / 0.28);
--shadow-lg: 0 10px 16px -3px hsl(var(--shadow-color) / 0.50), 0 4px 6px -4px hsl(var(--shadow-color) / 0.32);
--shadow-xl: 0 20px 28px -8px hsl(var(--shadow-color) / 0.55);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-success: var(--success);
--color-success-foreground: var(--success-foreground);
--color-warning: var(--warning);
--color-warning-foreground: var(--warning-foreground);
--color-info: var(--info);
--color-info-foreground: var(--info-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--radius-xs: calc(var(--radius) - 6px);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--radius-3xl: calc(var(--radius) + 16px);
--shadow-xs: var(--shadow-xs);
--shadow-sm: var(--shadow-sm);
--shadow-md: var(--shadow-md);
--shadow-lg: var(--shadow-lg);
--shadow-xl: var(--shadow-xl);
--font-sans: 'Inter', 'Noto Sans SC', system-ui, -apple-system, sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
}
@layer base {
* {
border-color: var(--border);
}
html, body, #root {
height: 100%;
}
body {
margin: 0;
background-color: var(--background);
color: var(--foreground);
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
line-height: 1.5;
overflow: hidden; /* desktop chrome: no body scroll, panels manage their own */
}
::selection {
background-color: color-mix(in oklab, var(--primary) 28%, transparent);
color: var(--foreground);
}
*::-webkit-scrollbar {
width: 10px;
height: 10px;
}
*::-webkit-scrollbar-track {
background: transparent;
}
*::-webkit-scrollbar-thumb {
background-color: color-mix(in oklab, var(--muted-foreground) 35%, transparent);
border-radius: 9999px;
border: 2px solid transparent;
background-clip: content-box;
}
*::-webkit-scrollbar-thumb:hover {
background-color: color-mix(in oklab, var(--muted-foreground) 55%, transparent);
background-clip: content-box;
}
html[data-density="compact"] body {
font-size: 13.5px;
line-height: 1.45;
}
}
@layer utilities {
.hover-lift {
transition: transform 180ms cubic-bezier(0.22, 1, 0.36, 1),
box-shadow 220ms cubic-bezier(0.22, 1, 0.36, 1);
}
.hover-lift:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
.surface-gloss {
background-image: linear-gradient(
180deg,
color-mix(in oklab, var(--foreground) 3%, transparent) 0%,
transparent 80%
);
}
.surface-glass {
background-color: color-mix(in oklab, var(--popover) 92%, transparent);
backdrop-filter: saturate(180%) blur(12px);
-webkit-backdrop-filter: saturate(180%) blur(12px);
}
}
@keyframes progress-marquee {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(400%);
}
}
+16
View File
@@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"jsx": "react-jsx",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["react", "react-dom"],
"ignoreDeprecations": "6.0"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
+12
View File
@@ -0,0 +1,12 @@
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
dts: true,
sourcemap: true,
clean: true,
external: ['react', 'react-dom'],
target: 'es2022',
treeshake: true,
});