@xenide-io/the-old-ui-theme 0.2.6 → 0.3.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 (126) hide show
  1. package/README.md +1 -1
  2. package/dist/chunk-FDMD3IIN.mjs +3831 -0
  3. package/dist/index-D1RTT2Ap.d.mts +1044 -0
  4. package/dist/index-D1RTT2Ap.d.ts +1044 -0
  5. package/dist/index.d.mts +11 -758
  6. package/dist/index.d.ts +11 -758
  7. package/dist/index.js +1749 -768
  8. package/dist/index.mjs +312 -2846
  9. package/dist/tailwind-preset.js +7 -0
  10. package/dist/tailwind-preset.mjs +7 -0
  11. package/dist/ui.d.mts +4 -0
  12. package/dist/ui.d.ts +4 -0
  13. package/dist/ui.js +3864 -0
  14. package/dist/ui.mjs +169 -0
  15. package/package.json +28 -6
  16. package/src/app/globals.css +1005 -227
  17. package/src/components/charts/index.ts +8 -0
  18. package/src/components/charts/ph-insight-charts.tsx +162 -0
  19. package/src/components/dashboard/DashboardFiltersBar.tsx +19 -0
  20. package/src/components/dashboard/DashboardGrid.tsx +23 -0
  21. package/src/components/dashboard/DashboardInsightPlaceholder.tsx +37 -0
  22. package/src/components/dashboard/DashboardKpiCard.tsx +25 -0
  23. package/src/components/dashboard/DashboardToolbar.tsx +23 -0
  24. package/src/components/dashboard/DashboardWell.tsx +10 -0
  25. package/src/components/dashboard/InsightMiniCard.tsx +26 -0
  26. package/src/components/dashboard/InsightShell.tsx +37 -0
  27. package/src/components/dashboard/InsightShellHeader.tsx +22 -0
  28. package/src/components/dashboard/MiniTrendBars.tsx +51 -0
  29. package/src/components/dashboard/index.ts +31 -0
  30. package/src/components/dashboard/types.ts +9 -0
  31. package/src/components/icons/IconBase.tsx +47 -0
  32. package/src/components/icons/createIconBase.tsx +32 -0
  33. package/src/components/icons/icons.tsx +557 -0
  34. package/src/components/icons/index.ts +3 -0
  35. package/src/components/sections/AccordionShowcase.tsx +45 -0
  36. package/src/components/sections/AlertShowcase.tsx +40 -0
  37. package/src/components/sections/AvatarShowcase.tsx +80 -0
  38. package/src/components/sections/BadgeShowcase.tsx +65 -0
  39. package/src/components/sections/ButtonShowcase.tsx +308 -0
  40. package/src/components/sections/CalendarShowcase.tsx +35 -0
  41. package/src/components/sections/CardShowcase.tsx +159 -0
  42. package/src/components/sections/ChartShowcase.tsx +190 -0
  43. package/src/components/sections/CodeMockupShowcase.tsx +56 -0
  44. package/src/components/sections/ColorPalette.tsx +117 -0
  45. package/src/components/sections/CommandPaletteShowcase.tsx +48 -0
  46. package/src/components/sections/CountdownShowcase.tsx +43 -0
  47. package/src/components/sections/DashboardShowcase.tsx +191 -0
  48. package/src/components/sections/DiffShowcase.tsx +70 -0
  49. package/src/components/sections/DrawerShowcase.tsx +97 -0
  50. package/src/components/sections/DropdownShowcase.tsx +98 -0
  51. package/src/components/sections/EmptyStateShowcase.tsx +50 -0
  52. package/src/components/sections/FilterChipsShowcase.tsx +98 -0
  53. package/src/components/sections/FormShowcase.tsx +127 -0
  54. package/src/components/sections/HoverCardShowcase.tsx +50 -0
  55. package/src/components/sections/IconShowcase.tsx +127 -0
  56. package/src/components/sections/IndicatorShowcase.tsx +52 -0
  57. package/src/components/sections/KbdShowcase.tsx +57 -0
  58. package/src/components/sections/ModalShowcase.tsx +211 -0
  59. package/src/components/sections/NavigationShowcase.tsx +199 -0
  60. package/src/components/sections/ProductLayoutsShowcase.tsx +138 -0
  61. package/src/components/sections/ProgressShowcase.tsx +82 -0
  62. package/src/components/sections/SegmentedControlShowcase.tsx +49 -0
  63. package/src/components/sections/SetupThemeShowcase.tsx +262 -0
  64. package/src/components/sections/StackShowcase.tsx +33 -0
  65. package/src/components/sections/StatShowcase.tsx +129 -0
  66. package/src/components/sections/StepperShowcase.tsx +37 -0
  67. package/src/components/sections/SwapShowcase.tsx +99 -0
  68. package/src/components/sections/TabShowcase.tsx +84 -0
  69. package/src/components/sections/TableShowcase.tsx +140 -0
  70. package/src/components/sections/TimelineShowcase.tsx +83 -0
  71. package/src/components/sections/ToastShowcase.tsx +108 -0
  72. package/src/components/sections/TooltipShowcase.tsx +44 -0
  73. package/src/components/sections/UtilityShowcase.tsx +81 -0
  74. package/src/components/ui/Accordion.tsx +77 -0
  75. package/src/components/ui/Alert.tsx +77 -0
  76. package/src/components/ui/AuthLayout.tsx +58 -0
  77. package/src/components/ui/Avatar.tsx +97 -0
  78. package/src/components/ui/Badge.tsx +60 -0
  79. package/src/components/ui/Button.tsx +120 -0
  80. package/src/components/ui/ButtonChrome.tsx +20 -0
  81. package/src/components/ui/Calendar.tsx +115 -0
  82. package/src/components/ui/Card.tsx +70 -0
  83. package/src/components/ui/ChatBubble.tsx +71 -0
  84. package/src/components/ui/CodeBlock.tsx +48 -0
  85. package/src/components/ui/CollapsibleSection.tsx +49 -0
  86. package/src/components/ui/CommandPalette.tsx +137 -0
  87. package/src/components/ui/ComponentDocs.tsx +52 -0
  88. package/src/components/ui/Drawer.tsx +71 -0
  89. package/src/components/ui/DropdownMenu.tsx +262 -0
  90. package/src/components/ui/EmptyState.tsx +33 -0
  91. package/src/components/ui/FilterBar.tsx +165 -0
  92. package/src/components/ui/FilterChips.tsx +80 -0
  93. package/src/components/ui/FilterControls.tsx +30 -0
  94. package/src/components/ui/FormField.tsx +69 -0
  95. package/src/components/ui/HoverCard.tsx +36 -0
  96. package/src/components/ui/Indicator.tsx +61 -0
  97. package/src/components/ui/Input.tsx +385 -0
  98. package/src/components/ui/Kbd.tsx +113 -0
  99. package/src/components/ui/Loader.tsx +96 -0
  100. package/src/components/ui/Modal.tsx +105 -0
  101. package/src/components/ui/Navigation.tsx +94 -0
  102. package/src/components/ui/Panel.tsx +18 -0
  103. package/src/components/ui/Progress.tsx +59 -0
  104. package/src/components/ui/Rating.tsx +65 -0
  105. package/src/components/ui/SearchInput.tsx +106 -0
  106. package/src/components/ui/SegmentedControl.tsx +44 -0
  107. package/src/components/ui/SettingsLayout.tsx +94 -0
  108. package/src/components/ui/ShowcaseWrapper.tsx +124 -0
  109. package/src/components/ui/Stat.tsx +39 -0
  110. package/src/components/ui/Stepper.tsx +74 -0
  111. package/src/components/ui/Table.tsx +55 -0
  112. package/src/components/ui/Tabs.tsx +53 -0
  113. package/src/components/ui/Terminal.tsx +54 -0
  114. package/src/components/ui/ThemeDomSync.tsx +17 -0
  115. package/src/components/ui/ThemeManager.tsx +18 -0
  116. package/src/components/ui/ThemeSwitcher.tsx +46 -0
  117. package/src/components/ui/Timeline.tsx +60 -0
  118. package/src/components/ui/Toast.tsx +70 -0
  119. package/src/components/ui/Typography.tsx +129 -0
  120. package/src/components/ui/first-slice.test.tsx +98 -0
  121. package/src/components/ui/index.ts +151 -0
  122. package/src/components/ui/interaction-primitives.test.tsx +77 -0
  123. package/src/components/ui/kbd-icons.test.tsx +90 -0
  124. package/src/components/ui/product-patterns.test.tsx +77 -0
  125. package/src/styles/themes.css +528 -0
  126. package/tailwind-preset.ts +7 -0
