@xenide-io/the-old-ui-theme 0.2.9 → 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 (61) hide show
  1. package/README.md +1 -1
  2. package/dist/{chunk-SHF57J7U.mjs → chunk-FDMD3IIN.mjs} +1573 -652
  3. package/dist/index-D1RTT2Ap.d.mts +1044 -0
  4. package/dist/index-D1RTT2Ap.d.ts +1044 -0
  5. package/dist/index.d.mts +10 -7
  6. package/dist/index.d.ts +10 -7
  7. package/dist/index.js +1705 -744
  8. package/dist/index.mjs +61 -1
  9. package/dist/tailwind-preset.js +7 -0
  10. package/dist/tailwind-preset.mjs +7 -0
  11. package/dist/ui.d.mts +3 -2
  12. package/dist/ui.d.ts +3 -2
  13. package/dist/ui.js +1584 -666
  14. package/dist/ui.mjs +31 -1
  15. package/package.json +18 -4
  16. package/src/app/globals.css +1005 -227
  17. package/src/components/icons/IconBase.tsx +20 -12
  18. package/src/components/icons/createIconBase.tsx +12 -7
  19. package/src/components/icons/icons.tsx +208 -18
  20. package/src/components/sections/AlertShowcase.tsx +2 -1
  21. package/src/components/sections/BadgeShowcase.tsx +1 -1
  22. package/src/components/sections/ButtonShowcase.tsx +6 -10
  23. package/src/components/sections/CardShowcase.tsx +22 -6
  24. package/src/components/sections/DropdownShowcase.tsx +4 -6
  25. package/src/components/sections/FilterChipsShowcase.tsx +74 -26
  26. package/src/components/sections/FormShowcase.tsx +2 -1
  27. package/src/components/sections/IconShowcase.tsx +86 -206
  28. package/src/components/sections/KbdShowcase.tsx +47 -21
  29. package/src/components/sections/ModalShowcase.tsx +7 -6
  30. package/src/components/sections/ProductLayoutsShowcase.tsx +138 -0
  31. package/src/components/sections/ProgressShowcase.tsx +50 -53
  32. package/src/components/sections/SwapShowcase.tsx +13 -5
  33. package/src/components/ui/Alert.tsx +28 -19
  34. package/src/components/ui/AuthLayout.tsx +58 -0
  35. package/src/components/ui/Badge.tsx +10 -5
  36. package/src/components/ui/Button.tsx +22 -12
  37. package/src/components/ui/ButtonChrome.tsx +1 -1
  38. package/src/components/ui/Calendar.tsx +6 -7
  39. package/src/components/ui/Card.tsx +21 -11
  40. package/src/components/ui/CollapsibleSection.tsx +11 -11
  41. package/src/components/ui/DropdownMenu.tsx +189 -65
  42. package/src/components/ui/FilterBar.tsx +165 -0
  43. package/src/components/ui/FilterChips.tsx +65 -35
  44. package/src/components/ui/FilterControls.tsx +30 -0
  45. package/src/components/ui/FormField.tsx +69 -0
  46. package/src/components/ui/Input.tsx +166 -140
  47. package/src/components/ui/Kbd.tsx +88 -13
  48. package/src/components/ui/Loader.tsx +96 -0
  49. package/src/components/ui/Modal.tsx +66 -36
  50. package/src/components/ui/SettingsLayout.tsx +94 -0
  51. package/src/components/ui/ShowcaseWrapper.tsx +4 -16
  52. package/src/components/ui/Stepper.tsx +5 -6
  53. package/src/components/ui/first-slice.test.tsx +98 -0
  54. package/src/components/ui/index.ts +62 -3
  55. package/src/components/ui/interaction-primitives.test.tsx +77 -0
  56. package/src/components/ui/kbd-icons.test.tsx +90 -0
  57. package/src/components/ui/product-patterns.test.tsx +77 -0
  58. package/src/styles/themes.css +411 -1
  59. package/tailwind-preset.ts +7 -0
  60. package/dist/index-CmS6hcUk.d.mts +0 -753
  61. package/dist/index-CmS6hcUk.d.ts +0 -753
