@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,129 @@
1
+ import type { ComponentPropsWithoutRef, ReactNode } from "react";
2
+ import { cn } from "@/lib/cn";
3
+
4
+ export type TextTone =
5
+ | "default"
6
+ | "muted"
7
+ | "subtle"
8
+ | "brand"
9
+ | "danger"
10
+ | "success"
11
+ | "warning"
12
+ | "info"
13
+ | "inherit";
14
+
15
+ export type TextWeight =
16
+ | "normal"
17
+ | "medium"
18
+ | "semibold"
19
+ | "bold"
20
+ | "extrabold";
21
+
22
+ export interface TextBaseProps {
23
+ tone?: TextTone;
24
+ weight?: TextWeight;
25
+ className?: string;
26
+ children?: ReactNode;
27
+ truncate?: boolean;
28
+ }
29
+
30
+ const toneMap: Record<TextTone, string> = {
31
+ default: "text-ph-ink",
32
+ muted: "text-ph-mutedtext",
33
+ subtle: "text-ph-subtle",
34
+ brand: "text-ph-brand",
35
+ danger: "text-ph-danger",
36
+ success: "text-ph-success",
37
+ warning: "text-ph-warning",
38
+ info: "text-ph-info",
39
+ inherit: "",
40
+ };
41
+
42
+ const weightMap: Record<TextWeight, string> = {
43
+ normal: "font-normal",
44
+ medium: "font-medium",
45
+ semibold: "font-semibold",
46
+ bold: "font-bold",
47
+ extrabold: "font-extrabold",
48
+ };
49
+
50
+ function textClass(
51
+ base: string,
52
+ tone: TextTone,
53
+ weight?: TextWeight,
54
+ truncate?: boolean,
55
+ className?: string
56
+ ) {
57
+ return cn(base, toneMap[tone], weight && weightMap[weight], truncate && "truncate", className);
58
+ }
59
+
60
+ /** Display text — largest heading, use once per page */
61
+ export function Display({
62
+ children,
63
+ tone = "default",
64
+ weight,
65
+ truncate,
66
+ className,
67
+ }: TextBaseProps) {
68
+ return <h1 className={textClass("text-4xl font-extrabold tracking-tight md:text-5xl", tone, weight, truncate, className)}>{children}</h1>;
69
+ }
70
+
71
+ /** H1 — page title */
72
+ export function H1({ children, tone = "default", weight, truncate, className }: TextBaseProps) {
73
+ return <h1 className={textClass("text-3xl font-bold tracking-tight md:text-4xl", tone, weight, truncate, className)}>{children}</h1>;
74
+ }
75
+
76
+ /** H2 — section title */
77
+ export function H2({ children, tone = "default", weight, truncate, className }: TextBaseProps) {
78
+ return <h2 className={textClass("text-2xl font-bold tracking-tight", tone, weight, truncate, className)}>{children}</h2>;
79
+ }
80
+
81
+ /** H3 — subsection title */
82
+ export function H3({ children, tone = "default", weight, truncate, className }: TextBaseProps) {
83
+ return <h3 className={textClass("text-xl font-semibold", tone, weight, truncate, className)}>{children}</h3>;
84
+ }
85
+
86
+ /** H4 — card / panel title */
87
+ export function H4({ children, tone = "default", weight, truncate, className }: TextBaseProps) {
88
+ return <h4 className={textClass("text-lg font-semibold", tone, weight, truncate, className)}>{children}</h4>;
89
+ }
90
+
91
+ /** H5 — small heading */
92
+ export function H5({ children, tone = "default", weight, truncate, className }: TextBaseProps) {
93
+ return <h5 className={textClass("text-base font-semibold", tone, weight, truncate, className)}>{children}</h5>;
94
+ }
95
+
96
+ /** Body — default paragraph text */
97
+ export function P({ children, tone = "default", weight, truncate, className }: TextBaseProps) {
98
+ return <p className={textClass("text-base leading-relaxed", tone, weight, truncate, className)}>{children}</p>;
99
+ }
100
+
101
+ /** Small body text */
102
+ export function Small({ children, tone = "default", weight, truncate, className }: TextBaseProps) {
103
+ return <span className={textClass("text-sm leading-relaxed", tone, weight, truncate, className)}>{children}</span>;
104
+ }
105
+
106
+ /** Extra small text — captions, meta */
107
+ export function Caption({ children, tone = "muted", weight, truncate, className }: TextBaseProps) {
108
+ return <span className={textClass("text-[11px] font-medium uppercase tracking-wider", tone, weight, truncate, className)}>{children}</span>;
109
+ }
110
+
111
+ /** Overline — tiny eyebrow text */
112
+ export function Overline({ children, tone = "muted", weight, truncate, className }: TextBaseProps) {
113
+ return <span className={textClass("text-[10px] font-bold uppercase tracking-[0.15em]", tone, weight, truncate, className)}>{children}</span>;
114
+ }
115
+
116
+ /** Lead — intro paragraph, larger and muted */
117
+ export function Lead({ children, tone = "subtle", weight, truncate, className }: TextBaseProps) {
118
+ return <p className={textClass("text-lg leading-relaxed", tone, weight, truncate, className)}>{children}</p>;
119
+ }
120
+
121
+ /** Mono — code, data, technical text */
122
+ export function Mono({ children, tone = "default", weight, truncate, className }: TextBaseProps) {
123
+ return <code className={textClass("text-sm font-mono leading-relaxed", tone, weight, truncate, className)}>{children}</code>;
124
+ }
125
+
126
+ /** Label — form labels, tags */
127
+ export function Label({ children, tone = "default", weight, truncate, className }: TextBaseProps) {
128
+ return <label className={textClass("text-sm font-medium", tone, weight, truncate, className)}>{children}</label>;
129
+ }
@@ -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
+ });
@@ -0,0 +1,151 @@
1
+ export { Button as Button, type ButtonProps as ButtonProps, type ButtonShape as ButtonShape, type ButtonSize as ButtonSize, type ButtonVariant as ButtonVariant } from "@/components/ui/Button";
2
+ export { ButtonChrome as ButtonChrome, type ButtonChromeProps as ButtonChromeProps } from "@/components/ui/ButtonChrome";
3
+ export { Alert as Alert, type AlertProps as AlertProps, type AlertStatus as AlertStatus } from "@/components/ui/Alert";
4
+ export { Badge as Badge, type BadgeProps as BadgeProps, type BadgeSize as BadgeSize, type BadgeVariant as BadgeVariant } from "@/components/ui/Badge";
5
+ export { Card as Card, type CardProps as CardProps, type CardVariant as CardVariant } from "@/components/ui/Card";
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 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";
35
+ export {
36
+ DropdownButton as DropdownButton,
37
+ DropdownItem as DropdownItem,
38
+ DropdownMenu as DropdownMenu,
39
+ DropdownRadioGroup as DropdownRadioGroup,
40
+ DropdownRadioItem as DropdownRadioItem,
41
+ type DropdownAlign as DropdownAlign,
42
+ type DropdownButtonProps as DropdownButtonProps,
43
+ type DropdownItemProps as DropdownItemProps,
44
+ type DropdownMenuProps as DropdownMenuProps,
45
+ type DropdownRadioGroupProps as DropdownRadioGroupProps,
46
+ type DropdownRadioItemProps as DropdownRadioItemProps,
47
+ type DropdownSide as DropdownSide,
48
+ } from "@/components/ui/DropdownMenu";
49
+ export {
50
+ Checkbox as Checkbox,
51
+ FileUpload as FileUpload,
52
+ Input as Input,
53
+ Radio as Radio,
54
+ Range as Range,
55
+ Select as Select,
56
+ Textarea as Textarea,
57
+ Toggle as Toggle,
58
+ type CheckboxProps as CheckboxProps,
59
+ type FileUploadProps as FileUploadProps,
60
+ type InputProps as InputProps,
61
+ type InputSize as InputSize,
62
+ type InputVariant as InputVariant,
63
+ type RadioProps as RadioProps,
64
+ type RangeProps as RangeProps,
65
+ type SelectProps as SelectProps,
66
+ type TextareaProps as TextareaProps,
67
+ type ToggleProps as ToggleProps,
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";
74
+ export { Modal as Modal, type ModalProps as ModalProps, type ModalSize as ModalSize } from "@/components/ui/Modal";
75
+ export { Breadcrumbs as Breadcrumbs, Pagination as Pagination, type BreadcrumbItem as BreadcrumbItem, type BreadcrumbsProps as BreadcrumbsProps, type PaginationProps as PaginationProps } from "@/components/ui/Navigation";
76
+ export { Panel as Panel, type PanelProps as PanelProps } from "@/components/ui/Panel";
77
+ export { Rating as Rating, type RatingProps as RatingProps } from "@/components/ui/Rating";
78
+ export { SearchGroup as SearchGroup, SearchInput as SearchInput, type SearchGroupProps as SearchGroupProps, type SearchInputProps as SearchInputProps } from "@/components/ui/SearchInput";
79
+ export { Table as Table, type TableColumn as TableColumn, type TableProps as TableProps } from "@/components/ui/Table";
80
+ export { Stat as Stat, type StatProps as StatProps, type StatTone as StatTone } from "@/components/ui/Stat";
81
+ export { Toast as Toast, ToastStack as ToastStack, type ToastProps as ToastProps, type ToastStackProps as ToastStackProps, type ToastStatus as ToastStatus } from "@/components/ui/Toast";
82
+ export { Tabs as Tabs, type TabItem as TabItem, type TabsProps as TabsProps, type TabsVariant as TabsVariant } from "@/components/ui/Tabs";
83
+ export { ThemeDomSync } from "@/components/ui/ThemeDomSync";
84
+ export { ThemeManager, type ThemeManagerProps } from "@/components/ui/ThemeManager";
85
+ export { ThemeSwitcher, type ThemeSwitcherProps } from "@/components/ui/ThemeSwitcher";
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";
94
+ export { Accordion } from "@/components/ui/Accordion";
95
+ export { Stepper } from "@/components/ui/Stepper";
96
+ export { SegmentedControl } from "@/components/ui/SegmentedControl";
97
+ export { CommandPalette } from "@/components/ui/CommandPalette";
98
+ export { Calendar } from "@/components/ui/Calendar";
99
+ export { HoverCard } from "@/components/ui/HoverCard";
100
+ export { EmptyState } from "@/components/ui/EmptyState";
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";
116
+ export { CodeBlock } from "@/components/ui/CodeBlock";
117
+ export { ComponentDocs, type ComponentDocsProps, type ComponentPropRow } from "@/components/ui/ComponentDocs";
118
+ export { ShowcaseWrapper } from "@/components/ui/ShowcaseWrapper";
119
+ export { CollapsibleSection } from "@/components/ui/CollapsibleSection";
120
+ export { Display, H1, H2, H3, H4, H5, P, Small, Caption, Overline, Lead, Mono, Label } from "@/components/ui/Typography";
121
+ export {
122
+ Avatar as Avatar,
123
+ AvatarGroup as AvatarGroup,
124
+ type AvatarProps as AvatarProps,
125
+ type AvatarSize as AvatarSize,
126
+ type AvatarStatus as AvatarStatus,
127
+ type AvatarGroupProps as AvatarGroupProps,
128
+ } from "@/components/ui/Avatar";
129
+ export {
130
+ Progress as Progress,
131
+ type ProgressProps as ProgressProps,
132
+ } from "@/components/ui/Progress";
133
+ export {
134
+ ChatBubble as ChatBubble,
135
+ type ChatBubbleProps as ChatBubbleProps,
136
+ type ChatBubbleVariant as ChatBubbleVariant,
137
+ } from "@/components/ui/ChatBubble";
138
+ export {
139
+ Terminal as Terminal,
140
+ type TerminalProps as TerminalProps,
141
+ } from "@/components/ui/Terminal";
142
+ export {
143
+ Indicator as Indicator,
144
+ type IndicatorProps as IndicatorProps,
145
+ type IndicatorPosition as IndicatorPosition,
146
+ } from "@/components/ui/Indicator";
147
+ export {
148
+ Timeline as Timeline,
149
+ type TimelineProps as TimelineProps,
150
+ type TimelineEvent as TimelineEvent,
151
+ } from "@/components/ui/Timeline";
@@ -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
+ });
@@ -0,0 +1,90 @@
1
+ import { createRef } from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { describe, expect, it } from "vitest";
4
+ import {
5
+ CANONICAL_ICONS,
6
+ OLD_UI_ICONS,
7
+ PH_ICONS,
8
+ PH_ICON_ALIASES,
9
+ IconByName,
10
+ IconInfo,
11
+ IconOldWindow,
12
+ IconTerminal,
13
+ } from "@/components/icons";
14
+ import { Kbd } from "@/components/ui/Kbd";
15
+
16
+ describe("Kbd", () => {
17
+ it("renders shortcuts as separate, spoken keycaps", () => {
18
+ render(<Kbd keys={["Command", "K"]} platform="mac" />);
19
+ const shortcut = screen.getByLabelText("Command + K");
20
+
21
+ expect(shortcut).toHaveClass("ph-keycap");
22
+ expect(shortcut.querySelectorAll(".ph-shortcut-key")).toHaveLength(2);
23
+ expect(shortcut).toHaveTextContent("⌘K");
24
+ });
25
+
26
+ it("keeps platform conversion explicit and preserves React nodes", () => {
27
+ const { rerender } = render(<Kbd keys={["Control", "K"]} />);
28
+ expect(screen.getByLabelText("Control + K")).toHaveTextContent("ControlK");
29
+
30
+ rerender(<Kbd keys={[<span key="mod">Fn</span>, "K"]} aria-label="Function + K" />);
31
+ expect(screen.getByLabelText("Function + K")).toHaveTextContent("FnK");
32
+ expect(screen.getByLabelText("Function + K")).not.toHaveTextContent("[object Object]");
33
+ });
34
+
35
+ it("distinguishes a physical key from a code token", () => {
36
+ render(
37
+ <>
38
+ <Kbd variant="key">Escape</Kbd>
39
+ <Kbd variant="token">HogQL</Kbd>
40
+ </>,
41
+ );
42
+
43
+ expect(screen.getByText("Escape")).toHaveClass("ph-keycap");
44
+ expect(screen.getByText("HogQL")).toHaveClass("ph-code-token");
45
+ });
46
+ });
47
+
48
+ describe("icons", () => {
49
+ it("hides decorative icons but exposes labeled icons", () => {
50
+ const { rerender } = render(<IconInfo data-testid="icon" />);
51
+ expect(screen.getByTestId("icon")).toHaveAttribute("aria-hidden", "true");
52
+
53
+ rerender(<IconInfo data-testid="icon" aria-label="Information" />);
54
+ expect(screen.getByRole("img", { name: "Information" })).not.toHaveAttribute("aria-hidden");
55
+ });
56
+
57
+ it("honors size, forwards refs, and renders original glyphs", () => {
58
+ const ref = createRef<SVGSVGElement>();
59
+ render(
60
+ <>
61
+ <IconTerminal ref={ref} size={16} data-testid="terminal" />
62
+ <IconOldWindow aria-label="Old window" />
63
+ </>,
64
+ );
65
+
66
+ expect(ref.current).toBe(screen.getByTestId("terminal"));
67
+ expect(screen.getByTestId("terminal")).toHaveStyle({ width: "16px", height: "16px" });
68
+ expect(screen.getByRole("img", { name: "Old window" })).toBeInTheDocument();
69
+ });
70
+
71
+ it("keeps aliases runtime-identical while originals stay canonical", () => {
72
+ expect(PH_ICONS.grid).toBe(CANONICAL_ICONS.gridView);
73
+ expect(PH_ICONS.play).toBe(CANONICAL_ICONS.playCircle);
74
+ expect(PH_ICONS.warning).toBe(CANONICAL_ICONS.exclamation);
75
+ expect(Object.keys(PH_ICON_ALIASES)).toEqual(expect.arrayContaining(["grid", "play", "warning"]));
76
+
77
+ for (const name of Object.keys(OLD_UI_ICONS)) {
78
+ expect(name in CANONICAL_ICONS).toBe(true);
79
+ }
80
+ });
81
+
82
+ it("gives generated icons useful identities and forwards dynamic refs", () => {
83
+ const ref = createRef<SVGSVGElement>();
84
+ render(<IconByName ref={ref} name="grid" data-testid="dynamic-icon" />);
85
+
86
+ expect(CANONICAL_ICONS.areaChart.displayName).toBe("IconAreaChart");
87
+ expect(ref.current).toBe(screen.getByTestId("dynamic-icon"));
88
+ expect(ref.current).toHaveAttribute("aria-hidden", "true");
89
+ });
90
+ });
@@ -0,0 +1,77 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { describe, expect, it } from "vitest";
3
+ import { AuthCard, AuthLayout } from "@/components/ui/AuthLayout";
4
+ import { FilterControls } from "@/components/ui/FilterControls";
5
+ import { Loader, LoadingState, Skeleton } from "@/components/ui/Loader";
6
+ import { SettingsLayout, SettingsNav } from "@/components/ui/SettingsLayout";
7
+ import { getTheme, isThemeId, THEMES } from "@/themes/registry";
8
+
9
+ describe("loading patterns", () => {
10
+ it("announces one useful loading status and keeps skeleton geometry decorative", () => {
11
+ render(
12
+ <>
13
+ <Loader variant="dots" label="Loading events" />
14
+ <LoadingState label="Preparing workspace" title="Almost ready" />
15
+ <Skeleton shape="block" data-testid="skeleton" />
16
+ </>,
17
+ );
18
+
19
+ expect(screen.getByRole("status", { name: "Loading events" })).toBeInTheDocument();
20
+ expect(screen.getByRole("status", { name: "Preparing workspace" })).toHaveTextContent("Almost ready");
21
+ expect(screen.getByTestId("skeleton")).toHaveAttribute("aria-hidden", "true");
22
+ });
23
+ });
24
+
25
+ describe("product layouts", () => {
26
+ it("labels auth and settings regions without owning their application state", () => {
27
+ render(
28
+ <>
29
+ <AuthLayout brand="Acme">
30
+ <AuthCard title="Welcome back"><input aria-label="Email" /></AuthCard>
31
+ </AuthLayout>
32
+ <SettingsLayout
33
+ title="Workspace settings"
34
+ navigation={
35
+ <SettingsNav groups={[{
36
+ label: "Workspace",
37
+ items: [{ id: "general", label: "General", href: "/settings", active: true }],
38
+ }]} />
39
+ }
40
+ >
41
+ Settings content
42
+ </SettingsLayout>
43
+ </>,
44
+ );
45
+
46
+ expect(screen.getByRole("heading", { name: "Welcome back" })).toBeInTheDocument();
47
+ expect(screen.getAllByRole("navigation", { name: "Settings" })).toHaveLength(2);
48
+ for (const link of screen.getAllByRole("link", { name: "General" })) {
49
+ expect(link).toHaveAttribute("aria-current", "page");
50
+ }
51
+ });
52
+
53
+ it("composes filter controls and applied chips in one explicit pattern", () => {
54
+ render(
55
+ <FilterControls
56
+ barProps={{ resultCount: "12 results" }}
57
+ chipsProps={{ chips: [{ id: "status", label: "Status", value: "Active", active: true }] }}
58
+ >
59
+ <button type="button">Add filter</button>
60
+ </FilterControls>,
61
+ );
62
+
63
+ expect(screen.getByRole("button", { name: "Add filter" })).toBeInTheDocument();
64
+ expect(screen.getByText("12 results")).toBeInTheDocument();
65
+ expect(screen.getByRole("group", { name: "Applied filters" })).toHaveTextContent("StatusActive");
66
+ });
67
+ });
68
+
69
+ describe("Kraken themes", () => {
70
+ it("registers a complete light and dark pair", () => {
71
+ expect(isThemeId("kraken-light")).toBe(true);
72
+ expect(isThemeId("kraken-dark")).toBe(true);
73
+ expect(getTheme("kraken-light")).toMatchObject({ colorScheme: "light", group: "Kraken" });
74
+ expect(getTheme("kraken-dark")).toMatchObject({ colorScheme: "dark", group: "Kraken" });
75
+ expect(THEMES.filter((theme) => theme.group === "Kraken")).toHaveLength(2);
76
+ });
77
+ });