@xenide-io/the-old-ui-theme 0.5.4 → 0.6.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.
Files changed (63) hide show
  1. package/dist/{Chip-C5hRFAhe.d.mts → Chip-DzC5xtK9.d.mts} +8 -3
  2. package/dist/{Chip-C5hRFAhe.d.ts → Chip-DzC5xtK9.d.ts} +8 -3
  3. package/dist/chunk-3G43JEI3.mjs +2564 -0
  4. package/dist/index.d.mts +2 -2
  5. package/dist/index.d.ts +2 -2
  6. package/dist/index.js +7 -2
  7. package/dist/index.mjs +3 -1
  8. package/dist/tailwind-preset.js +1 -0
  9. package/dist/tailwind-preset.mjs +1 -0
  10. package/dist/ui.d.mts +4 -531
  11. package/dist/ui.d.ts +4 -531
  12. package/dist/ui.js +407 -2504
  13. package/dist/ui.mjs +5 -91
  14. package/package.json +1 -1
  15. package/src/app/globals.css +18 -0
  16. package/src/components/ui/Typography.tsx +12 -4
  17. package/src/components/ui/index.ts +7 -184
  18. package/src/components/ui/typography.test.tsx +27 -0
  19. package/src/styles/suite-skin.css +21 -1
  20. package/src/styles/typography.css +72 -3
  21. package/tailwind-preset.ts +1 -0
  22. package/dist/chunk-SBBKXV35.mjs +0 -4620
  23. package/src/components/ui/AlertDialog.tsx +0 -62
  24. package/src/components/ui/AppLayout.tsx +0 -27
  25. package/src/components/ui/Autocomplete.tsx +0 -106
  26. package/src/components/ui/Banner.tsx +0 -63
  27. package/src/components/ui/ButtonGroup.tsx +0 -16
  28. package/src/components/ui/ChatBubble.tsx +0 -71
  29. package/src/components/ui/Collapsible.tsx +0 -46
  30. package/src/components/ui/Combobox.tsx +0 -119
  31. package/src/components/ui/ContextMenu.tsx +0 -81
  32. package/src/components/ui/DataTable.tsx +0 -133
  33. package/src/components/ui/DatePicker.tsx +0 -96
  34. package/src/components/ui/Dialog.tsx +0 -75
  35. package/src/components/ui/Divider.tsx +0 -39
  36. package/src/components/ui/Drawer.tsx +0 -71
  37. package/src/components/ui/HoverCard.tsx +0 -36
  38. package/src/components/ui/Indicator.tsx +0 -61
  39. package/src/components/ui/InputGroup.tsx +0 -16
  40. package/src/components/ui/Lettermark.tsx +0 -47
  41. package/src/components/ui/LoadingBar.tsx +0 -25
  42. package/src/components/ui/Menubar.tsx +0 -66
  43. package/src/components/ui/Metric.tsx +0 -34
  44. package/src/components/ui/Navigation.tsx +0 -94
  45. package/src/components/ui/NumberField.tsx +0 -76
  46. package/src/components/ui/Popover.tsx +0 -73
  47. package/src/components/ui/ProgressCircle.tsx +0 -63
  48. package/src/components/ui/Rating.tsx +0 -65
  49. package/src/components/ui/Resizable.tsx +0 -71
  50. package/src/components/ui/Row.tsx +0 -51
  51. package/src/components/ui/ScrollArea.tsx +0 -54
  52. package/src/components/ui/Sidebar.tsx +0 -79
  53. package/src/components/ui/Snack.tsx +0 -48
  54. package/src/components/ui/Splotch.tsx +0 -33
  55. package/src/components/ui/Stepper.tsx +0 -74
  56. package/src/components/ui/Tabs.tsx +0 -53
  57. package/src/components/ui/Tag.tsx +0 -65
  58. package/src/components/ui/Terminal.tsx +0 -54
  59. package/src/components/ui/ThemeManager.tsx +0 -18
  60. package/src/components/ui/Timeline.tsx +0 -60
  61. package/src/components/ui/Toast.tsx +0 -70
  62. package/src/components/ui/Toggle.tsx +0 -62
  63. package/src/components/ui/Widget.tsx +0 -31
