diff options
Diffstat (limited to 'apps/web/src/components/ui')
| -rw-r--r-- | apps/web/src/components/ui/editable-timecode.tsx | 274 | ||||
| -rw-r--r-- | apps/web/src/components/ui/input.tsx | 2 | ||||
| -rw-r--r-- | apps/web/src/components/ui/sponsor-button.tsx | 8 | ||||
| -rw-r--r-- | apps/web/src/components/ui/tooltip.tsx | 78 |
4 files changed, 201 insertions, 161 deletions
diff --git a/apps/web/src/components/ui/editable-timecode.tsx b/apps/web/src/components/ui/editable-timecode.tsx index 55375e33..f6d29f49 100644 --- a/apps/web/src/components/ui/editable-timecode.tsx +++ b/apps/web/src/components/ui/editable-timecode.tsx @@ -1,137 +1,137 @@ -"use client"; - -import { useState, useRef, useEffect } from "react"; -import { cn } from "@/lib/utils"; -import { formatTimeCode, parseTimeCode } from "@/lib/time"; - -interface EditableTimecodeProps { - time: number; - duration?: number; - format?: "MM:SS" | "HH:MM:SS" | "HH:MM:SS:CS" | "HH:MM:SS:FF"; - fps?: number; - onTimeChange?: (time: number) => void; - className?: string; - disabled?: boolean; -} - -export function EditableTimecode({ - time, - duration, - format = "HH:MM:SS:FF", - fps = 30, - onTimeChange, - className, - disabled = false, -}: EditableTimecodeProps) { - const [isEditing, setIsEditing] = useState(false); - const [inputValue, setInputValue] = useState(""); - const [hasError, setHasError] = useState(false); - const inputRef = useRef<HTMLInputElement>(null); - const enterPressedRef = useRef(false); - - const formattedTime = formatTimeCode(time, format, fps); - - const startEditing = () => { - if (disabled) return; - setIsEditing(true); - setInputValue(formattedTime); - setHasError(false); - enterPressedRef.current = false; - }; - - const cancelEditing = () => { - setIsEditing(false); - setInputValue(""); - setHasError(false); - enterPressedRef.current = false; - }; - - const applyEdit = () => { - const parsedTime = parseTimeCode(inputValue, format, fps); - - if (parsedTime === null) { - setHasError(true); - return; - } - - // Clamp time to valid range - const clampedTime = Math.max( - 0, - duration ? Math.min(duration, parsedTime) : parsedTime - ); - - onTimeChange?.(clampedTime); - setIsEditing(false); - setInputValue(""); - setHasError(false); - enterPressedRef.current = false; - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - e.preventDefault(); - enterPressedRef.current = true; - applyEdit(); - } else if (e.key === "Escape") { - e.preventDefault(); - cancelEditing(); - } - }; - - const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { - setInputValue(e.target.value); - setHasError(false); - }; - - const handleBlur = () => { - // Only apply edit if Enter wasn't pressed (to avoid double processing) - if (!enterPressedRef.current && isEditing) { - applyEdit(); - } - }; - - // Focus input when entering edit mode - useEffect(() => { - if (isEditing && inputRef.current) { - inputRef.current.focus(); - inputRef.current.select(); - } - }, [isEditing]); - - if (isEditing) { - return ( - <input - ref={inputRef} - type="text" - value={inputValue} - onChange={handleInputChange} - onKeyDown={handleKeyDown} - onBlur={handleBlur} - className={cn( - "text-xs font-mono bg-transparent border-none outline-none", - "focus:bg-background focus:border focus:border-primary focus:px-1 focus:rounded", - "tabular-nums text-primary", - hasError && "text-destructive focus:border-destructive", - className - )} - style={{ width: `${formattedTime.length + 1}ch` }} - placeholder={formattedTime} - /> - ); - } - - return ( - <span - onClick={startEditing} - className={cn( - "text-xs font-mono tabular-nums text-primary cursor-pointer", - "hover:bg-muted/50 hover:rounded px-1 -mx-1 transition-colors", - disabled && "cursor-default hover:bg-transparent", - className - )} - title={disabled ? undefined : "Click to edit time"} - > - {formattedTime} - </span> - ); -} +"use client";
+
+import { useState, useRef, useEffect } from "react";
+import { cn } from "@/lib/utils";
+import { formatTimeCode, parseTimeCode } from "@/lib/time";
+
+interface EditableTimecodeProps {
+ time: number;
+ duration?: number;
+ format?: "MM:SS" | "HH:MM:SS" | "HH:MM:SS:CS" | "HH:MM:SS:FF";
+ fps?: number;
+ onTimeChange?: (time: number) => void;
+ className?: string;
+ disabled?: boolean;
+}
+
+export function EditableTimecode({
+ time,
+ duration,
+ format = "HH:MM:SS:FF",
+ fps = 30,
+ onTimeChange,
+ className,
+ disabled = false,
+}: EditableTimecodeProps) {
+ const [isEditing, setIsEditing] = useState(false);
+ const [inputValue, setInputValue] = useState("");
+ const [hasError, setHasError] = useState(false);
+ const inputRef = useRef<HTMLInputElement>(null);
+ const enterPressedRef = useRef(false);
+
+ const formattedTime = formatTimeCode(time, format, fps);
+
+ const startEditing = () => {
+ if (disabled) return;
+ setIsEditing(true);
+ setInputValue(formattedTime);
+ setHasError(false);
+ enterPressedRef.current = false;
+ };
+
+ const cancelEditing = () => {
+ setIsEditing(false);
+ setInputValue("");
+ setHasError(false);
+ enterPressedRef.current = false;
+ };
+
+ const applyEdit = () => {
+ const parsedTime = parseTimeCode(inputValue, format, fps);
+
+ if (parsedTime === null) {
+ setHasError(true);
+ return;
+ }
+
+ // Clamp time to valid range
+ const clampedTime = Math.max(
+ 0,
+ duration ? Math.min(duration, parsedTime) : parsedTime
+ );
+
+ onTimeChange?.(clampedTime);
+ setIsEditing(false);
+ setInputValue("");
+ setHasError(false);
+ enterPressedRef.current = false;
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ enterPressedRef.current = true;
+ applyEdit();
+ } else if (e.key === "Escape") {
+ e.preventDefault();
+ cancelEditing();
+ }
+ };
+
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ setInputValue(e.target.value);
+ setHasError(false);
+ };
+
+ const handleBlur = () => {
+ // Only apply edit if Enter wasn't pressed (to avoid double processing)
+ if (!enterPressedRef.current && isEditing) {
+ applyEdit();
+ }
+ };
+
+ // Focus input when entering edit mode
+ useEffect(() => {
+ if (isEditing && inputRef.current) {
+ inputRef.current.focus();
+ inputRef.current.select();
+ }
+ }, [isEditing]);
+
+ if (isEditing) {
+ return (
+ <input
+ ref={inputRef}
+ type="text"
+ value={inputValue}
+ onChange={handleInputChange}
+ onKeyDown={handleKeyDown}
+ onBlur={handleBlur}
+ className={cn(
+ "text-xs font-mono bg-transparent border-none outline-none",
+ "focus:bg-background focus:border focus:border-primary focus:px-1 focus:rounded",
+ "tabular-nums text-primary",
+ hasError && "text-destructive focus:border-destructive",
+ className
+ )}
+ style={{ width: `${formattedTime.length + 1}ch` }}
+ placeholder={formattedTime}
+ />
+ );
+ }
+
+ return (
+ <span
+ onClick={startEditing}
+ className={cn(
+ "text-xs font-mono tabular-nums text-primary cursor-pointer",
+ "hover:bg-muted/50 hover:rounded px-1 -mx-1 transition-colors",
+ disabled && "cursor-default hover:bg-transparent",
+ className
+ )}
+ title={disabled ? undefined : "Click to edit time"}
+ >
+ {formattedTime}
+ </span>
+ );
+}
diff --git a/apps/web/src/components/ui/input.tsx b/apps/web/src/components/ui/input.tsx index 440191f2..65c7660d 100644 --- a/apps/web/src/components/ui/input.tsx +++ b/apps/web/src/components/ui/input.tsx @@ -51,7 +51,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>( <input type={inputType} className={cn( - "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", + "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-accent/50 px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[2px]", "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", paddingRight, diff --git a/apps/web/src/components/ui/sponsor-button.tsx b/apps/web/src/components/ui/sponsor-button.tsx index 9dc8e966..146566a6 100644 --- a/apps/web/src/components/ui/sponsor-button.tsx +++ b/apps/web/src/components/ui/sponsor-button.tsx @@ -19,18 +19,18 @@ export function SponsorButton({ href={href} target="_blank" rel="noopener noreferrer" - className={`inline-flex items-center gap-2 px-3 py-2 rounded-full border border-white/10 bg-white/5 backdrop-blur-xs hover:bg-white/10 hover:border-white/20 transition-all duration-200 group shadow-lg ${className}`} + className={`inline-flex items-center gap-2 px-3 py-2 rounded-md border border-border bg-background/5 backdrop-blur-xs hover:bg-background/10 transition-all duration-200 group shadow-lg ${className}`} > - <span className="text-xs font-medium text-zinc-400 group-hover:text-zinc-300 transition-colors"> + <span className="text-xs font-medium text-muted-foreground group-hover:text-foreground transition-colors"> Sponsored by </span> <div className="flex items-center gap-1.5"> - <div className="text-zinc-100 group-hover:text-white transition-colors"> + <div className="text-foreground/90 group-hover:text-foreground transition-colors"> <Logo className="w-4 h-4" /> </div> - <span className="text-xs font-medium text-zinc-100 group-hover:text-white transition-colors"> + <span className="text-xs font-medium text-foreground/90 group-hover:text-foreground transition-colors"> {companyName} </span> </div> diff --git a/apps/web/src/components/ui/tooltip.tsx b/apps/web/src/components/ui/tooltip.tsx index 4971e262..bfb75e2e 100644 --- a/apps/web/src/components/ui/tooltip.tsx +++ b/apps/web/src/components/ui/tooltip.tsx @@ -1,9 +1,8 @@ -"use client"; +import { cva, type VariantProps } from 'class-variance-authority'; +import { Tooltip as TooltipPrimitive } from 'radix-ui'; +import * as React from 'react'; -import * as React from "react"; -import { Tooltip as TooltipPrimitive } from "radix-ui"; - -import { cn } from "../../lib/utils"; +import { cn } from '@/lib/utils'; const TooltipProvider = TooltipPrimitive.Provider; @@ -11,22 +10,63 @@ const Tooltip = TooltipPrimitive.Root; const TooltipTrigger = TooltipPrimitive.Trigger; +const tooltipVariants = cva( + 'z-50 overflow-visible rounded-sm text-sm shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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', + { + variants: { + variant: { + default: 'bg-popover text-popover-foreground border px-3 py-1.5', + destructive: + 'bg-destructive/10 text-destructive dark:bg-destructive/20 border-destructive [border-width:0.5px]', + outline: 'border-border', + important: + 'bg-amber-100/90 text-amber-900 dark:bg-amber-900/20 dark:text-amber-300 border-amber-900 [border-width:0.5px]', + promotions: + 'bg-red-100/90 text-red-900 dark:bg-red-900/20 dark:text-red-300 border-red-900 [border-width:0.5px]', + personal: + 'bg-green-100/90 text-green-900 dark:bg-green-900/20 dark:text-green-300 border-green-900 [border-width:0.5px]', + updates: + 'bg-purple-100/90 text-purple-900 dark:bg-purple-900/20 dark:text-purple-300 border-purple-900 [border-width:0.5px]', + forums: + 'bg-blue-100/90 text-blue-900 dark:bg-blue-900/20 dark:text-blue-300 border-blue-900 [border-width:0.5px]', + sidebar: 'bg-white dark:bg-[#413F3E] p-2.5 flex flex-col gap-2', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +); + +interface TooltipContentProps + extends React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>, + VariantProps<typeof tooltipVariants> {} + const TooltipContent = React.forwardRef< React.ElementRef<typeof TooltipPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> ->(({ className, sideOffset = 4, ...props }, ref) => ( - <TooltipPrimitive.Portal> - <TooltipPrimitive.Content - ref={ref} - sideOffset={sideOffset} - className={cn( - "z-50 overflow-hidden rounded-md bg-foreground px-3 py-1.5 text-xs text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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} - /> - </TooltipPrimitive.Portal> + TooltipContentProps +>(({ className, sideOffset = 4, variant, ...props }, ref) => ( + <TooltipPrimitive.Content + ref={ref} + sideOffset={sideOffset} + className={cn(tooltipVariants({ variant }), className)} + {...props} + > + {variant === 'sidebar' && ( + <svg + width="6" + height="10" + viewBox="0 0 6 10" + fill="none" + xmlns="http://www.w3.org/2000/svg" + className="absolute left-[-6px] top-1/2 -translate-y-1/2" + > + <path d="M6 0L0 5L6 10V0Z" className="fill-white/80 dark:fill-[#413F3E]" /> + </svg> + )} + {props.children} + </TooltipPrimitive.Content> )); TooltipContent.displayName = TooltipPrimitive.Content.displayName; -export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
\ No newline at end of file |
