Files
chaos-api/web/default/src/components/status-badge.tsx
T
t0ng7u d146e45e2f ⚖️ chore(web/default): add reusable copyright header tooling
Add a Bun script to apply and normalize AGPL copyright headers across the default frontend source files.

The script keeps headers idempotent, upgrades existing headers to the 2023-2026 QuantumNous range, and is exposed through `bun run copyright` for future maintenance.
2026-05-09 11:35:07 +08:00

191 lines
4.7 KiB
TypeScript
Vendored

/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
/* eslint-disable react-refresh/only-export-components */
import * as React from 'react'
import { type LucideIcon } from 'lucide-react'
import { stringToColor } from '@/lib/colors'
import { cn } from '@/lib/utils'
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
export const dotColorMap = {
success: 'bg-success',
warning: 'bg-warning',
danger: 'bg-destructive',
info: 'bg-info',
neutral: 'bg-neutral',
purple: 'bg-chart-4',
amber: 'bg-warning',
blue: 'bg-chart-1',
cyan: 'bg-chart-2',
green: 'bg-success',
grey: 'bg-neutral',
indigo: 'bg-chart-1',
'light-blue': 'bg-info',
'light-green': 'bg-success',
lime: 'bg-chart-3',
orange: 'bg-warning',
pink: 'bg-chart-5',
red: 'bg-destructive',
teal: 'bg-chart-2',
violet: 'bg-chart-4',
yellow: 'bg-warning',
} as const
export const textColorMap = {
success: 'text-success',
warning: 'text-warning',
danger: 'text-destructive',
info: 'text-info',
neutral: 'text-muted-foreground',
purple: 'text-chart-4',
amber: 'text-warning',
blue: 'text-chart-1',
cyan: 'text-chart-2',
green: 'text-success',
grey: 'text-muted-foreground',
indigo: 'text-chart-1',
'light-blue': 'text-info',
'light-green': 'text-success',
lime: 'text-chart-3',
orange: 'text-warning',
pink: 'text-chart-5',
red: 'text-destructive',
teal: 'text-chart-2',
violet: 'text-chart-4',
yellow: 'text-warning',
} as const
export type StatusVariant = keyof typeof dotColorMap
const sizeMap = {
sm: 'text-xs gap-1.5',
md: 'text-xs gap-1.5',
lg: 'text-sm gap-2',
} as const
export interface StatusBadgeProps extends Omit<
React.HTMLAttributes<HTMLSpanElement>,
'children'
> {
label?: string
children?: React.ReactNode
icon?: LucideIcon
pulse?: boolean
/** When false, hides the leading dot */
showDot?: boolean
variant?: StatusVariant | null
size?: 'sm' | 'md' | 'lg' | null
copyable?: boolean
copyText?: string
autoColor?: string
}
export function StatusBadge({
label,
children,
icon: Icon,
variant,
size = 'sm',
pulse = false,
showDot = true,
copyable = true,
copyText,
autoColor,
className,
onClick,
...props
}: StatusBadgeProps) {
const { copyToClipboard } = useCopyToClipboard()
const computedVariant: StatusVariant = autoColor
? (stringToColor(autoColor) as StatusVariant)
: (variant ?? 'neutral')
const handleClick = (e: React.MouseEvent<HTMLSpanElement>) => {
if (copyable) {
e.stopPropagation()
copyToClipboard(copyText || label || '')
}
onClick?.(e)
}
const content =
children ?? (label ? <span className='truncate'>{label}</span> : null)
return (
<span
className={cn(
'inline-flex w-fit shrink-0 items-center font-medium whitespace-nowrap',
sizeMap[size ?? 'sm'],
textColorMap[computedVariant],
pulse && 'animate-pulse',
copyable &&
'cursor-pointer transition-opacity hover:opacity-70 active:scale-95',
className
)}
onClick={handleClick}
title={copyable ? `Click to copy: ${copyText || label || ''}` : undefined}
{...props}
>
{showDot && (
<span
className={cn(
'inline-block size-1.5 shrink-0 rounded-full',
dotColorMap[computedVariant]
)}
aria-hidden='true'
/>
)}
{Icon && <Icon className='size-3 shrink-0' />}
{content}
</span>
)
}
export const statusPresets = {
active: {
variant: 'success' as const,
label: 'Active',
showDot: true,
},
inactive: {
variant: 'neutral' as const,
label: 'Inactive',
showDot: true,
},
invited: {
variant: 'info' as const,
label: 'Invited',
showDot: true,
},
suspended: {
variant: 'danger' as const,
label: 'Suspended',
showDot: true,
},
pending: {
variant: 'warning' as const,
label: 'Pending',
showDot: true,
pulse: true,
},
} as const
export type StatusPreset = keyof typeof statusPresets