@exitvibing/hqui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,122 @@
1
+ import React, { createContext, useContext, useState, forwardRef } from "react";
2
+ import { cn } from "../lib/cn";
3
+
4
+ interface TabsContextValue {
5
+ value: string;
6
+ onValueChange: (value: string) => void;
7
+ }
8
+
9
+ const TabsContext = createContext<TabsContextValue>({
10
+ value: "",
11
+ onValueChange: () => {},
12
+ });
13
+
14
+ export interface TabsProps extends React.HTMLAttributes<HTMLDivElement> {
15
+ defaultValue?: string;
16
+ value?: string;
17
+ onValueChange?: (value: string) => void;
18
+ }
19
+
20
+ export const Tabs = forwardRef<HTMLDivElement, TabsProps>(
21
+ (
22
+ {
23
+ className,
24
+ defaultValue = "",
25
+ value: controlledValue,
26
+ onValueChange,
27
+ children,
28
+ ...props
29
+ },
30
+ ref,
31
+ ) => {
32
+ const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
33
+ const isControlled = controlledValue !== undefined;
34
+ const currentValue = isControlled ? controlledValue : uncontrolledValue;
35
+
36
+ const handleChange = (newValue: string) => {
37
+ if (!isControlled) setUncontrolledValue(newValue);
38
+ onValueChange?.(newValue);
39
+ };
40
+
41
+ return (
42
+ <TabsContext.Provider
43
+ value={{ value: currentValue, onValueChange: handleChange }}
44
+ >
45
+ <div ref={ref} className={cn("w-full", className)} {...props}>
46
+ {children}
47
+ </div>
48
+ </TabsContext.Provider>
49
+ );
50
+ },
51
+ );
52
+ Tabs.displayName = "Tabs";
53
+
54
+ export const TabsList = forwardRef<
55
+ HTMLDivElement,
56
+ React.HTMLAttributes<HTMLDivElement>
57
+ >(({ className, ...props }, ref) => (
58
+ <div
59
+ ref={ref}
60
+ className={cn(
61
+ "inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
62
+ className,
63
+ )}
64
+ {...props}
65
+ />
66
+ ));
67
+ TabsList.displayName = "TabsList";
68
+
69
+ export interface TabsTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
70
+ value: string;
71
+ }
72
+
73
+ export const TabsTrigger = forwardRef<HTMLButtonElement, TabsTriggerProps>(
74
+ ({ className, value, ...props }, ref) => {
75
+ const ctx = useContext(TabsContext);
76
+ const isActive = ctx.value === value;
77
+
78
+ return (
79
+ <button
80
+ ref={ref}
81
+ type="button"
82
+ role="tab"
83
+ aria-selected={isActive}
84
+ data-state={isActive ? "active" : "inactive"}
85
+ className={cn(
86
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
87
+ isActive
88
+ ? "bg-background text-foreground shadow"
89
+ : "hover:bg-background/50 hover:text-foreground",
90
+ className,
91
+ )}
92
+ onClick={() => ctx.onValueChange(value)}
93
+ {...props}
94
+ />
95
+ );
96
+ },
97
+ );
98
+ TabsTrigger.displayName = "TabsTrigger";
99
+
100
+ export interface TabsContentProps extends React.HTMLAttributes<HTMLDivElement> {
101
+ value: string;
102
+ }
103
+
104
+ export const TabsContent = forwardRef<HTMLDivElement, TabsContentProps>(
105
+ ({ className, value, ...props }, ref) => {
106
+ const ctx = useContext(TabsContext);
107
+ if (ctx.value !== value) return null;
108
+
109
+ return (
110
+ <div
111
+ ref={ref}
112
+ role="tabpanel"
113
+ className={cn(
114
+ "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 animate-fade-in",
115
+ className,
116
+ )}
117
+ {...props}
118
+ />
119
+ );
120
+ },
121
+ );
122
+ TabsContent.displayName = "TabsContent";
@@ -0,0 +1,40 @@
1
+ import React, { forwardRef, useEffect, useState } from "react";
2
+ import { Moon, Sun } from "lucide-react";
3
+ import { Button } from "./Button";
4
+ import { cn } from "../lib/cn";
5
+
6
+ export interface ThemeToggleProps {
7
+ className?: string;
8
+ defaultTheme?: "light" | "dark";
9
+ }
10
+
11
+ export const ThemeToggle = forwardRef<HTMLButtonElement, ThemeToggleProps>(
12
+ ({ className, defaultTheme }, ref) => {
13
+ const [isDark, setIsDark] = useState(() => {
14
+ if (typeof window === "undefined") return defaultTheme === "dark";
15
+ return document.documentElement.classList.contains("dark");
16
+ });
17
+
18
+ useEffect(() => {
19
+ if (isDark) {
20
+ document.documentElement.classList.add("dark");
21
+ } else {
22
+ document.documentElement.classList.remove("dark");
23
+ }
24
+ }, [isDark]);
25
+
26
+ return (
27
+ <Button
28
+ ref={ref}
29
+ variant="ghost"
30
+ size="icon"
31
+ className={cn("rounded-full", className)}
32
+ onClick={() => setIsDark((prev) => !prev)}
33
+ aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
34
+ >
35
+ {isDark ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
36
+ </Button>
37
+ );
38
+ },
39
+ );
40
+ ThemeToggle.displayName = "ThemeToggle";
@@ -0,0 +1,120 @@
1
+ import React, {
2
+ HTMLAttributes,
3
+ forwardRef,
4
+ useState,
5
+ useRef,
6
+ useEffect,
7
+ useCallback,
8
+ } from "react";
9
+ import { cn } from "../lib/cn";
10
+
11
+ export interface TooltipProps extends Omit<
12
+ HTMLAttributes<HTMLDivElement>,
13
+ "content"
14
+ > {
15
+ content: React.ReactNode;
16
+ side?: "top" | "bottom" | "left" | "right";
17
+ delayMs?: number;
18
+ }
19
+
20
+ export const Tooltip = forwardRef<HTMLDivElement, TooltipProps>(
21
+ (
22
+ { className, content, side = "top", delayMs = 200, children, ...props },
23
+ ref,
24
+ ) => {
25
+ const [visible, setVisible] = useState(false);
26
+ const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
27
+ const wrapperRef = useRef<HTMLDivElement>(null);
28
+ const tooltipRef = useRef<HTMLDivElement>(null);
29
+ const [position, setPosition] = useState({ top: 0, left: 0 });
30
+
31
+ const show = () => {
32
+ timeoutRef.current = setTimeout(() => setVisible(true), delayMs);
33
+ };
34
+ const hide = () => {
35
+ clearTimeout(timeoutRef.current);
36
+ setVisible(false);
37
+ };
38
+
39
+ const updatePosition = useCallback(() => {
40
+ if (!wrapperRef.current || !tooltipRef.current) return;
41
+ const trigger = wrapperRef.current.getBoundingClientRect();
42
+ const tip = tooltipRef.current.getBoundingClientRect();
43
+ const gap = 8;
44
+
45
+ let top = 0;
46
+ let left = 0;
47
+
48
+ switch (side) {
49
+ case "top":
50
+ top = trigger.top - tip.height - gap;
51
+ left = trigger.left + trigger.width / 2 - tip.width / 2;
52
+ break;
53
+ case "bottom":
54
+ top = trigger.bottom + gap;
55
+ left = trigger.left + trigger.width / 2 - tip.width / 2;
56
+ break;
57
+ case "left":
58
+ top = trigger.top + trigger.height / 2 - tip.height / 2;
59
+ left = trigger.left - tip.width - gap;
60
+ break;
61
+ case "right":
62
+ top = trigger.top + trigger.height / 2 - tip.height / 2;
63
+ left = trigger.right + gap;
64
+ break;
65
+ }
66
+
67
+ setPosition({ top, left });
68
+ }, [side]);
69
+
70
+ useEffect(() => {
71
+ if (visible) {
72
+ // Use requestAnimationFrame so the tooltip DOM is painted before measuring
73
+ requestAnimationFrame(updatePosition);
74
+ }
75
+ }, [visible, updatePosition]);
76
+
77
+ useEffect(() => {
78
+ return () => clearTimeout(timeoutRef.current);
79
+ }, []);
80
+
81
+ return (
82
+ <>
83
+ <div
84
+ ref={(node) => {
85
+ (
86
+ wrapperRef as React.MutableRefObject<HTMLDivElement | null>
87
+ ).current = node;
88
+ if (typeof ref === "function") ref(node);
89
+ else if (ref)
90
+ (ref as React.MutableRefObject<HTMLDivElement | null>).current =
91
+ node;
92
+ }}
93
+ className={cn("inline-flex", className)}
94
+ onMouseEnter={show}
95
+ onMouseLeave={hide}
96
+ onFocus={show}
97
+ onBlur={hide}
98
+ {...props}
99
+ >
100
+ {children}
101
+ </div>
102
+ {visible && (
103
+ <div
104
+ ref={tooltipRef}
105
+ role="tooltip"
106
+ className="fixed z-[9999] rounded-md bg-foreground px-3 py-1.5 text-xs text-background shadow-md whitespace-nowrap pointer-events-none"
107
+ style={{
108
+ top: position.top,
109
+ left: position.left,
110
+ animation: "popup-fade-in 0.1s ease-out",
111
+ }}
112
+ >
113
+ {content}
114
+ </div>
115
+ )}
116
+ </>
117
+ );
118
+ },
119
+ );
120
+ Tooltip.displayName = "Tooltip";
@@ -0,0 +1,28 @@
1
+ import React, { forwardRef } from "react";
2
+ import { ChevronLeft, ChevronRight } from "lucide-react";
3
+ import { Button, ButtonProps } from "../Button";
4
+ import { cn } from "../../lib/cn";
5
+
6
+ export interface ArrowButtonProps extends ButtonProps {
7
+ direction?: "left" | "right";
8
+ text?: string;
9
+ }
10
+
11
+ export const ArrowButton = forwardRef<HTMLButtonElement, ArrowButtonProps>(
12
+ ({ direction = "right", text, className, children, ...props }, ref) => {
13
+ const Icon = direction === "left" ? ChevronLeft : ChevronRight;
14
+
15
+ return (
16
+ <Button
17
+ ref={ref}
18
+ className={cn("gap-1", !text && !children && "px-2", className)}
19
+ {...props}
20
+ >
21
+ {direction === "left" && <Icon className="h-4 w-4" />}
22
+ {(text || children) && <span>{text || children}</span>}
23
+ {direction === "right" && <Icon className="h-4 w-4" />}
24
+ </Button>
25
+ );
26
+ },
27
+ );
28
+ ArrowButton.displayName = "ArrowButton";
@@ -0,0 +1,38 @@
1
+ import React, { SelectHTMLAttributes, forwardRef } from "react";
2
+ import { ChevronDown } from "lucide-react";
3
+ import { cn } from "../../lib/cn";
4
+
5
+ export interface ChooseListProps extends SelectHTMLAttributes<HTMLSelectElement> {
6
+ options: { label: string; value: string | number }[];
7
+ }
8
+
9
+ export const ChooseList = forwardRef<HTMLSelectElement, ChooseListProps>(
10
+ ({ className, options, ...props }, ref) => {
11
+ return (
12
+ <div className="relative inline-block w-full">
13
+ <select
14
+ ref={ref}
15
+ className={cn(
16
+ "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring appearance-none cursor-pointer pr-10",
17
+ className,
18
+ )}
19
+ {...props}
20
+ >
21
+ {options.map((opt) => (
22
+ <option
23
+ key={opt.value}
24
+ value={opt.value}
25
+ className="bg-card text-foreground"
26
+ >
27
+ {opt.label}
28
+ </option>
29
+ ))}
30
+ </select>
31
+ <div className="pointer-events-none absolute inset-y-0 right-0 flex items-center px-3 text-muted-foreground">
32
+ <ChevronDown className="h-4 w-4" />
33
+ </div>
34
+ </div>
35
+ );
36
+ },
37
+ );
38
+ ChooseList.displayName = "ChooseList";
@@ -0,0 +1,74 @@
1
+ import React, { useState, forwardRef } from "react";
2
+ import { Plus, Minus } from "lucide-react";
3
+ import { Button } from "../Button";
4
+ import { cn } from "../../lib/cn";
5
+
6
+ export interface CounterProps {
7
+ initialValue?: number;
8
+ min?: number;
9
+ max?: number;
10
+ step?: number;
11
+ onChange?: (value: number) => void;
12
+ className?: string;
13
+ }
14
+
15
+ export const Counter = forwardRef<HTMLDivElement, CounterProps>(
16
+ (
17
+ {
18
+ initialValue = 0,
19
+ min = Number.MIN_SAFE_INTEGER,
20
+ max = Number.MAX_SAFE_INTEGER,
21
+ step = 1,
22
+ onChange,
23
+ className,
24
+ },
25
+ ref,
26
+ ) => {
27
+ const [value, setValue] = useState(initialValue);
28
+
29
+ const handleIncrement = () => {
30
+ const next = Math.min(max, value + step);
31
+ setValue(next);
32
+ onChange?.(next);
33
+ };
34
+
35
+ const handleDecrement = () => {
36
+ const next = Math.max(min, value - step);
37
+ setValue(next);
38
+ onChange?.(next);
39
+ };
40
+
41
+ return (
42
+ <div
43
+ ref={ref}
44
+ className={cn(
45
+ "inline-flex items-center rounded-lg border border-border bg-card",
46
+ className,
47
+ )}
48
+ >
49
+ <Button
50
+ variant="ghost"
51
+ size="icon"
52
+ className="h-8 w-8 rounded-r-none"
53
+ onClick={handleDecrement}
54
+ disabled={value <= min}
55
+ >
56
+ <Minus className="h-3.5 w-3.5" />
57
+ </Button>
58
+ <span className="w-10 text-center text-sm font-medium tabular-nums border-x border-border">
59
+ {value}
60
+ </span>
61
+ <Button
62
+ variant="ghost"
63
+ size="icon"
64
+ className="h-8 w-8 rounded-l-none"
65
+ onClick={handleIncrement}
66
+ disabled={value >= max}
67
+ >
68
+ <Plus className="h-3.5 w-3.5" />
69
+ </Button>
70
+ </div>
71
+ );
72
+ },
73
+ );
74
+ Counter.displayName = "Counter";
@@ -0,0 +1,78 @@
1
+ import React, { HTMLAttributes, forwardRef, useEffect } from "react";
2
+ import { X } from "lucide-react";
3
+ import { Button } from "../Button";
4
+ import { cn } from "../../lib/cn";
5
+
6
+ export interface PopupProps extends Omit<
7
+ HTMLAttributes<HTMLDivElement>,
8
+ "title"
9
+ > {
10
+ open: boolean;
11
+ onClose: () => void;
12
+ title?: React.ReactNode;
13
+ }
14
+
15
+ export const Popup = forwardRef<HTMLDivElement, PopupProps>(
16
+ ({ className, open, onClose, title, children, ...props }, ref) => {
17
+ useEffect(() => {
18
+ if (open) {
19
+ document.body.style.overflow = "hidden";
20
+ } else {
21
+ document.body.style.overflow = "";
22
+ }
23
+ return () => {
24
+ document.body.style.overflow = "";
25
+ };
26
+ }, [open]);
27
+
28
+ useEffect(() => {
29
+ const handleEscape = (e: KeyboardEvent) => {
30
+ if (e.key === "Escape") onClose();
31
+ };
32
+ if (open) document.addEventListener("keydown", handleEscape);
33
+ return () => document.removeEventListener("keydown", handleEscape);
34
+ }, [open, onClose]);
35
+
36
+ if (!open) return null;
37
+
38
+ return (
39
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-4">
40
+ {/* Backdrop */}
41
+ <div
42
+ className="fixed inset-0 bg-background/80 backdrop-blur-sm"
43
+ style={{ animation: "popup-fade-in 0.15s ease-out" }}
44
+ onClick={onClose}
45
+ />
46
+ {/* Content */}
47
+ <div
48
+ ref={ref}
49
+ role="dialog"
50
+ aria-modal="true"
51
+ className={cn(
52
+ "relative z-50 w-full max-w-lg rounded-xl border border-border bg-card p-6 shadow-2xl",
53
+ className,
54
+ )}
55
+ style={{ animation: "popup-scale-in 0.15s ease-out" }}
56
+ {...props}
57
+ >
58
+ <div className="flex items-center justify-between mb-4">
59
+ {title && (
60
+ <h2 className="text-lg font-semibold tracking-tight">{title}</h2>
61
+ )}
62
+ <Button
63
+ variant="ghost"
64
+ size="icon"
65
+ className="h-7 w-7 rounded-full ml-auto"
66
+ onClick={onClose}
67
+ aria-label="Close popup"
68
+ >
69
+ <X className="h-4 w-4" />
70
+ </Button>
71
+ </div>
72
+ <div className="text-sm text-muted-foreground">{children}</div>
73
+ </div>
74
+ </div>
75
+ );
76
+ },
77
+ );
78
+ Popup.displayName = "Popup";
@@ -0,0 +1,45 @@
1
+ import React, { HTMLAttributes, forwardRef } from "react";
2
+ import { cn } from "../../lib/cn";
3
+
4
+ export interface StatusBarProps extends HTMLAttributes<HTMLDivElement> {
5
+ status?: "idle" | "working" | "error" | "ready" | "live";
6
+ label?: string;
7
+ }
8
+
9
+ const statusStyles = {
10
+ idle: "bg-muted-foreground",
11
+ working: "bg-[hsl(var(--blue))] animate-pulse",
12
+ error: "bg-[hsl(var(--orange))]",
13
+ ready: "bg-[hsl(var(--purple))]",
14
+ live: "bg-[hsl(var(--purple))] animate-pulse",
15
+ };
16
+
17
+ export const StatusBar = forwardRef<HTMLDivElement, StatusBarProps>(
18
+ ({ className, status = "idle", label, children, ...props }, ref) => {
19
+ return (
20
+ <div
21
+ ref={ref}
22
+ role="status"
23
+ className={cn(
24
+ "inline-flex items-center gap-2 rounded-full border border-border bg-card px-3 py-1",
25
+ className,
26
+ )}
27
+ {...props}
28
+ >
29
+ <span
30
+ className={cn("h-2 w-2 rounded-full", statusStyles[status])}
31
+ aria-hidden="true"
32
+ />
33
+ <span className="text-xs font-semibold uppercase tracking-wider">
34
+ {label || status}
35
+ </span>
36
+ {children && (
37
+ <span className="text-xs text-muted-foreground border-l border-border pl-2">
38
+ {children}
39
+ </span>
40
+ )}
41
+ </div>
42
+ );
43
+ },
44
+ );
45
+ StatusBar.displayName = "StatusBar";
@@ -0,0 +1,126 @@
1
+ import React, { HTMLAttributes, forwardRef } from "react";
2
+ import { cn } from "../../lib/cn";
3
+
4
+ export interface WeekViewEvent {
5
+ id: string;
6
+ dayIndex: number;
7
+ startHour: number;
8
+ durationHours: number;
9
+ title: string;
10
+ color?: string;
11
+ }
12
+
13
+ export interface WeekViewCalendarProps extends HTMLAttributes<HTMLDivElement> {
14
+ events?: WeekViewEvent[];
15
+ startHour?: number;
16
+ endHour?: number;
17
+ }
18
+
19
+ export const WeekViewCalendar = forwardRef<
20
+ HTMLDivElement,
21
+ WeekViewCalendarProps
22
+ >(({ className, events = [], startHour = 8, endHour = 18, ...props }, ref) => {
23
+ const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
24
+ const hours = Array.from(
25
+ { length: endHour - startHour },
26
+ (_, i) => i + startHour,
27
+ );
28
+
29
+ return (
30
+ <div
31
+ ref={ref}
32
+ className={cn(
33
+ "flex flex-col rounded-lg border border-border overflow-hidden bg-card",
34
+ className,
35
+ )}
36
+ {...props}
37
+ >
38
+ {/* Header */}
39
+ <div className="grid grid-cols-8 border-b border-border bg-muted/30">
40
+ <div className="flex items-center justify-center border-r border-border p-2 text-xs font-medium text-muted-foreground w-16">
41
+ Time
42
+ </div>
43
+ {days.map((day, i) => (
44
+ <div
45
+ key={day}
46
+ className={cn(
47
+ "p-2 text-center text-sm font-semibold",
48
+ i < 6 && "border-r border-border",
49
+ )}
50
+ >
51
+ {day}
52
+ </div>
53
+ ))}
54
+ </div>
55
+
56
+ {/* Body */}
57
+ <div className="relative overflow-y-auto max-h-[500px] custom-scrollbar">
58
+ {hours.map((hour, hIndex) => (
59
+ <div
60
+ key={hour}
61
+ className={cn(
62
+ "grid grid-cols-8",
63
+ hIndex < hours.length - 1 && "border-b border-border/50",
64
+ )}
65
+ >
66
+ <div className="flex items-start justify-center border-r border-border p-2 text-xs text-muted-foreground w-16 sticky left-0 bg-card">
67
+ {`${hour === 0 ? 12 : hour > 12 ? hour - 12 : hour} ${hour >= 12 ? "PM" : "AM"}`}
68
+ </div>
69
+ {days.map((_, dIndex) => (
70
+ <div
71
+ key={dIndex}
72
+ className={cn(
73
+ "h-16 relative",
74
+ dIndex < 6 && "border-r border-border/50",
75
+ )}
76
+ />
77
+ ))}
78
+ </div>
79
+ ))}
80
+
81
+ {/* Events layer */}
82
+ <div
83
+ className="absolute inset-0 z-10 pointer-events-none"
84
+ style={{ paddingLeft: "4rem" }}
85
+ >
86
+ <div className="relative w-full h-full">
87
+ {events.map((event) => {
88
+ if (event.startHour < startHour || event.startHour >= endHour)
89
+ return null;
90
+ const top =
91
+ ((event.startHour - startHour) / (endHour - startHour)) * 100;
92
+ const height =
93
+ (event.durationHours / (endHour - startHour)) * 100;
94
+ const left = (event.dayIndex / 7) * 100;
95
+ const width = 100 / 7;
96
+
97
+ return (
98
+ <div
99
+ key={event.id}
100
+ className="absolute p-0.5 pointer-events-auto"
101
+ style={{
102
+ top: `${top}%`,
103
+ left: `${left}%`,
104
+ height: `${height}%`,
105
+ width: `${width}%`,
106
+ }}
107
+ >
108
+ <div
109
+ className="h-full w-full rounded-md border border-white/10 p-1.5 text-xs font-semibold text-white overflow-hidden shadow-sm transition-transform hover:scale-[1.02] hover:shadow-md cursor-pointer"
110
+ style={{
111
+ backgroundColor: event.color || "hsl(var(--primary))",
112
+ }}
113
+ title={event.title}
114
+ >
115
+ {event.title}
116
+ </div>
117
+ </div>
118
+ );
119
+ })}
120
+ </div>
121
+ </div>
122
+ </div>
123
+ </div>
124
+ );
125
+ });
126
+ WeekViewCalendar.displayName = "WeekViewCalendar";