@@ -0,0 +1,96 @@
1
+ import type { ComponentPropsWithoutRef, ReactNode } from "react";
2
+ import { cn } from "@/lib/cn";
3
+
4
+ export type LoaderVariant = "spinner" | "dots" | "pulse";
5
+ export type LoaderSize = "sm" | "md" | "lg";
6
+ export type SkeletonShape = "title" | "text" | "short-text" | "avatar" | "button" | "block";
7
+
8
+ export interface LoaderProps extends Omit<ComponentPropsWithoutRef<"span">, "children"> {
9
+ variant?: LoaderVariant;
10
+ size?: LoaderSize;
11
+ label?: string;
12
+ }
13
+
14
+ export interface SkeletonProps extends ComponentPropsWithoutRef<"span"> {
15
+ shape?: SkeletonShape;
16
+ }
17
+
18
+ export interface LoadingStateProps extends Omit<ComponentPropsWithoutRef<"div">, "title"> {
19
+ label?: string;
20
+ title?: ReactNode;
21
+ description?: ReactNode;
22
+ variant?: LoaderVariant;
23
+ size?: LoaderSize;
24
+ }
25
+
26
+ const loaderSizeClass: Record<LoaderSize, string> = {
27
+ sm: "ph-loader-sm",
28
+ md: "ph-loader-md",
29
+ lg: "ph-loader-lg",
30
+ };
31
+
32
+ const skeletonShapeClass: Record<SkeletonShape, string> = {
33
+ title: "ph-skeleton-title",
34
+ text: "ph-skeleton-text",
35
+ "short-text": "ph-skeleton-text-short",
36
+ avatar: "ph-skeleton-avatar",
37
+ button: "ph-skeleton-button",
38
+ block: "ph-skeleton-block",
39
+ };
40
+
41
+ export function Loader({
42
+ variant = "spinner",
43
+ size = "md",
44
+ label = "Loading",
45
+ className,
46
+ ...props
47
+ }: LoaderProps) {
48
+ return (
49
+ <span
50
+ role="status"
51
+ aria-label={label}
52
+ className={cn("ph-loader", loaderSizeClass[size], className)}
53
+ {...props}
54
+ >
55
+ {variant === "spinner" ? <span className="ph-loading" aria-hidden /> : null}
56
+ {variant === "dots" ? (
57
+ <span className="ph-loading-dots" aria-hidden>
58
+ <span />
59
+ <span />
60
+ <span />
61
+ </span>
62
+ ) : null}
63
+ {variant === "pulse" ? <span className="ph-loading-pulse" aria-hidden /> : null}
64
+ </span>
65
+ );
66
+ }
67
+
68
+ export function Skeleton({ shape = "text", className, ...props }: SkeletonProps) {
69
+ return (
70
+ <span
71
+ className={cn("ph-skeleton", skeletonShapeClass[shape], className)}
72
+ aria-hidden="true"
73
+ {...props}
74
+ />
75
+ );
76
+ }
77
+
78
+ export function LoadingState({
79
+ label = "Loading",
80
+ title = "Loading",
81
+ description,
82
+ variant = "spinner",
83
+ size = "lg",
84
+ className,
85
+ ...props
86
+ }: LoadingStateProps) {
87
+ return (
88
+ <div className={cn("ph-loading-state", className)} role="status" aria-label={label} {...props}>
89
+ <Loader variant={variant} size={size} label={label} aria-hidden="true" />
90
+ <div className="ph-loading-state__copy">
91
+ <strong>{title}</strong>
92
+ {description ? <span>{description}</span> : null}
93
+ </div>
94
+ </div>
95
+ );
96
+ }
@@ -0,0 +1,105 @@
1
+ "use client";
2
+
3
+ import * as Dialog from "@radix-ui/react-dialog";
4
+ import { useRef, type ReactNode } from "react";
5
+ import { IconClose } from "@/components/icons";
6
+ import { cn } from "@/lib/cn";
7
+
8
+ export type ModalSize = "sm" | "md" | "lg" | "xl" | "full";
9
+
10
+ export interface ModalProps {
11
+ open?: boolean;
12
+ onClose?: () => void;
13
+ title?: string;
14
+ description?: string;
15
+ children?: ReactNode;
16
+ footer?: ReactNode;
17
+ size?: ModalSize;
18
+ className?: string;
19
+ showCloseButton?: boolean;
20
+ closeOnOutsideClick?: boolean;
21
+ closeOnEscape?: boolean;
22
+ "aria-label"?: string;
23
+ }
24
+
25
+ const sizeMap: Record<ModalSize, string> = {
26
+ sm: "max-w-sm",
27
+ md: "max-w-lg",
28
+ lg: "max-w-2xl",
29
+ xl: "max-w-4xl",
30
+ full: "max-w-none",
31
+ };
32
+
33
+ export function Modal({
34
+ open = false,
35
+ onClose,
36
+ title,
37
+ description,
38
+ children,
39
+ footer,
40
+ size = "md",
41
+ className,
42
+ showCloseButton = true,
43
+ closeOnOutsideClick = true,
44
+ closeOnEscape = true,
45
+ "aria-label": ariaLabel,
46
+ }: ModalProps) {
47
+ const canDismiss = Boolean(onClose);
48
+ const restoreFocusRef = useRef<HTMLElement | null>(null);
49
+
50
+ return (
51
+ <Dialog.Root
52
+ open={open}
53
+ onOpenChange={(nextOpen) => {
54
+ if (!nextOpen) onClose?.();
55
+ }}
56
+ >
57
+ <Dialog.Portal>
58
+ <Dialog.Overlay className="ph-modal-overlay" />
59
+ <Dialog.Content
60
+ className={cn("ph-modal-panel", sizeMap[size], className)}
61
+ aria-label={ariaLabel}
62
+ {...(description ? {} : { "aria-describedby": undefined })}
63
+ onOpenAutoFocus={() => {
64
+ restoreFocusRef.current = document.activeElement instanceof HTMLElement
65
+ ? document.activeElement
66
+ : null;
67
+ }}
68
+ onCloseAutoFocus={(event) => {
69
+ event.preventDefault();
70
+ restoreFocusRef.current?.focus();
71
+ }}
72
+ onEscapeKeyDown={(event) => {
73
+ if (!canDismiss || !closeOnEscape) event.preventDefault();
74
+ }}
75
+ onPointerDownOutside={(event) => {
76
+ if (!canDismiss || !closeOnOutsideClick) event.preventDefault();
77
+ }}
78
+ >
79
+ {title ? null : <Dialog.Title className="sr-only">{ariaLabel ?? "Dialog"}</Dialog.Title>}
80
+ {(title || description || (showCloseButton && canDismiss)) ? (
81
+ <header className="ph-modal-header">
82
+ <div className="min-w-0">
83
+ {title ? <Dialog.Title className="text-lg font-bold text-ph-ink">{title}</Dialog.Title> : null}
84
+ {description ? (
85
+ <Dialog.Description className="mt-1 text-sm text-ph-subtle">
86
+ {description}
87
+ </Dialog.Description>
88
+ ) : null}
89
+ </div>
90
+ {showCloseButton && canDismiss ? (
91
+ <Dialog.Close asChild>
92
+ <button type="button" className="ph-modal-close" aria-label="Close dialog">
93
+ <IconClose className="h-5 w-5" aria-hidden />
94
+ </button>
95
+ </Dialog.Close>
96
+ ) : null}
97
+ </header>
98
+ ) : null}
99
+ <div className="ph-modal-body">{children}</div>
100
+ {footer ? <div className="ph-modal-footer">{footer}</div> : null}
101
+ </Dialog.Content>
102
+ </Dialog.Portal>
103
+ </Dialog.Root>
104
+ );
105
+ }
@@ -0,0 +1,94 @@
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
+ }
@@ -0,0 +1,18 @@
1
+ import type { ComponentPropsWithoutRef, ReactNode } from "react";
2
+ import { cn } from "@/lib/cn";
3
+
4
+ export interface PanelProps extends Omit<ComponentPropsWithoutRef<"div">, "title"> {
5
+ title?: ReactNode;
6
+ description?: ReactNode;
7
+ children?: ReactNode;
8
+ }
9
+
10
+ export function Panel({ title, description, children, className, ...props }: PanelProps) {
11
+ return (
12
+ <div className={cn("ph-panel", className)} {...props}>
13
+ {title && <h3 className="text-sm font-semibold uppercase tracking-wider text-ph-mutedtext">{title}</h3>}
14
+ {description && <p className="text-sm text-ph-subtle">{description}</p>}
15
+ {children}
16
+ </div>
17
+ );
18
+ }
@@ -0,0 +1,59 @@
1
+ import type { ComponentPropsWithoutRef } from "react";
2
+ import { cn } from "@/lib/cn";
3
+
4
+ export interface ProgressProps extends ComponentPropsWithoutRef<"div"> {
5
+ /** Current value (0-100) */
6
+ value: number;
7
+ /** Maximum value (default 100) */
8
+ max?: number;
9
+ /** Color class for the bar */
10
+ color?: string;
11
+ /** Optional label shown above */
12
+ label?: string;
13
+ /** Show percentage text */
14
+ showPercentage?: boolean;
15
+ /** Size variant */
16
+ size?: "sm" | "md" | "lg";
17
+ }
18
+
19
+ const sizeMap = {
20
+ sm: "h-1",
21
+ md: "h-2",
22
+ lg: "h-3",
23
+ };
24
+
25
+ export function Progress({
26
+ value,
27
+ max = 100,
28
+ color = "bg-ph-brand",
29
+ label,
30
+ showPercentage = false,
31
+ size = "md",
32
+ className,
33
+ ...props
34
+ }: ProgressProps) {
35
+ const percentage = Math.min(100, Math.max(0, (value / max) * 100));
36
+
37
+ return (
38
+ <div className={cn("w-full", className)} {...props}>
39
+ {(label || showPercentage) && (
40
+ <div className="mb-1.5 flex justify-between text-sm text-ph-subtle">
41
+ {label && <span>{label}</span>}
42
+ {showPercentage && (
43
+ <span className="tabular-nums">{Math.round(percentage)}%</span>
44
+ )}
45
+ </div>
46
+ )}
47
+ <div className={cn("ph-progress", sizeMap[size])}>
48
+ <div
49
+ className={cn("ph-progress-bar", color)}
50
+ style={{ width: `${percentage}%` }}
51
+ role="progressbar"
52
+ aria-valuenow={value}
53
+ aria-valuemin={0}
54
+ aria-valuemax={max}
55
+ />
56
+ </div>
57
+ </div>
58
+ );
59
+ }
@@ -0,0 +1,65 @@
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
+ }
@@ -0,0 +1,106 @@
1
+ import type { ComponentPropsWithoutRef } from "react";
2
+ import { useId } from "react";
3
+ import { IconSearch } from "@/components/icons";
4
+ import { cn } from "@/lib/cn";
5
+ import { Button } from "@/components/ui/Button";
6
+
7
+ export interface SearchInputProps extends Omit<ComponentPropsWithoutRef<"input">, "type"> {
8
+ /** Associated label text — always rendered (visually hidden when `hideLabel`) */
9
+ label?: string;
10
+ hideLabel?: boolean;
11
+ /** Optional shortcut hint shown inside the field (e.g. "⌘K") */
12
+ shortcut?: string;
13
+ density?: "default" | "compact";
14
+ wrapperClassName?: string;
15
+ }
16
+
17
+ /** Labelled search field — use in nav bars and toolbars. */
18
+ export function SearchInput({
19
+ id: idProp,
20
+ label = "Search",
21
+ hideLabel = false,
22
+ shortcut,
23
+ density = "default",
24
+ className,
25
+ wrapperClassName,
26
+ placeholder,
27
+ ...props
28
+ }: SearchInputProps) {
29
+ const autoId = useId();
30
+ const id = idProp ?? autoId;
31
+
32
+ return (
33
+ <div className={cn("relative", wrapperClassName)}>
34
+ <label htmlFor={id} className={hideLabel ? "sr-only" : "ph-label mb-1.5 block"}>
35
+ {label}
36
+ </label>
37
+ <div className="relative">
38
+ {shortcut != null ? (
39
+ <span
40
+ className="ph-shortcut pointer-events-none absolute left-3 top-1/2 z-10 -translate-y-1/2 text-xs"
41
+ aria-hidden
42
+ >
43
+ {shortcut}
44
+ </span>
45
+ ) : null}
46
+ <input
47
+ id={id}
48
+ type="search"
49
+ placeholder={placeholder}
50
+ aria-label={hideLabel ? label : undefined}
51
+ className={cn(
52
+ "ph-input w-full",
53
+ density === "compact" && "max-w-xs !min-h-[2rem] py-1.5 text-sm",
54
+ shortcut != null && "pl-[3.25rem]",
55
+ className,
56
+ )}
57
+ {...props}
58
+ />
59
+ </div>
60
+ </div>
61
+ );
62
+ }
63
+
64
+ export interface SearchGroupProps extends Omit<ComponentPropsWithoutRef<"input">, "type"> {
65
+ label?: string;
66
+ submitLabel?: string;
67
+ wrapperClassName?: string;
68
+ }
69
+
70
+ /** Input + primary search — one control shell, 1px vertical seam, matched height to `ph-input`. */
71
+ export function SearchGroup({
72
+ id: idProp,
73
+ label = "Search records",
74
+ submitLabel = "Search",
75
+ placeholder = "Persons, actions, flags…",
76
+ wrapperClassName,
77
+ className,
78
+ ...props
79
+ }: SearchGroupProps) {
80
+ const autoId = useId();
81
+ const id = idProp ?? autoId;
82
+
83
+ return (
84
+ <div className={cn("ph-field", wrapperClassName)}>
85
+ <label htmlFor={id} className="ph-label">
86
+ {label}
87
+ </label>
88
+ <div className="ph-search-group flex w-full max-w-lg min-h-[2.3125rem] items-stretch divide-x divide-ph-border">
89
+ <input
90
+ id={id}
91
+ type="search"
92
+ placeholder={placeholder}
93
+ className={cn("ph-input ph-input-group-field min-w-0 flex-1", className)}
94
+ {...props}
95
+ />
96
+ <Button
97
+ type="submit"
98
+ variant="primary"
99
+ icon={<IconSearch className="h-4 w-4" aria-hidden />}
100
+ className="ph-search-group__submit"
101
+ aria-label={submitLabel}
102
+ />
103
+ </div>
104
+ </div>
105
+ );
106
+ }
@@ -0,0 +1,44 @@
1
+ "use client";
2
+
3
+ import { cn } from "@/lib/cn";
4
+
5
+ interface SegmentedControlOption {
6
+ value: string;
7
+ label: string;
8
+ icon?: React.ReactNode;
9
+ }
10
+
11
+ interface SegmentedControlProps {
12
+ options: SegmentedControlOption[];
13
+ value: string;
14
+ onChange: (value: string) => void;
15
+ className?: string;
16
+ }
17
+
18
+ export function SegmentedControl({ options, value, onChange, className }: SegmentedControlProps) {
19
+ return (
20
+ <div
21
+ className={cn(
22
+ "inline-flex rounded-lg bg-ph-muted p-1",
23
+ className
24
+ )}
25
+ >
26
+ {options.map((option) => (
27
+ <button
28
+ key={option.value}
29
+ type="button"
30
+ onClick={() => onChange(option.value)}
31
+ className={cn(
32
+ "relative flex items-center gap-1.5 rounded-md px-3.5 py-1.5 text-sm font-medium transition-all",
33
+ value === option.value
34
+ ? "bg-ph-surface text-ph-ink shadow-ph"
35
+ : "text-ph-subtle hover:text-ph-ink"
36
+ )}
37
+ >
38
+ {option.icon && <span className="h-4 w-4">{option.icon}</span>}
39
+ {option.label}
40
+ </button>
41
+ ))}
42
+ </div>
43
+ );
44
+ }
@@ -0,0 +1,94 @@
1
+ import type { ComponentPropsWithoutRef, ReactNode } from "react";
2
+ import { IconTuning } from "@/components/icons";
3
+ import { cn } from "@/lib/cn";
4
+
5
+ export interface SettingsNavItem {
6
+ id: string;
7
+ label: ReactNode;
8
+ href: string;
9
+ icon?: ReactNode;
10
+ active?: boolean;
11
+ badge?: ReactNode;
12
+ }
13
+
14
+ export interface SettingsNavGroup {
15
+ label: ReactNode;
16
+ items: readonly SettingsNavItem[];
17
+ }
18
+
19
+ export interface SettingsNavProps extends ComponentPropsWithoutRef<"div"> {
20
+ groups: readonly SettingsNavGroup[];
21
+ }
22
+
23
+ export interface SettingsLayoutProps extends Omit<ComponentPropsWithoutRef<"section">, "title"> {
24
+ title: ReactNode;
25
+ description?: ReactNode;
26
+ actions?: ReactNode;
27
+ navigation: ReactNode;
28
+ navigationLabel?: string;
29
+ children: ReactNode;
30
+ }
31
+
32
+ export function SettingsNav({ groups, className, ...props }: SettingsNavProps) {
33
+ return (
34
+ <div className={cn("ph-settings-nav", className)} {...props}>
35
+ {groups.map((group, index) => (
36
+ <div className="ph-settings-nav__group" key={index}>
37
+ <h3>{group.label}</h3>
38
+ <div className="ph-settings-nav__items">
39
+ {group.items.map((item) => (
40
+ <a
41
+ key={item.id}
42
+ href={item.href}
43
+ className={cn("ph-settings-nav__item", item.active && "ph-settings-nav__item--active")}
44
+ aria-current={item.active ? "page" : undefined}
45
+ >
46
+ {item.icon ? <span className="ph-settings-nav__icon">{item.icon}</span> : null}
47
+ <span>{item.label}</span>
48
+ {item.badge ? <span className="ph-settings-nav__badge">{item.badge}</span> : null}
49
+ </a>
50
+ ))}
51
+ </div>
52
+ </div>
53
+ ))}
54
+ </div>
55
+ );
56
+ }
57
+
58
+ export function SettingsLayout({
59
+ title,
60
+ description,
61
+ actions,
62
+ navigation,
63
+ navigationLabel = "Settings",
64
+ children,
65
+ className,
66
+ ...props
67
+ }: SettingsLayoutProps) {
68
+ return (
69
+ <section className={cn("ph-settings-layout", className)} {...props}>
70
+ <header className="ph-settings-layout__header">
71
+ <div>
72
+ <h2>{title}</h2>
73
+ {description ? <p>{description}</p> : null}
74
+ </div>
75
+ {actions ? <div className="ph-settings-layout__actions">{actions}</div> : null}
76
+ </header>
77
+
78
+ <details className="ph-settings-layout__mobile-nav">
79
+ <summary>
80
+ <IconTuning className="h-4 w-4" aria-hidden />
81
+ Settings menu
82
+ </summary>
83
+ <nav aria-label={navigationLabel}>{navigation}</nav>
84
+ </details>
85
+
86
+ <div className="ph-settings-layout__grid">
87
+ <aside className="ph-settings-layout__sidebar">
88
+ <nav aria-label={navigationLabel}>{navigation}</nav>
89
+ </aside>
90
+ <div className="ph-settings-layout__content">{children}</div>
91
+ </div>
92
+ </section>
93
+ );
94
+ }