@@ -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
+ }
@@ -1,4 +1,7 @@
1
- import type { ReactNode } from "react";
1
+ "use client";
2
+
3
+ import * as Dialog from "@radix-ui/react-dialog";
4
+ import { useRef, type ReactNode } from "react";
2
5
  import { IconClose } from "@/components/icons";
3
6
  import { cn } from "@/lib/cn";
4
7
 
@@ -14,6 +17,9 @@ export interface ModalProps {
14
17
  size?: ModalSize;
15
18
  className?: string;
16
19
  showCloseButton?: boolean;
20
+ closeOnOutsideClick?: boolean;
21
+ closeOnEscape?: boolean;
22
+ "aria-label"?: string;
17
23
  }
18
24
 
19
25
  const sizeMap: Record<ModalSize, string> = {
@@ -21,7 +27,7 @@ const sizeMap: Record<ModalSize, string> = {
21
27
  md: "max-w-lg",
22
28
  lg: "max-w-2xl",
23
29
  xl: "max-w-4xl",
24
- full: "max-w-[calc(100%-2rem)]",
30
+ full: "max-w-none",
25
31
  };
26
32
 
27
33
  export function Modal({
@@ -34,42 +40,66 @@ export function Modal({
34
40
  size = "md",
35
41
  className,
36
42
  showCloseButton = true,
43
+ closeOnOutsideClick = true,
44
+ closeOnEscape = true,
45
+ "aria-label": ariaLabel,
37
46
  }: ModalProps) {
38
- if (!open) return null;
47
+ const canDismiss = Boolean(onClose);
48
+ const restoreFocusRef = useRef<HTMLElement | null>(null);
39
49
 
40
50
  return (
41
- <div className="ph-modal-overlay" role="presentation" onClick={onClose}>
42
- <div
43
- role="dialog"
44
- aria-modal
45
- className={cn(
46
- "ph-modal-panel",
47
- sizeMap[size],
48
- className
49
- )}
50
- onClick={(e) => e.stopPropagation()}
51
- >
52
- {(title || showCloseButton) && (
53
- <header className="flex items-start justify-between gap-4">
54
- <div>
55
- {title && <h3 className="text-lg font-bold text-ph-ink">{title}</h3>}
56
- {description && <p className="mt-1 text-sm text-ph-subtle">{description}</p>}
57
- </div>
58
- {showCloseButton && onClose && (
59
- <button
60
- type="button"
61
- onClick={onClose}
62
- className="shrink-0 rounded-md p-1 text-ph-subtle hover:bg-black/5 hover:text-ph-ink"
63
- aria-label="Close"
64
- >
65
- <IconClose className="h-5 w-5" />
66
- </button>
67
- )}
68
- </header>
69
- )}
70
- <div className="mt-4">{children}</div>
71
- {footer && <div className="mt-6 flex justify-end gap-2 border-t border-ph-border pt-5">{footer}</div>}
72
- </div>
73
- </div>
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>
74
104
  );
75
105
  }
@@ -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
+ }
@@ -3,6 +3,7 @@
3
3
  import { useState, type ReactNode } from "react";
4
4
  import { cn } from "@/lib/cn";
5
5
  import { CodeBlock } from "@/components/ui";
6
+ import { IconCodePreview } from "@/components/icons";
6
7
 
7
8
  interface ShowcaseWrapperProps {
8
9
  id?: string;
@@ -39,26 +40,13 @@ export function ShowcaseWrapper({
39
40
  className={cn(
40
41
  "flex h-10 w-10 items-center justify-center rounded-full border transition-all",
41
42
  showCode
42
- ? "border-ph-brand bg-ph-brand text-white shadow-lg shadow-ph-brand/25"
43
+ ? "border-ph-brand bg-ph-brand text-[var(--ph-on-accent)] shadow-lg shadow-ph-brand/25"
43
44
  : "border-ph-border bg-ph-surface text-ph-subtle hover:border-ph-brand hover:text-ph-brand"
44
45
  )}
45
46
  title={showCode ? "Hide code" : "Show code"}
47
+ aria-label={showCode ? "Hide code" : "Show code"}
46
48
  >
47
- <svg
48
- className="h-5 w-5"
49
- viewBox="0 0 24 24"
50
- fill="none"
51
- stroke="currentColor"
52
- strokeWidth="2"
53
- strokeLinecap="round"
54
- strokeLinejoin="round"
55
- >
56
- <rect x="2" y="3" width="20" height="14" rx="2" ry="2" />
57
- <line x1="8" y1="21" x2="16" y2="21" />
58
- <line x1="12" y1="17" x2="12" y2="21" />
59
- <line x1="6" y1="8" x2="6" y2="8" />
60
- <line x1="10" y1="8" x2="10" y2="8" />
61
- </svg>
49
+ <IconCodePreview className="h-5 w-5" aria-hidden />
62
50
  </button>
63
51
  </div>
64
52
  <hr />
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { cn } from "@/lib/cn";
4
+ import { IconCheck } from "@/components/icons";
4
5
 
5
6
  interface Step {
6
7
  id: string;
@@ -30,16 +31,14 @@ export function Stepper({ steps, currentStep, className }: StepperProps) {
30
31
  <div
31
32
  className={cn(
32
33
  "flex h-9 w-9 items-center justify-center rounded-full text-sm font-bold transition-colors",
33
- isCompleted && "bg-ph-success text-white",
34
- isCurrent && !isError && "bg-ph-brand text-white ring-4 ring-ph-brand/20",
35
- isCurrent && isError && "bg-ph-danger text-white",
34
+ isCompleted && "bg-ph-success text-[var(--ph-on-success)]",
35
+ isCurrent && !isError && "bg-ph-brand text-[var(--ph-on-accent)] ring-4 ring-ph-brand/20",
36
+ isCurrent && isError && "bg-ph-danger text-[var(--ph-on-danger)]",
36
37
  isPending && "bg-ph-muted text-ph-mutedtext"
37
38
  )}
38
39
  >
39
40
  {isCompleted ? (
40
- <svg className="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3">
41
- <path d="M5 13l4 4L19 7" />
42
- </svg>
41
+ <IconCheck className="h-5 w-5" aria-hidden />
43
42
  ) : (
44
43
  index + 1
45
44
  )}
@@ -0,0 +1,98 @@
1
+ import { createRef, useState } from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { describe, expect, it, vi } from "vitest";
5
+ import { Alert } from "@/components/ui/Alert";
6
+ import { Badge } from "@/components/ui/Badge";
7
+ import { Button } from "@/components/ui/Button";
8
+ import { Card } from "@/components/ui/Card";
9
+ import { Input } from "@/components/ui/Input";
10
+ import { Modal } from "@/components/ui/Modal";
11
+
12
+ describe("first-slice components", () => {
13
+ it("forwards a button ref and exposes loading state", () => {
14
+ const ref = createRef<HTMLButtonElement>();
15
+ render(<Button ref={ref} loading>Save changes</Button>);
16
+
17
+ expect(ref.current).toBe(screen.getByRole("button", { name: "Save changes" }));
18
+ expect(ref.current).toBeDisabled();
19
+ expect(ref.current).toHaveAttribute("aria-busy", "true");
20
+ });
21
+
22
+ it("uses the same tactile chrome for every filled variant", () => {
23
+ const { rerender } = render(<Button variant="signal">Launch</Button>);
24
+ expect(screen.getByRole("button", { name: "Launch" })).toHaveClass("ph-btn-raised");
25
+ expect(screen.getByRole("button", { name: "Launch" }).querySelector(".ph-btn-chrome")).toBeInTheDocument();
26
+
27
+ rerender(<Button variant="danger">Delete</Button>);
28
+ expect(screen.getByRole("button", { name: "Delete" }).querySelector(".ph-btn-chrome")).toBeInTheDocument();
29
+ });
30
+
31
+ it("associates input help and error messages", () => {
32
+ const { rerender } = render(
33
+ <Input label="Email" helperText="Use your work address" />,
34
+ );
35
+
36
+ expect(screen.getByLabelText("Email")).toHaveAccessibleDescription("Use your work address");
37
+
38
+ rerender(<Input label="Email" error="Enter a valid email address" />);
39
+ const input = screen.getByLabelText("Email");
40
+ expect(input).toHaveAttribute("aria-invalid", "true");
41
+ expect(input).toHaveAccessibleErrorMessage("Enter a valid email address");
42
+ });
43
+
44
+ it("keeps cards static unless explicitly interactive", () => {
45
+ const { rerender } = render(<Card title="Static card" />);
46
+ expect(screen.getByText("Static card").closest(".ph-card")).not.toHaveClass("ph-card-interactive");
47
+
48
+ rerender(<Card title="Interactive card" interactive />);
49
+ expect(screen.getByText("Interactive card").closest(".ph-card")).toHaveClass("ph-card-interactive");
50
+ });
51
+
52
+ it("forwards badge attributes and keeps static alerts out of live regions", () => {
53
+ render(
54
+ <>
55
+ <Badge data-testid="badge">Live</Badge>
56
+ <Alert title="Saved" />
57
+ <Alert title="Background sync complete" live="polite" />
58
+ </>,
59
+ );
60
+
61
+ expect(screen.getByTestId("badge")).toHaveTextContent("Live");
62
+ expect(screen.getByText("Saved").closest(".ph-alert")).not.toHaveAttribute("role");
63
+ expect(screen.getByRole("status")).toHaveTextContent("Background sync complete");
64
+ });
65
+
66
+ it("closes the modal with Escape and restores trigger focus", async () => {
67
+ const user = userEvent.setup();
68
+ const onClose = vi.fn();
69
+
70
+ function Fixture() {
71
+ const [open, setOpen] = useState(false);
72
+
73
+ return (
74
+ <>
75
+ <button type="button" onClick={() => setOpen(true)}>Open settings</button>
76
+ <Modal
77
+ open={open}
78
+ onClose={() => {
79
+ onClose();
80
+ setOpen(false);
81
+ }}
82
+ title="Workspace settings"
83
+ >
84
+ <button type="button">Save settings</button>
85
+ </Modal>
86
+ </>
87
+ );
88
+ }
89
+
90
+ render(<Fixture />);
91
+ const trigger = screen.getByRole("button", { name: "Open settings" });
92
+ await user.click(trigger);
93
+ expect(screen.getByRole("dialog", { name: "Workspace settings" })).toBeInTheDocument();
94
+ await user.keyboard("{Escape}");
95
+ expect(onClose).toHaveBeenCalledTimes(1);
96
+ expect(trigger).toHaveFocus();
97
+ });
98
+ });
@@ -4,14 +4,47 @@ export { Alert as Alert, type AlertProps as AlertProps, type AlertStatus as Aler
4
4
  export { Badge as Badge, type BadgeProps as BadgeProps, type BadgeSize as BadgeSize, type BadgeVariant as BadgeVariant } from "@/components/ui/Badge";
5
5
  export { Card as Card, type CardProps as CardProps, type CardVariant as CardVariant } from "@/components/ui/Card";
6
6
  export { Drawer as Drawer, type DrawerProps as DrawerProps, type DrawerSide as DrawerSide, type DrawerSize as DrawerSize } from "@/components/ui/Drawer";
7
- export { Kbd as Kbd, type KbdProps as KbdProps, type KbdVariant as KbdVariant } from "@/components/ui/Kbd";
7
+ export { Kbd as Kbd, type KbdPlatform as KbdPlatform, type KbdProps as KbdProps, type KbdVariant as KbdVariant } from "@/components/ui/Kbd";
8
+ export {
9
+ Loader,
10
+ LoadingState,
11
+ Skeleton,
12
+ type LoaderProps,
13
+ type LoaderSize,
14
+ type LoaderVariant,
15
+ type LoadingStateProps,
16
+ type SkeletonProps,
17
+ type SkeletonShape,
18
+ } from "@/components/ui/Loader";
19
+ export {
20
+ AuthCard,
21
+ AuthDivider,
22
+ AuthLayout,
23
+ type AuthCardProps,
24
+ type AuthDividerProps,
25
+ type AuthLayoutProps,
26
+ } from "@/components/ui/AuthLayout";
27
+ export {
28
+ SettingsLayout,
29
+ SettingsNav,
30
+ type SettingsLayoutProps,
31
+ type SettingsNavGroup,
32
+ type SettingsNavItem,
33
+ type SettingsNavProps,
34
+ } from "@/components/ui/SettingsLayout";
8
35
  export {
9
36
  DropdownButton as DropdownButton,
10
37
  DropdownItem as DropdownItem,
11
38
  DropdownMenu as DropdownMenu,
39
+ DropdownRadioGroup as DropdownRadioGroup,
40
+ DropdownRadioItem as DropdownRadioItem,
12
41
  type DropdownAlign as DropdownAlign,
13
42
  type DropdownButtonProps as DropdownButtonProps,
43
+ type DropdownItemProps as DropdownItemProps,
14
44
  type DropdownMenuProps as DropdownMenuProps,
45
+ type DropdownRadioGroupProps as DropdownRadioGroupProps,
46
+ type DropdownRadioItemProps as DropdownRadioItemProps,
47
+ type DropdownSide as DropdownSide,
15
48
  } from "@/components/ui/DropdownMenu";
16
49
  export {
17
50
  Checkbox as Checkbox,
@@ -33,6 +66,11 @@ export {
33
66
  type TextareaProps as TextareaProps,
34
67
  type ToggleProps as ToggleProps,
35
68
  } from "@/components/ui/Input";
69
+ export {
70
+ FormField as FormField,
71
+ type FormFieldControlProps as FormFieldControlProps,
72
+ type FormFieldProps as FormFieldProps,
73
+ } from "@/components/ui/FormField";
36
74
  export { Modal as Modal, type ModalProps as ModalProps, type ModalSize as ModalSize } from "@/components/ui/Modal";
37
75
  export { Breadcrumbs as Breadcrumbs, Pagination as Pagination, type BreadcrumbItem as BreadcrumbItem, type BreadcrumbsProps as BreadcrumbsProps, type PaginationProps as PaginationProps } from "@/components/ui/Navigation";
38
76
  export { Panel as Panel, type PanelProps as PanelProps } from "@/components/ui/Panel";
@@ -45,7 +83,14 @@ export { Tabs as Tabs, type TabItem as TabItem, type TabsProps as TabsProps, typ
45
83
  export { ThemeDomSync } from "@/components/ui/ThemeDomSync";
46
84
  export { ThemeManager, type ThemeManagerProps } from "@/components/ui/ThemeManager";
47
85
  export { ThemeSwitcher, type ThemeSwitcherProps } from "@/components/ui/ThemeSwitcher";
48
- export { IconBase as Icon, IconByName as IconByName, type IconProps as IconProps, type IconName as IconName } from "@/components/icons";
86
+ export {
87
+ IconBase as Icon,
88
+ IconByName as IconByName,
89
+ type CanonicalIconName as CanonicalIconName,
90
+ type IconAliasName as IconAliasName,
91
+ type IconProps as IconProps,
92
+ type IconName as IconName,
93
+ } from "@/components/icons";
49
94
  export { Accordion } from "@/components/ui/Accordion";
50
95
  export { Stepper } from "@/components/ui/Stepper";
51
96
  export { SegmentedControl } from "@/components/ui/SegmentedControl";
@@ -53,7 +98,21 @@ export { CommandPalette } from "@/components/ui/CommandPalette";
53
98
  export { Calendar } from "@/components/ui/Calendar";
54
99
  export { HoverCard } from "@/components/ui/HoverCard";
55
100
  export { EmptyState } from "@/components/ui/EmptyState";
56
- export { FilterChips } from "@/components/ui/FilterChips";
101
+ export {
102
+ FilterChips,
103
+ type FilterChip,
104
+ type FilterChipsProps,
105
+ } from "@/components/ui/FilterChips";
106
+ export {
107
+ FilterBar,
108
+ FilterMenu,
109
+ SortMenu,
110
+ type FilterBarProps,
111
+ type FilterMenuOption,
112
+ type FilterMenuProps,
113
+ type SortMenuProps,
114
+ } from "@/components/ui/FilterBar";
115
+ export { FilterControls, type FilterControlsProps } from "@/components/ui/FilterControls";
57
116
  export { CodeBlock } from "@/components/ui/CodeBlock";
58
117
  export { ComponentDocs, type ComponentDocsProps, type ComponentPropRow } from "@/components/ui/ComponentDocs";
59
118
  export { ShowcaseWrapper } from "@/components/ui/ShowcaseWrapper";
@@ -0,0 +1,77 @@
1
+ import { useState } from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { describe, expect, it, vi } from "vitest";
5
+ import { DropdownButton, DropdownItem } from "@/components/ui/DropdownMenu";
6
+ import { FilterMenu } from "@/components/ui/FilterBar";
7
+ import { FilterChips } from "@/components/ui/FilterChips";
8
+
9
+ describe("adaptive dropdown", () => {
10
+ it("supports keyboard navigation, selection, closing, and focus restoration", async () => {
11
+ const user = userEvent.setup();
12
+ const onSelect = vi.fn();
13
+
14
+ render(
15
+ <DropdownButton label="Actions">
16
+ <DropdownItem onClick={onSelect}>Export report</DropdownItem>
17
+ <DropdownItem>Duplicate</DropdownItem>
18
+ </DropdownButton>,
19
+ );
20
+
21
+ const trigger = screen.getByRole("button", { name: "Actions" });
22
+ await user.click(trigger);
23
+ expect(screen.getByRole("menu")).toBeInTheDocument();
24
+
25
+ await user.keyboard("{ArrowDown}{Enter}");
26
+ expect(onSelect).toHaveBeenCalledTimes(1);
27
+ expect(screen.queryByRole("menu")).not.toBeInTheDocument();
28
+ expect(trigger).toHaveFocus();
29
+ });
30
+
31
+ it("exposes filter choices as a controlled radio menu", async () => {
32
+ const user = userEvent.setup();
33
+
34
+ function Fixture() {
35
+ const [value, setValue] = useState("active");
36
+ return (
37
+ <FilterMenu
38
+ label="Status"
39
+ value={value}
40
+ onValueChange={setValue}
41
+ options={[
42
+ { value: "active", label: "Active" },
43
+ { value: "draft", label: "Draft" },
44
+ ]}
45
+ />
46
+ );
47
+ }
48
+
49
+ render(<Fixture />);
50
+ await user.click(screen.getByRole("button", { name: "Status: Active" }));
51
+ expect(screen.getByRole("menuitemradio", { name: "Active" })).toHaveAttribute("data-state", "checked");
52
+ await user.click(screen.getByRole("menuitemradio", { name: "Draft" }));
53
+ expect(screen.getByRole("button", { name: "Status: Draft" })).toBeInTheDocument();
54
+ });
55
+ });
56
+
57
+ describe("filter chips", () => {
58
+ it("keeps toggle and removal as independent keyboard controls", async () => {
59
+ const user = userEvent.setup();
60
+ const onToggle = vi.fn();
61
+ const onRemove = vi.fn();
62
+
63
+ render(
64
+ <FilterChips
65
+ chips={[{ id: "status", label: "Status", value: "Active", active: true }]}
66
+ onToggle={onToggle}
67
+ onRemove={onRemove}
68
+ />,
69
+ );
70
+
71
+ const toggle = screen.getByRole("button", { name: "Status Active" });
72
+ expect(toggle).toHaveAttribute("aria-pressed", "true");
73
+ await user.click(screen.getByRole("button", { name: "Remove Status: Active filter" }));
74
+ expect(onRemove).toHaveBeenCalledWith("status");
75
+ expect(onToggle).not.toHaveBeenCalled();
76
+ });
77
+ });