@@ -1,34 +0,0 @@
1
- import { type ReactNode, type HTMLAttributes } from "react";
2
- import { cn } from "@/lib/cn";
3
-
4
- export interface MetricProps extends HTMLAttributes<HTMLDivElement> {
5
- value: string | number;
6
- label: string;
7
- badge?: ReactNode;
8
- trend?: ReactNode;
9
- icon?: ReactNode;
10
- }
11
-
12
- export function Metric({
13
- value,
14
- label,
15
- badge,
16
- trend,
17
- icon,
18
- className,
19
- ...props
20
- }: MetricProps) {
21
- return (
22
- <div className={cn("ph-metric", className)} {...props}>
23
- <div className="ph-metric__header">
24
- {icon && <span className="ph-metric__icon">{icon}</span>}
25
- <span className="ph-metric__label">{label}</span>
26
- {badge && <span className="ph-metric__badge">{badge}</span>}
27
- </div>
28
- <div className="ph-metric__value">{value}</div>
29
- {trend && <div className="ph-metric__trend">{trend}</div>}
30
- </div>
31
- );
32
- }
33
-
34
- Metric.displayName = "Metric";
@@ -1,94 +0,0 @@
1
- import { IconChevronLeft, IconChevronRight } from "@/components/icons";
2
- import { cn } from "@/lib/cn";
3
-
4
- export interface BreadcrumbItem {
5
- label: string;
6
- href?: string;
7
- current?: boolean;
8
- }
9
-
10
- export interface BreadcrumbsProps {
11
- items: BreadcrumbItem[];
12
- label?: string;
13
- className?: string;
14
- }
15
-
16
- export function Breadcrumbs({ items, label = "Breadcrumb", className }: BreadcrumbsProps) {
17
- return (
18
- <nav aria-label={label} className={cn("ph-breadcrumbs", className)}>
19
- {items.map((item, index) => (
20
- <span key={`${item.label}-${index}`} className="contents">
21
- {index > 0 ? <span className="ph-breadcrumb-sep" aria-hidden="true">/</span> : null}
22
- {item.current || !item.href ? (
23
- <span className="ph-breadcrumb-current" aria-current={item.current ? "page" : undefined}>
24
- {item.label}
25
- </span>
26
- ) : (
27
- <a href={item.href} className="ph-breadcrumb-link">
28
- {item.label}
29
- </a>
30
- )}
31
- </span>
32
- ))}
33
- </nav>
34
- );
35
- }
36
-
37
- export interface PaginationProps {
38
- page: number;
39
- totalPages: number;
40
- onPageChange?: (page: number) => void;
41
- label?: string;
42
- compact?: boolean;
43
- className?: string;
44
- }
45
-
46
- export function Pagination({
47
- page,
48
- totalPages,
49
- onPageChange,
50
- label = "Pagination",
51
- compact = false,
52
- className,
53
- }: PaginationProps) {
54
- const pages = compact ? [Math.max(1, page - 1), page, Math.min(totalPages, page + 1)] : [1, 2, 3, totalPages];
55
- const uniquePages = Array.from(new Set(pages)).filter((n) => n >= 1 && n <= totalPages);
56
-
57
- return (
58
- <nav aria-label={label} className={cn("ph-pagination", className)}>
59
- <button
60
- type="button"
61
- className="ph-pagination-btn"
62
- disabled={page <= 1}
63
- aria-label="Previous page"
64
- onClick={() => onPageChange?.(page - 1)}
65
- >
66
- <IconChevronLeft className="mx-auto h-4 w-4" aria-hidden />
67
- </button>
68
- {uniquePages.map((pageNumber, index) => (
69
- <span key={pageNumber} className="contents">
70
- {!compact && index === uniquePages.length - 1 && pageNumber > 4 ? (
71
- <span className="ph-pagination-ellipsis" aria-hidden="true">...</span>
72
- ) : null}
73
- <button
74
- type="button"
75
- className={cn("ph-pagination-btn", pageNumber === page && "ph-pagination-btn-active")}
76
- aria-current={pageNumber === page ? "page" : undefined}
77
- onClick={() => onPageChange?.(pageNumber)}
78
- >
79
- {pageNumber}
80
- </button>
81
- </span>
82
- ))}
83
- <button
84
- type="button"
85
- className="ph-pagination-btn"
86
- disabled={page >= totalPages}
87
- aria-label="Next page"
88
- onClick={() => onPageChange?.(page + 1)}
89
- >
90
- <IconChevronRight className="mx-auto h-4 w-4" aria-hidden />
91
- </button>
92
- </nav>
93
- );
94
- }
@@ -1,76 +0,0 @@
1
- "use client";
2
-
3
- import { useState, type InputHTMLAttributes } from "react";
4
- import { cn } from "@/lib/cn";
5
-
6
- export interface NumberFieldProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "onChange"> {
7
- value?: number;
8
- onChange?: (value: number) => void;
9
- min?: number;
10
- max?: number;
11
- step?: number;
12
- label?: string;
13
- }
14
-
15
- export function NumberField({
16
- value: controlledValue,
17
- onChange,
18
- min,
19
- max,
20
- step = 1,
21
- label,
22
- className,
23
- disabled,
24
- defaultValue,
25
- ...inputProps
26
- }: NumberFieldProps) {
27
- const [internal, setInternal] = useState(controlledValue ?? 0);
28
- const value = controlledValue ?? internal;
29
-
30
- const set = (v: number) => {
31
- const clamped = Math.max(min ?? -Infinity, Math.min(max ?? Infinity, v));
32
- setInternal(clamped);
33
- onChange?.(clamped);
34
- };
35
-
36
- return (
37
- <div className={cn("ph-number-field", className)}>
38
- {label && <label className="ph-label">{label}</label>}
39
- <div className="ph-number-field__control">
40
- <button
41
- type="button"
42
- disabled={disabled || (min !== undefined && value <= min)}
43
- onClick={() => set(value - step)}
44
- className="ph-number-field__btn"
45
- aria-label="Decrease"
46
- >
47
- <svg width="12" height="2" viewBox="0 0 12 2" fill="currentColor"><rect y="0" width="12" height="2" rx="1" /></svg>
48
- </button>
49
- <input
50
- type="number"
51
- value={value}
52
- onChange={(e) => set(Number(e.target.value))}
53
- disabled={disabled}
54
- min={min}
55
- max={max}
56
- step={step}
57
- className="ph-number-field__input"
58
- {...inputProps}
59
- />
60
- <button
61
- type="button"
62
- disabled={disabled || (max !== undefined && value >= max)}
63
- onClick={() => set(value + step)}
64
- className="ph-number-field__btn"
65
- aria-label="Increase"
66
- >
67
- <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
68
- <path d="M6 2v8M2 6h8" />
69
- </svg>
70
- </button>
71
- </div>
72
- </div>
73
- );
74
- }
75
-
76
- NumberField.displayName = "NumberField";
@@ -1,73 +0,0 @@
1
- import { useState, useRef, useEffect, type ReactNode, type HTMLAttributes } from "react";
2
- import { cn } from "@/lib/cn";
3
-
4
- export interface PopoverProps extends HTMLAttributes<HTMLDivElement> {
5
- trigger: ReactNode;
6
- children: ReactNode;
7
- open?: boolean;
8
- onOpenChange?: (open: boolean) => void;
9
- placement?: "top" | "bottom" | "left" | "right";
10
- matchTriggerWidth?: boolean;
11
- padded?: boolean;
12
- }
13
-
14
- export function Popover({
15
- trigger,
16
- children,
17
- open: controlledOpen,
18
- onOpenChange,
19
- placement = "bottom",
20
- matchTriggerWidth = false,
21
- padded = true,
22
- className,
23
- ...props
24
- }: PopoverProps) {
25
- const [internalOpen, setInternalOpen] = useState(false);
26
- const isOpen = controlledOpen ?? internalOpen;
27
- const setOpen = onOpenChange ?? setInternalOpen;
28
-
29
- const triggerRef = useRef<HTMLDivElement>(null);
30
- const panelRef = useRef<HTMLDivElement>(null);
31
-
32
- useEffect(() => {
33
- if (!isOpen) return;
34
- const handleClick = (e: MouseEvent) => {
35
- if (
36
- triggerRef.current && !triggerRef.current.contains(e.target as Node) &&
37
- panelRef.current && !panelRef.current.contains(e.target as Node)
38
- ) {
39
- setOpen(false);
40
- }
41
- };
42
- document.addEventListener("mousedown", handleClick);
43
- return () => document.removeEventListener("mousedown", handleClick);
44
- }, [isOpen, setOpen]);
45
-
46
- return (
47
- <div className={cn("ph-popover relative inline-flex", className)} {...props}>
48
- <div
49
- ref={triggerRef}
50
- onClick={() => setOpen(!isOpen)}
51
- className="ph-popover__trigger"
52
- >
53
- {trigger}
54
- </div>
55
- {isOpen && (
56
- <div
57
- ref={panelRef}
58
- className={cn(
59
- "ph-popover__panel",
60
- `ph-popover--${placement}`,
61
- padded && "ph-popover__panel--padded",
62
- matchTriggerWidth && "ph-popover__panel--match"
63
- )}
64
- style={matchTriggerWidth ? { width: triggerRef.current?.offsetWidth } : undefined}
65
- >
66
- {children}
67
- </div>
68
- )}
69
- </div>
70
- );
71
- }
72
-
73
- Popover.displayName = "Popover";
@@ -1,63 +0,0 @@
1
- import type { ReactNode, HTMLAttributes } from "react";
2
- import { cn } from "@/lib/cn";
3
-
4
- export interface ProgressCircleProps extends HTMLAttributes<HTMLSpanElement> {
5
- progress: number;
6
- size?: number;
7
- strokeWidth?: number;
8
- children?: ReactNode;
9
- }
10
-
11
- export function ProgressCircle({
12
- progress,
13
- size = 40,
14
- strokeWidth: sw,
15
- children,
16
- className,
17
- ...props
18
- }: ProgressCircleProps) {
19
- const stroke = sw ?? size * 0.08;
20
- const radius = (size - stroke) / 2;
21
- const circumference = 2 * Math.PI * radius;
22
- const offset = circumference - (Math.min(1, Math.max(0, progress)) * circumference);
23
-
24
- return (
25
- <span
26
- className={cn("ph-progress-circle", className)}
27
- style={{ width: size, height: size }}
28
- role="progressbar"
29
- aria-valuenow={Math.round(progress * 100)}
30
- aria-valuemin={0}
31
- aria-valuemax={100}
32
- {...props}
33
- >
34
- <svg width={size} height={size}>
35
- <circle
36
- cx={size / 2}
37
- cy={size / 2}
38
- r={radius}
39
- fill="none"
40
- stroke="currentColor"
41
- strokeWidth={stroke}
42
- opacity={0.15}
43
- />
44
- <circle
45
- cx={size / 2}
46
- cy={size / 2}
47
- r={radius}
48
- fill="none"
49
- stroke="currentColor"
50
- strokeWidth={stroke}
51
- strokeDasharray={circumference}
52
- strokeDashoffset={offset}
53
- strokeLinecap="round"
54
- transform={`rotate(-90 ${size / 2} ${size / 2})`}
55
- className="ph-progress-circle__bar"
56
- />
57
- </svg>
58
- {children && <span className="ph-progress-circle__content">{children}</span>}
59
- </span>
60
- );
61
- }
62
-
63
- ProgressCircle.displayName = "ProgressCircle";
@@ -1,65 +0,0 @@
1
- import type { ComponentPropsWithoutRef } from "react";
2
- import { IconStar } from "@/components/icons";
3
- import { cn } from "@/lib/cn";
4
-
5
- export interface RatingProps extends Omit<ComponentPropsWithoutRef<"div">, "onChange"> {
6
- label?: string;
7
- value?: number;
8
- max?: number;
9
- readOnly?: boolean;
10
- onChange?: (value: number) => void;
11
- wrapperClassName?: string;
12
- }
13
-
14
- export function Rating({
15
- label,
16
- value = 0,
17
- max = 5,
18
- readOnly = true,
19
- onChange,
20
- className,
21
- wrapperClassName,
22
- ...props
23
- }: RatingProps) {
24
- const stars = Array.from({ length: max }, (_, index) => index + 1);
25
-
26
- return (
27
- <div className={cn("ph-field", wrapperClassName)}>
28
- {label && <span className="ph-label">{label}</span>}
29
- <div
30
- className={cn("ph-rating flex", className)}
31
- role={readOnly ? "img" : "radiogroup"}
32
- aria-label={label ?? `Rating: ${value} out of ${max}`}
33
- {...props}
34
- >
35
- {stars.map((star) => {
36
- const active = star <= value;
37
-
38
- if (readOnly) {
39
- return (
40
- <IconStar
41
- key={star}
42
- className={cn("h-6 w-6", active ? "text-amber-500" : "text-ph-border")}
43
- aria-hidden
44
- />
45
- );
46
- }
47
-
48
- return (
49
- <button
50
- key={star}
51
- type="button"
52
- className="rounded p-0.5 transition hover:scale-105 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ph-brand"
53
- role="radio"
54
- aria-checked={active}
55
- aria-label={`${star} out of ${max}`}
56
- onClick={() => onChange?.(star)}
57
- >
58
- <IconStar className={cn("h-6 w-6", active ? "text-amber-500" : "text-ph-border")} />
59
- </button>
60
- );
61
- })}
62
- </div>
63
- </div>
64
- );
65
- }
@@ -1,71 +0,0 @@
1
- "use client";
2
-
3
- import { useState, useRef, useCallback, type ReactNode, type HTMLAttributes } from "react";
4
- import { cn } from "@/lib/cn";
5
-
6
- export interface ResizableProps extends HTMLAttributes<HTMLDivElement> {
7
- left: ReactNode;
8
- right: ReactNode;
9
- defaultLeftWidth?: number;
10
- minLeftWidth?: number;
11
- minRightWidth?: number;
12
- }
13
-
14
- export function Resizable({
15
- left,
16
- right,
17
- defaultLeftWidth = 50,
18
- minLeftWidth = 20,
19
- minRightWidth = 20,
20
- className,
21
- ...props
22
- }: ResizableProps) {
23
- const [leftPercent, setLeftPercent] = useState(defaultLeftWidth);
24
- const dragging = useRef(false);
25
- const containerRef = useRef<HTMLDivElement>(null);
26
-
27
- const handleMouseDown = useCallback((e: React.MouseEvent) => {
28
- e.preventDefault();
29
- dragging.current = true;
30
- const startX = e.clientX;
31
- const startPercent = leftPercent;
32
-
33
- const handleMove = (ev: MouseEvent) => {
34
- if (!dragging.current || !containerRef.current) return;
35
- const rect = containerRef.current.getBoundingClientRect();
36
- const delta = ((ev.clientX - startX) / rect.width) * 100;
37
- const newVal = Math.max(minLeftWidth, Math.min(100 - minRightWidth, startPercent + delta));
38
- setLeftPercent(newVal);
39
- };
40
-
41
- const handleUp = () => {
42
- dragging.current = false;
43
- document.removeEventListener("mousemove", handleMove);
44
- document.removeEventListener("mouseup", handleUp);
45
- };
46
-
47
- document.addEventListener("mousemove", handleMove);
48
- document.addEventListener("mouseup", handleUp);
49
- }, [leftPercent, minLeftWidth, minRightWidth]);
50
-
51
- return (
52
- <div
53
- ref={containerRef}
54
- className={cn("ph-resizable flex overflow-hidden", className)}
55
- {...props}
56
- >
57
- <div className="ph-resizable__left overflow-auto" style={{ width: `${leftPercent}%` }}>
58
- {left}
59
- </div>
60
- <div
61
- className="ph-resizable__handle cursor-col-resize shrink-0"
62
- onMouseDown={handleMouseDown}
63
- />
64
- <div className="ph-resizable__right flex-1 overflow-auto">
65
- {right}
66
- </div>
67
- </div>
68
- );
69
- }
70
-
71
- Resizable.displayName = "Resizable";
@@ -1,51 +0,0 @@
1
- import { forwardRef, type ReactElement, type ReactNode, type HTMLAttributes } from "react";
2
- import { cn } from "@/lib/cn";
3
-
4
- export interface RowProps extends HTMLAttributes<HTMLDivElement> {
5
- icon?: ReactElement | null;
6
- sideIcon?: ReactElement | null;
7
- status?: "default" | "success" | "warning" | "danger" | "muted";
8
- fullWidth?: boolean;
9
- center?: boolean;
10
- size?: "sm" | "md" | "lg";
11
- children?: ReactNode;
12
- }
13
-
14
- export const Row = forwardRef<HTMLDivElement, RowProps>(function Row({
15
- icon,
16
- sideIcon,
17
- status = "default",
18
- fullWidth = false,
19
- center = false,
20
- size = "md",
21
- className,
22
- children,
23
- ...props
24
- }, ref) {
25
- const symbolic = children === null || children === undefined || children === false;
26
-
27
- return (
28
- <div
29
- ref={ref}
30
- className={cn(
31
- "ph-row",
32
- status !== "default" && `ph-row--${status}`,
33
- fullWidth && "ph-row--full-width",
34
- center && "ph-row--center",
35
- size === "sm" && "ph-row--sm",
36
- size === "lg" && "ph-row--lg",
37
- symbolic && "ph-row--symbolic",
38
- className
39
- )}
40
- {...props}
41
- >
42
- <div className="ph-row__main">
43
- {icon && <span className="ph-row__icon">{icon}</span>}
44
- {!symbolic && <div className="ph-row__content">{children}</div>}
45
- {sideIcon && <span className="ph-row__side-icon">{sideIcon}</span>}
46
- </div>
47
- </div>
48
- );
49
- });
50
-
51
- Row.displayName = "Row";
@@ -1,54 +0,0 @@
1
- "use client";
2
-
3
- import { useRef, useState, useEffect, type ReactNode, type HTMLAttributes } from "react";
4
- import { cn } from "@/lib/cn";
5
-
6
- export interface ScrollAreaProps extends HTMLAttributes<HTMLDivElement> {
7
- children: ReactNode;
8
- orientation?: "vertical" | "horizontal" | "both";
9
- }
10
-
11
- export function ScrollArea({
12
- children,
13
- orientation = "vertical",
14
- className,
15
- ...props
16
- }: ScrollAreaProps) {
17
- const viewportRef = useRef<HTMLDivElement>(null);
18
- const [showTop, setShowTop] = useState(false);
19
- const [showBottom, setShowBottom] = useState(false);
20
-
21
- useEffect(() => {
22
- const el = viewportRef.current;
23
- if (!el) return;
24
- const update = () => {
25
- setShowTop(el.scrollTop > 4);
26
- setShowBottom(el.scrollTop < el.scrollHeight - el.clientHeight - 4);
27
- };
28
- update();
29
- el.addEventListener("scroll", update, { passive: true });
30
- const ro = new ResizeObserver(update);
31
- ro.observe(el);
32
- return () => { el.removeEventListener("scroll", update); ro.disconnect(); };
33
- }, []);
34
-
35
- return (
36
- <div
37
- className={cn(
38
- "ph-scroll-area relative overflow-hidden",
39
- orientation === "horizontal" && "overflow-x-auto",
40
- orientation === "both" && "overflow-auto",
41
- className
42
- )}
43
- {...props}
44
- >
45
- {showTop && <div className="ph-scroll-area__shadow-top" />}
46
- <div ref={viewportRef} className="ph-scroll-area__viewport overflow-y-auto overscroll-contain">
47
- {children}
48
- </div>
49
- {showBottom && <div className="ph-scroll-area__shadow-bottom" />}
50
- </div>
51
- );
52
- }
53
-
54
- ScrollArea.displayName = "ScrollArea";
@@ -1,79 +0,0 @@
1
- "use client";
2
-
3
- import { type ReactNode } from "react";
4
- import { cn } from "@/lib/cn";
5
-
6
- export interface SidebarGroup {
7
- label?: string;
8
- items: SidebarItemDef[];
9
- }
10
-
11
- export interface SidebarItemDef {
12
- label: string;
13
- icon?: ReactNode;
14
- href?: string;
15
- badge?: ReactNode;
16
- active?: boolean;
17
- onClick?: () => void;
18
- }
19
-
20
- export interface SidebarProps {
21
- groups: SidebarGroup[];
22
- className?: string;
23
- header?: ReactNode;
24
- footer?: ReactNode;
25
- children?: ReactNode;
26
- collapsed?: boolean;
27
- onCollapse?: (collapsed: boolean) => void;
28
- }
29
-
30
- export function Sidebar({ groups, className, header, footer, children, collapsed = false, onCollapse }: SidebarProps) {
31
- return (
32
- <nav className={cn("ph-sidebar flex flex-col h-full", collapsed && "ph-sidebar--collapsed", className)}>
33
- {header && <div className="ph-sidebar__header px-3 py-3 border-b border-ph-border-subtle">{header}</div>}
34
- <div className="ph-sidebar__body flex-1 overflow-y-auto px-2 py-3 space-y-5">
35
- {groups.map((group) => (
36
- <div key={group.label ?? 'default'} className="ph-sidebar__group">
37
- {group.label && !collapsed && (
38
- <h3 className="ph-sidebar__group-label px-2 mb-1 text-[10px] font-semibold uppercase tracking-wider text-ph-mutedtext">
39
- {group.label}
40
- </h3>
41
- )}
42
- <div className="ph-sidebar__items space-y-0.5">
43
- {group.items.map((item) => (
44
- <SidebarItem key={item.label} {...item} collapsed={collapsed} />
45
- ))}
46
- </div>
47
- </div>
48
- ))}
49
- </div>
50
- {children}
51
- {footer && <div className="ph-sidebar__footer px-3 py-3 border-t border-ph-border-subtle">{footer}</div>}
52
- </nav>
53
- );
54
- }
55
-
56
- function SidebarItem({ label, icon, badge, active, href, onClick, collapsed }: SidebarItemDef & { collapsed: boolean }) {
57
- const Comp = onClick ? "button" : "a";
58
- return (
59
- <Comp
60
- type={onClick ? "button" as const : undefined}
61
- href={onClick ? undefined : href}
62
- onClick={onClick}
63
- className={cn(
64
- "ph-sidebar__item flex items-center gap-2.5 w-full rounded-md px-2.5 py-2 text-sm text-left transition",
65
- active
66
- ? "bg-ph-selected-bg text-ph-selected-text font-medium"
67
- : "text-ph-subtle hover:bg-ph-muted hover:text-ph-ink",
68
- collapsed && "justify-center px-2"
69
- )}
70
- title={collapsed ? label : undefined}
71
- >
72
- {icon && <span className={cn("ph-sidebar__item-icon shrink-0 flex items-center justify-center", collapsed ? "w-5 h-5" : "w-4 h-4")}>{icon}</span>}
73
- {!collapsed && <span className="ph-sidebar__item-label truncate flex-1">{label}</span>}
74
- {!collapsed && badge && <span className="ph-sidebar__item-badge shrink-0">{badge}</span>}
75
- </Comp>
76
- );
77
- }
78
-
79
- Sidebar.displayName = "Sidebar";