Create standalone browser port of LTX Athena with authenticated native API transport
This commit is contained in:
commit
1333b0c36b
202 files changed
+63054
No files matched your search
@@ -0,0 +1,48 @@
|
||||
import * as React from 'react'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center 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',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
||||
destructive: 'bg-red-500 text-white shadow-sm hover:bg-red-600',
|
||||
outline: 'border border-border bg-transparent shadow-sm hover:bg-secondary',
|
||||
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-secondary hover:text-secondary-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',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = 'Button'
|
||||
|
||||
export { Button, buttonVariants }
|
||||
@@ -0,0 +1,40 @@
|
||||
import * as React from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface ProgressProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
value?: number
|
||||
max?: number
|
||||
showLabel?: boolean
|
||||
}
|
||||
|
||||
const Progress = React.forwardRef<HTMLDivElement, ProgressProps>(
|
||||
({ className, value = 0, max = 100, showLabel = false, ...props }, ref) => {
|
||||
const percentage = Math.min(100, Math.max(0, (value / max) * 100))
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative h-2 w-full overflow-hidden rounded-full bg-secondary',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className="h-full bg-primary transition-all duration-300 ease-in-out"
|
||||
style={{ width: `${percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
{showLabel && (
|
||||
<span className="text-xs text-muted-foreground mt-1 block text-right">
|
||||
{Math.round(percentage)}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
Progress.displayName = 'Progress'
|
||||
|
||||
export { Progress }
|
||||
@@ -0,0 +1,47 @@
|
||||
import * as React from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { ChevronDown } from 'lucide-react'
|
||||
|
||||
interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {
|
||||
label?: string
|
||||
badge?: string
|
||||
}
|
||||
|
||||
const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
|
||||
({ className, label, badge, children, ...props }, ref) => {
|
||||
return (
|
||||
<div className="relative">
|
||||
{label && (
|
||||
<label className="flex items-center gap-2 text-[12px] font-semibold text-zinc-500 mb-2 uppercase leading-4 h-4">
|
||||
{label}
|
||||
{badge && (
|
||||
<span className="px-1.5 py-0.5 text-[10px] bg-zinc-700 text-zinc-300 rounded border border-zinc-600 font-semibold leading-none">
|
||||
{badge}
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
)}
|
||||
<div className="relative">
|
||||
<select
|
||||
className={cn(
|
||||
'flex h-10 w-full appearance-none rounded-lg border border-zinc-700 bg-zinc-800 px-3 py-2 text-sm text-white',
|
||||
'focus:outline-none focus:ring-1 focus:ring-zinc-500 focus:border-zinc-500',
|
||||
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'pr-8 cursor-pointer',
|
||||
'[&>option]:bg-zinc-800 [&>option]:text-white [&>option:disabled]:text-zinc-500',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</select>
|
||||
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-zinc-400 pointer-events-none" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
Select.displayName = 'Select'
|
||||
|
||||
export { Select }
|
||||
@@ -0,0 +1,48 @@
|
||||
import * as React from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
||||
label?: string
|
||||
helperText?: string
|
||||
charCount?: number
|
||||
maxChars?: number
|
||||
}
|
||||
|
||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ className, label, helperText, charCount, maxChars, ...props }, ref) => {
|
||||
return (
|
||||
<div className="w-full">
|
||||
{label && (
|
||||
<label className="block text-[12px] font-semibold text-zinc-500 mb-2 uppercase leading-4">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<textarea
|
||||
className={cn(
|
||||
'flex min-h-[120px] w-full rounded-lg border border-zinc-700 bg-zinc-800 px-3 py-3 text-sm text-white',
|
||||
'placeholder:text-zinc-500',
|
||||
'focus:outline-none focus:ring-1 focus:ring-zinc-500 focus:border-zinc-500',
|
||||
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'resize-y',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
<div className="flex justify-between mt-2">
|
||||
{helperText && (
|
||||
<span className="text-xs text-zinc-500">{helperText}</span>
|
||||
)}
|
||||
{maxChars !== undefined && (
|
||||
<span className="text-xs text-zinc-500 ml-auto">
|
||||
{charCount ?? 0}/{maxChars}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
Textarea.displayName = 'Textarea'
|
||||
|
||||
export { Textarea }
|
||||
@@ -0,0 +1,75 @@
|
||||
import React, { useState, useRef, useCallback, useEffect } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface TooltipProps {
|
||||
content: React.ReactNode
|
||||
children: React.ReactNode
|
||||
/** Which side of the trigger the tooltip appears on. Default: 'top' */
|
||||
side?: 'top' | 'bottom' | 'left' | 'right'
|
||||
className?: string
|
||||
}
|
||||
|
||||
const DELAY_MS = 500
|
||||
const GAP_PX = 6
|
||||
|
||||
/**
|
||||
* Styled tooltip with 250ms show delay and instant hide.
|
||||
* Renders via a portal into document.body so it is never clipped
|
||||
* by overflow-hidden ancestors.
|
||||
*/
|
||||
export function Tooltip({ content, children, side = 'top', className }: TooltipProps) {
|
||||
const [visible, setVisible] = useState(false)
|
||||
const [style, setStyle] = useState<React.CSSProperties>({})
|
||||
const wrapperRef = useRef<HTMLDivElement>(null)
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
const computeStyle = useCallback(() => {
|
||||
const rect = wrapperRef.current?.getBoundingClientRect()
|
||||
if (!rect) return {}
|
||||
switch (side) {
|
||||
case 'top':
|
||||
return { left: rect.left + rect.width / 2, top: rect.top - GAP_PX, transform: 'translate(-50%, -100%)' }
|
||||
case 'bottom':
|
||||
return { left: rect.left + rect.width / 2, top: rect.bottom + GAP_PX, transform: 'translate(-50%, 0)' }
|
||||
case 'left':
|
||||
return { left: rect.left - GAP_PX, top: rect.top + rect.height / 2, transform: 'translate(-100%, -50%)' }
|
||||
case 'right':
|
||||
return { left: rect.right + GAP_PX, top: rect.top + rect.height / 2, transform: 'translate(0, -50%)' }
|
||||
}
|
||||
}, [side])
|
||||
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
timerRef.current = setTimeout(() => {
|
||||
setStyle(computeStyle() ?? {})
|
||||
setVisible(true)
|
||||
}, DELAY_MS)
|
||||
}, [computeStyle])
|
||||
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null }
|
||||
setVisible(false)
|
||||
}, [])
|
||||
|
||||
useEffect(() => () => { if (timerRef.current) clearTimeout(timerRef.current) }, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
className={cn('inline-flex', className)}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
{children}
|
||||
{visible && ReactDOM.createPortal(
|
||||
<div
|
||||
className="fixed z-[99999] px-2.5 py-1.5 bg-white text-zinc-800 text-xs font-medium rounded-md shadow-md whitespace-nowrap pointer-events-none select-none"
|
||||
style={style}
|
||||
>
|
||||
{content}
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user