{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "loaders", "title": "Loading Primitives", "description": "Accessible loaders, pending buttons, section and page indicators, local overlays, and list/table skeleton recipes", "dependencies": ["daisyui"], "files": [ { "path": "registry/default/ui/loaders/loader.tsx", "content": "import type { ComponentPropsWithRef, ReactNode } from 'react';\nimport { Children } from 'react';\nimport { cn } from '@/lib/utils';\n\nexport type LoaderVariant = 'spinner' | 'dots' | 'bars' | 'ring';\nexport type LoaderSize = 'xs' | 'sm' | 'md' | 'lg';\n\nconst variantClass: Record = {\n\tspinner: 'loading-spinner',\n\tdots: 'loading-dots',\n\tbars: 'loading-bars',\n\tring: 'loading-ring',\n};\n\nconst sizeClass: Record = {\n\txs: 'loading-xs',\n\tsm: 'loading-sm',\n\tmd: 'loading-md',\n\tlg: 'loading-lg',\n};\n\nfunction hasNode(value: ReactNode) {\n\treturn Children.toArray(value).some((node) => typeof node !== 'string' || node.trim().length > 0);\n}\n\nexport type LoaderProps = Omit, 'children'> & {\n\tvariant?: LoaderVariant;\n\tsize?: LoaderSize;\n\tlabel?: string;\n\tdecorative?: boolean;\n};\n\nexport function Loader({\n\tvariant = 'spinner',\n\tsize = 'sm',\n\tlabel = '加载中',\n\tdecorative = false,\n\tclassName,\n\trole,\n\t'aria-label': ariaLabel,\n\t'aria-hidden': ariaHidden,\n\t...props\n}: LoaderProps) {\n\treturn (\n\t\t\n\t\t\t\n\t\t\n\t);\n}\n\nexport type LoadingIndicatorLayout = 'inline' | 'section' | 'page';\n\nexport type LoadingIndicatorProps = ComponentPropsWithRef<'div'> & {\n\tvariant?: LoaderVariant;\n\tsize?: LoaderSize;\n\tlayout?: LoadingIndicatorLayout;\n\tlabel?: ReactNode;\n\tdescription?: ReactNode;\n\tloaderLabel?: string;\n\tcontentClassName?: string;\n};\n\nconst layoutClass: Record = {\n\tinline: 'inline-flex min-h-0 flex-row items-center gap-2 text-left',\n\tsection: 'flex min-h-40 flex-col items-center justify-center gap-2 text-center',\n\tpage: 'flex min-h-[24rem] flex-col items-center justify-center gap-3 text-center',\n};\n\nexport function LoadingIndicator({\n\tvariant = 'dots',\n\tsize = 'md',\n\tlayout = 'section',\n\tlabel = '正在加载',\n\tdescription,\n\tloaderLabel = '加载中',\n\tcontentClassName,\n\tclassName,\n\t'aria-label': ariaLabel,\n\t...props\n}: LoadingIndicatorProps) {\n\tconst showLabel = hasNode(label);\n\tconst showDescription = hasNode(description);\n\treturn (\n\t\t\n\t\t\t\n\t\t\t{showLabel || showDescription ? (\n\t\t\t\t
\n\t\t\t\t\t{showLabel ?
{label}
: null}\n\t\t\t\t\t{showDescription ?
{description}
: null}\n\t\t\t\t
\n\t\t\t) : null}\n\t\t\n\t);\n}\n", "type": "registry:component", "target": "@components/loaders/loader.tsx" }, { "path": "registry/default/ui/loaders/pending-button.tsx", "content": "import type { ComponentPropsWithRef, ReactNode } from 'react';\nimport { cn } from '@/lib/utils';\nimport type { LoaderSize, LoaderVariant } from './loader';\nimport { Loader } from './loader';\n\nexport type PendingButtonProps = ComponentPropsWithRef<'button'> & {\n\tpending?: boolean;\n\tpendingLabel?: ReactNode;\n\tpendingAriaLabel?: string;\n\tloaderVariant?: LoaderVariant;\n\tloaderSize?: LoaderSize;\n};\n\nexport function PendingButton({\n\tpending = false,\n\tpendingLabel = '处理中',\n\tpendingAriaLabel,\n\tloaderVariant = 'spinner',\n\tloaderSize = 'xs',\n\tdisabled,\n\ttype = 'button',\n\tchildren,\n\tclassName,\n\t'aria-label': ariaLabel,\n\t...props\n}: PendingButtonProps) {\n\tconst inferredIdleLabel = typeof children === 'string' || typeof children === 'number' ? String(children) : undefined;\n\tconst inferredPendingLabel =\n\t\ttypeof pendingLabel === 'string' || typeof pendingLabel === 'number' ? String(pendingLabel) : undefined;\n\treturn (\n\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t{children}\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t{pendingLabel}\n\t\t\t\t\n\t\t\t\n\t\t\n\t);\n}\n", "type": "registry:component", "target": "@components/loaders/pending-button.tsx" }, { "path": "registry/default/ui/loaders/loading-overlay.tsx", "content": "import type { ComponentPropsWithRef, ReactNode } from 'react';\nimport { cn } from '@/lib/utils';\nimport type { LoaderSize, LoaderVariant } from './loader';\nimport { LoadingIndicator } from './loader';\n\nexport type LoadingOverlayBackdrop = 'subtle' | 'solid' | 'transparent';\n\nexport type LoadingOverlayProps = ComponentPropsWithRef<'div'> & {\n\tactive?: boolean;\n\tlabel?: ReactNode;\n\tdescription?: ReactNode;\n\tvariant?: LoaderVariant;\n\tsize?: LoaderSize;\n\tbackdrop?: LoadingOverlayBackdrop;\n\tcontentClassName?: string;\n};\n\nconst backdropClass: Record = {\n\tsubtle: 'bg-base-100/70 backdrop-blur-[1px]',\n\tsolid: 'bg-base-100',\n\ttransparent: 'bg-transparent',\n};\n\nexport function LoadingOverlay({\n\tactive = false,\n\tlabel = '正在刷新',\n\tdescription,\n\tvariant = 'spinner',\n\tsize = 'md',\n\tbackdrop = 'subtle',\n\tcontentClassName,\n\tchildren,\n\tclassName,\n\t...props\n}: LoadingOverlayProps) {\n\treturn (\n\t\t\n\t\t\t\n\t\t\t\t{children}\n\t\t\t\n\t\t\t{active ? (\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t) : null}\n\t\t\n\t);\n}\n", "type": "registry:component", "target": "@components/loaders/loading-overlay.tsx" }, { "path": "registry/default/ui/loaders/skeleton.tsx", "content": "import type { ComponentPropsWithRef } from 'react';\nimport { cn } from '@/lib/utils';\n\nexport type SkeletonShape = 'text' | 'rect' | 'circle';\n\nexport type SkeletonProps = Omit, 'children'> & {\n\tshape?: SkeletonShape;\n};\n\nconst skeletonShapeClass: Record = {\n\ttext: 'h-4 w-full rounded-sm',\n\trect: 'h-20 w-full rounded-md',\n\tcircle: 'size-10 shrink-0 rounded-full',\n};\n\nexport function Skeleton({ shape = 'text', className, 'aria-hidden': ariaHidden = true, ...props }: SkeletonProps) {\n\treturn (\n\t\t\n\t);\n}\n\nexport type LoadingRowsVariant = 'list' | 'table';\nexport type LoadingRowsDensity = 'compact' | 'comfortable';\nexport type LoadingRowsColumns = 1 | 2 | 3 | 4;\n\nexport type LoadingRowsProps = Omit, 'children'> & {\n\tvariant?: LoadingRowsVariant;\n\tdensity?: LoadingRowsDensity;\n\trows?: number;\n\tcolumns?: LoadingRowsColumns;\n\tshowHeader?: boolean;\n\tshowLeading?: boolean;\n\tshowTrailing?: boolean;\n\tlabel?: string;\n};\n\nconst tableColumnsClass: Record = {\n\t1: 'grid-cols-1',\n\t2: 'grid-cols-[minmax(0,1.5fr)_minmax(0,1fr)]',\n\t3: 'grid-cols-[minmax(0,1.5fr)_minmax(0,1fr)_minmax(0,.75fr)]',\n\t4: 'grid-cols-[minmax(0,1.5fr)_minmax(0,1fr)_minmax(0,.75fr)_minmax(0,.5fr)]',\n};\n\nconst cellWidthClass = ['w-4/5', 'w-2/3', 'w-3/4', 'w-1/2'] as const;\nconst rowPaddingClass: Record = {\n\tcompact: 'py-2',\n\tcomfortable: 'py-3',\n};\n\nfunction normalizeRows(rows: number) {\n\treturn Number.isFinite(rows) ? Math.min(20, Math.max(1, Math.floor(rows))) : 5;\n}\n\nexport function LoadingRows({\n\tvariant = 'list',\n\tdensity = 'comfortable',\n\trows = 5,\n\tcolumns = 4,\n\tshowHeader = true,\n\tshowLeading = true,\n\tshowTrailing = true,\n\tlabel = '正在加载列表',\n\tclassName,\n\t'aria-label': ariaLabel,\n\t...props\n}: LoadingRowsProps) {\n\tconst rowCount = normalizeRows(rows);\n\treturn (\n\t\t\n\t\t\t\n\t\t\t\t\t) : (\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{showLeading ? : null}\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t{showTrailing ? : null}\n\t\t\t\t\t\t\n\t\t\t\t\t),\n\t\t\t\t)}\n\t\t\t\n\t\t\n\t);\n}\n", "type": "registry:component", "target": "@components/loaders/skeleton.tsx" }, { "path": "registry/default/ui/loaders/index.ts", "content": "export * from './loader';\nexport * from './loading-overlay';\nexport * from './pending-button';\nexport * from './skeleton';\n", "type": "registry:component", "target": "@components/loaders/index.ts" } ], "type": "registry:component" }