@hilum/ui 0.1.1

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 (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +42 -0
  3. package/dist/icons.d.ts +1 -0
  4. package/dist/icons.js +3 -0
  5. package/dist/icons.js.map +1 -0
  6. package/dist/index.d.ts +992 -0
  7. package/dist/index.js +4433 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/tokens.css +265 -0
  10. package/dist/tokens.d.ts +189 -0
  11. package/dist/tokens.js +193 -0
  12. package/dist/tokens.js.map +1 -0
  13. package/package.json +80 -0
  14. package/src/components/__tests__/data.test.tsx +180 -0
  15. package/src/components/__tests__/disclosure.test.tsx +198 -0
  16. package/src/components/__tests__/display.test.tsx +262 -0
  17. package/src/components/__tests__/feedback.test.tsx +138 -0
  18. package/src/components/__tests__/form.test.tsx +297 -0
  19. package/src/components/__tests__/layout.test.tsx +388 -0
  20. package/src/components/__tests__/overlay.test.tsx +90 -0
  21. package/src/components/__tests__/primitives.test.tsx +248 -0
  22. package/src/components/accordion.tsx +68 -0
  23. package/src/components/action-panel.tsx +112 -0
  24. package/src/components/activity-feed.tsx +69 -0
  25. package/src/components/alert-dialog.tsx +132 -0
  26. package/src/components/alert.tsx +44 -0
  27. package/src/components/aspect-ratio.tsx +19 -0
  28. package/src/components/avatar-stack.tsx +70 -0
  29. package/src/components/avatar.tsx +103 -0
  30. package/src/components/badge.tsx +33 -0
  31. package/src/components/breadcrumb.tsx +76 -0
  32. package/src/components/button-group.tsx +45 -0
  33. package/src/components/button.tsx +60 -0
  34. package/src/components/calendar.tsx +61 -0
  35. package/src/components/card-heading.tsx +60 -0
  36. package/src/components/card.tsx +120 -0
  37. package/src/components/carousel.tsx +222 -0
  38. package/src/components/chart.tsx +77 -0
  39. package/src/components/checkbox.tsx +35 -0
  40. package/src/components/collapsible.tsx +9 -0
  41. package/src/components/color-input.tsx +90 -0
  42. package/src/components/color-picker.tsx +111 -0
  43. package/src/components/combobox.tsx +207 -0
  44. package/src/components/command-palette.tsx +137 -0
  45. package/src/components/command.tsx +248 -0
  46. package/src/components/context-menu.tsx +201 -0
  47. package/src/components/data-table.tsx +151 -0
  48. package/src/components/date-picker.tsx +56 -0
  49. package/src/components/description-list.tsx +70 -0
  50. package/src/components/dialog.tsx +97 -0
  51. package/src/components/drawer.tsx +90 -0
  52. package/src/components/dropdown-menu.tsx +202 -0
  53. package/src/components/empty-state.tsx +43 -0
  54. package/src/components/field.tsx +45 -0
  55. package/src/components/grid-list.tsx +159 -0
  56. package/src/components/hover-card.tsx +34 -0
  57. package/src/components/input-group.tsx +134 -0
  58. package/src/components/input-number.tsx +142 -0
  59. package/src/components/input-otp.tsx +68 -0
  60. package/src/components/input.tsx +23 -0
  61. package/src/components/kbd.tsx +18 -0
  62. package/src/components/label.tsx +23 -0
  63. package/src/components/media-object.tsx +49 -0
  64. package/src/components/menubar.tsx +235 -0
  65. package/src/components/native-select.tsx +44 -0
  66. package/src/components/navigation-menu.tsx +158 -0
  67. package/src/components/notification.tsx +109 -0
  68. package/src/components/page-heading.tsx +125 -0
  69. package/src/components/pagination.tsx +89 -0
  70. package/src/components/popover.tsx +35 -0
  71. package/src/components/progress.tsx +23 -0
  72. package/src/components/property-row.tsx +56 -0
  73. package/src/components/radio-card.tsx +105 -0
  74. package/src/components/radio-group.tsx +38 -0
  75. package/src/components/resizable.tsx +74 -0
  76. package/src/components/scroll-area.tsx +42 -0
  77. package/src/components/section-heading.tsx +64 -0
  78. package/src/components/select.tsx +130 -0
  79. package/src/components/separator.tsx +25 -0
  80. package/src/components/sheet.tsx +117 -0
  81. package/src/components/sidebar.tsx +636 -0
  82. package/src/components/skeleton.tsx +14 -0
  83. package/src/components/slider.tsx +24 -0
  84. package/src/components/sonner.tsx +34 -0
  85. package/src/components/spinner.tsx +37 -0
  86. package/src/components/stacked-list.tsx +64 -0
  87. package/src/components/stat-card.tsx +54 -0
  88. package/src/components/steps.tsx +179 -0
  89. package/src/components/switch.tsx +33 -0
  90. package/src/components/table.tsx +101 -0
  91. package/src/components/tabs.tsx +55 -0
  92. package/src/components/textarea.tsx +21 -0
  93. package/src/components/toggle-group.tsx +49 -0
  94. package/src/components/toggle.tsx +66 -0
  95. package/src/components/tooltip.tsx +43 -0
  96. package/src/icons.ts +158 -0
  97. package/src/index.ts +84 -0
  98. package/src/lib/utils.ts +6 -0
  99. package/src/tokens/tokens.ts +194 -0
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "@hilum/ui",
3
+ "version": "0.1.1",
4
+ "private": false,
5
+ "description": "Hilum UI — design system primitives (Button, Input, Dialog, Combobox, …) + tokens + fonts + icons",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+ssh://git@github.com/hilum-labs/hilum-ui.git",
10
+ "directory": "packages/ui"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "type": "module",
16
+ "sideEffects": [
17
+ "**/*.css"
18
+ ],
19
+ "exports": {
20
+ ".": {
21
+ "import": "./dist/index.js",
22
+ "types": "./dist/index.d.ts"
23
+ },
24
+ "./icons": {
25
+ "import": "./dist/icons.js",
26
+ "types": "./dist/icons.d.ts"
27
+ },
28
+ "./tokens": {
29
+ "import": "./dist/tokens.js",
30
+ "types": "./dist/tokens.d.ts"
31
+ },
32
+ "./tokens.css": "./dist/tokens.css"
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "src/components",
37
+ "!src/components/__tests__",
38
+ "src/icons.ts",
39
+ "src/index.ts",
40
+ "src/lib",
41
+ "src/tokens",
42
+ "README.md",
43
+ "LICENSE"
44
+ ],
45
+ "peerDependencies": {
46
+ "react": "^19.0.0",
47
+ "react-dom": "^19.0.0"
48
+ },
49
+ "dependencies": {
50
+ "@radix-ui/react-context": "^1.1.2",
51
+ "@radix-ui/react-dialog": "^1.1.15",
52
+ "@tanstack/react-table": "^8.21.3",
53
+ "class-variance-authority": "^0.7.1",
54
+ "clsx": "^2.1.1",
55
+ "embla-carousel-react": "^8.6.0",
56
+ "input-otp": "^1.4.2",
57
+ "lucide-react": "^0.511.0",
58
+ "radix-ui": "^1.1.3",
59
+ "react-day-picker": "^9.14.0",
60
+ "react-resizable-panels": "^4.10.0",
61
+ "recharts": "^3.8.1",
62
+ "sonner": "^2.0.7",
63
+ "tailwind-merge": "^2.6.0",
64
+ "vaul": "^1.1.2"
65
+ },
66
+ "devDependencies": {
67
+ "@types/react": "^19.0.0",
68
+ "@types/react-dom": "^19.0.0",
69
+ "tailwindcss": "^4.2.0",
70
+ "tsup": "^8.3.0",
71
+ "typescript": "^5.7.0"
72
+ },
73
+ "scripts": {
74
+ "build": "tsup && node ./scripts/build-tokens.mjs",
75
+ "dev": "tsup --watch",
76
+ "typecheck": "tsc --noEmit",
77
+ "lint": "eslint src",
78
+ "clean": "rm -rf dist .turbo"
79
+ }
80
+ }
@@ -0,0 +1,180 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import {
4
+ Table,
5
+ TableBody,
6
+ TableCell,
7
+ TableHead,
8
+ TableHeader,
9
+ TableRow,
10
+ TableFooter,
11
+ TableCaption,
12
+ } from "../table";
13
+ import {
14
+ Pagination,
15
+ PaginationContent,
16
+ PaginationEllipsis,
17
+ PaginationItem,
18
+ PaginationLink,
19
+ PaginationNext,
20
+ PaginationPrevious,
21
+ } from "../pagination";
22
+ import { ScrollArea } from "../scroll-area";
23
+ import { AspectRatio } from "../aspect-ratio";
24
+
25
+ /* ------------------------------------------------------------------ */
26
+ /* Table */
27
+ /* ------------------------------------------------------------------ */
28
+
29
+ describe("Table", () => {
30
+ function TestTable() {
31
+ return (
32
+ <Table>
33
+ <TableHeader>
34
+ <TableRow>
35
+ <TableHead>Name</TableHead>
36
+ <TableHead>Email</TableHead>
37
+ </TableRow>
38
+ </TableHeader>
39
+ <TableBody>
40
+ <TableRow>
41
+ <TableCell>Alice</TableCell>
42
+ <TableCell>alice@example.com</TableCell>
43
+ </TableRow>
44
+ </TableBody>
45
+ <TableFooter>
46
+ <TableRow>
47
+ <TableCell>Total</TableCell>
48
+ <TableCell>1 user</TableCell>
49
+ </TableRow>
50
+ </TableFooter>
51
+ </Table>
52
+ );
53
+ }
54
+
55
+ it("renders a table element", () => {
56
+ render(<TestTable />);
57
+ expect(screen.getByRole("table")).toBeInTheDocument();
58
+ });
59
+
60
+ it("renders column headers", () => {
61
+ render(<TestTable />);
62
+ expect(screen.getByRole("columnheader", { name: "Name" })).toBeInTheDocument();
63
+ expect(screen.getByRole("columnheader", { name: "Email" })).toBeInTheDocument();
64
+ });
65
+
66
+ it("renders data cells", () => {
67
+ render(<TestTable />);
68
+ expect(screen.getByRole("cell", { name: "Alice" })).toBeInTheDocument();
69
+ expect(screen.getByRole("cell", { name: "alice@example.com" })).toBeInTheDocument();
70
+ });
71
+
72
+ it("renders caption when provided", () => {
73
+ render(
74
+ <Table>
75
+ <TableCaption>Users list</TableCaption>
76
+ <TableBody>
77
+ <TableRow>
78
+ <TableCell>Cell</TableCell>
79
+ </TableRow>
80
+ </TableBody>
81
+ </Table>,
82
+ );
83
+ expect(screen.getByText("Users list")).toBeInTheDocument();
84
+ });
85
+ });
86
+
87
+ /* ------------------------------------------------------------------ */
88
+ /* Pagination */
89
+ /* ------------------------------------------------------------------ */
90
+
91
+ describe("Pagination", () => {
92
+ it("renders navigation", () => {
93
+ render(
94
+ <Pagination>
95
+ <PaginationContent>
96
+ <PaginationItem>
97
+ <PaginationPrevious href="#" />
98
+ </PaginationItem>
99
+ <PaginationItem>
100
+ <PaginationLink href="#" isActive>
101
+ 1
102
+ </PaginationLink>
103
+ </PaginationItem>
104
+ <PaginationItem>
105
+ <PaginationLink href="#">2</PaginationLink>
106
+ </PaginationItem>
107
+ <PaginationItem>
108
+ <PaginationEllipsis />
109
+ </PaginationItem>
110
+ <PaginationItem>
111
+ <PaginationNext href="#" />
112
+ </PaginationItem>
113
+ </PaginationContent>
114
+ </Pagination>,
115
+ );
116
+ expect(screen.getByRole("navigation", { name: /pagination/i })).toBeInTheDocument();
117
+ });
118
+
119
+ it("renders previous and next links", () => {
120
+ render(
121
+ <Pagination>
122
+ <PaginationContent>
123
+ <PaginationItem>
124
+ <PaginationPrevious href="/prev" />
125
+ </PaginationItem>
126
+ <PaginationItem>
127
+ <PaginationNext href="/next" />
128
+ </PaginationItem>
129
+ </PaginationContent>
130
+ </Pagination>,
131
+ );
132
+ expect(screen.getByRole("link", { name: /previous/i })).toBeInTheDocument();
133
+ expect(screen.getByRole("link", { name: /next/i })).toBeInTheDocument();
134
+ });
135
+
136
+ it("marks active link with aria-current", () => {
137
+ render(
138
+ <Pagination>
139
+ <PaginationContent>
140
+ <PaginationItem>
141
+ <PaginationLink href="#" isActive>
142
+ 3
143
+ </PaginationLink>
144
+ </PaginationItem>
145
+ </PaginationContent>
146
+ </Pagination>,
147
+ );
148
+ expect(screen.getByRole("link", { name: "3" })).toHaveAttribute("aria-current", "page");
149
+ });
150
+ });
151
+
152
+ /* ------------------------------------------------------------------ */
153
+ /* ScrollArea */
154
+ /* ------------------------------------------------------------------ */
155
+
156
+ describe("ScrollArea", () => {
157
+ it("renders children", () => {
158
+ render(
159
+ <ScrollArea style={{ height: 100 }}>
160
+ <p>Scrollable content</p>
161
+ </ScrollArea>,
162
+ );
163
+ expect(screen.getByText("Scrollable content")).toBeInTheDocument();
164
+ });
165
+ });
166
+
167
+ /* ------------------------------------------------------------------ */
168
+ /* AspectRatio */
169
+ /* ------------------------------------------------------------------ */
170
+
171
+ describe("AspectRatio", () => {
172
+ it("renders children", () => {
173
+ render(
174
+ <AspectRatio ratio={16 / 9}>
175
+ <img alt="hero" src="" />
176
+ </AspectRatio>,
177
+ );
178
+ expect(screen.getByAltText("hero")).toBeInTheDocument();
179
+ });
180
+ });
@@ -0,0 +1,198 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { userEvent } from "@testing-library/user-event";
4
+ import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from "../accordion";
5
+ import { Tabs, TabsList, TabsTrigger, TabsContent } from "../tabs";
6
+ import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "../collapsible";
7
+ import { Toggle } from "../toggle";
8
+ import { ToggleGroup, ToggleGroupItem } from "../toggle-group";
9
+
10
+ /* ------------------------------------------------------------------ */
11
+ /* Accordion */
12
+ /* ------------------------------------------------------------------ */
13
+
14
+ describe("Accordion", () => {
15
+ function TestAccordion() {
16
+ return (
17
+ <Accordion type="single" collapsible>
18
+ <AccordionItem value="a">
19
+ <AccordionTrigger>Section A</AccordionTrigger>
20
+ <AccordionContent>Content A</AccordionContent>
21
+ </AccordionItem>
22
+ <AccordionItem value="b">
23
+ <AccordionTrigger>Section B</AccordionTrigger>
24
+ <AccordionContent>Content B</AccordionContent>
25
+ </AccordionItem>
26
+ </Accordion>
27
+ );
28
+ }
29
+
30
+ it("renders all trigger labels", () => {
31
+ render(<TestAccordion />);
32
+ expect(screen.getByText("Section A")).toBeInTheDocument();
33
+ expect(screen.getByText("Section B")).toBeInTheDocument();
34
+ });
35
+
36
+ it("expands item on trigger click", async () => {
37
+ const user = userEvent.setup();
38
+ render(<TestAccordion />);
39
+ const triggerA = screen.getByText("Section A");
40
+ await user.click(triggerA);
41
+ expect(screen.getByText("Content A")).toBeVisible();
42
+ });
43
+
44
+ it("renders multiple type without error", () => {
45
+ render(
46
+ <Accordion type="multiple">
47
+ <AccordionItem value="x">
48
+ <AccordionTrigger>X</AccordionTrigger>
49
+ <AccordionContent>X content</AccordionContent>
50
+ </AccordionItem>
51
+ </Accordion>,
52
+ );
53
+ expect(screen.getByText("X")).toBeInTheDocument();
54
+ });
55
+ });
56
+
57
+ /* ------------------------------------------------------------------ */
58
+ /* Tabs */
59
+ /* ------------------------------------------------------------------ */
60
+
61
+ describe("Tabs", () => {
62
+ function TestTabs() {
63
+ return (
64
+ <Tabs defaultValue="tab1">
65
+ <TabsList>
66
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
67
+ <TabsTrigger value="tab2">Tab 2</TabsTrigger>
68
+ </TabsList>
69
+ <TabsContent value="tab1">Panel 1</TabsContent>
70
+ <TabsContent value="tab2">Panel 2</TabsContent>
71
+ </Tabs>
72
+ );
73
+ }
74
+
75
+ it("renders tab triggers", () => {
76
+ render(<TestTabs />);
77
+ expect(screen.getByRole("tab", { name: "Tab 1" })).toBeInTheDocument();
78
+ expect(screen.getByRole("tab", { name: "Tab 2" })).toBeInTheDocument();
79
+ });
80
+
81
+ it("shows the default tab panel", () => {
82
+ render(<TestTabs />);
83
+ expect(screen.getByText("Panel 1")).toBeInTheDocument();
84
+ });
85
+
86
+ it("switches to second tab on click", async () => {
87
+ const user = userEvent.setup();
88
+ render(<TestTabs />);
89
+ await user.click(screen.getByRole("tab", { name: "Tab 2" }));
90
+ expect(screen.getByText("Panel 2")).toBeInTheDocument();
91
+ });
92
+ });
93
+
94
+ /* ------------------------------------------------------------------ */
95
+ /* Collapsible */
96
+ /* ------------------------------------------------------------------ */
97
+
98
+ describe("Collapsible", () => {
99
+ it("renders trigger and content", () => {
100
+ render(
101
+ <Collapsible>
102
+ <CollapsibleTrigger>Toggle</CollapsibleTrigger>
103
+ <CollapsibleContent>Hidden content</CollapsibleContent>
104
+ </Collapsible>,
105
+ );
106
+ expect(screen.getByText("Toggle")).toBeInTheDocument();
107
+ });
108
+
109
+ it("renders as open by default when defaultOpen is set", () => {
110
+ render(
111
+ <Collapsible defaultOpen>
112
+ <CollapsibleTrigger>Show</CollapsibleTrigger>
113
+ <CollapsibleContent>Visible</CollapsibleContent>
114
+ </Collapsible>,
115
+ );
116
+ expect(screen.getByText("Visible")).toBeInTheDocument();
117
+ });
118
+ });
119
+
120
+ /* ------------------------------------------------------------------ */
121
+ /* Toggle */
122
+ /* ------------------------------------------------------------------ */
123
+
124
+ describe("Toggle", () => {
125
+ it("renders as a button", () => {
126
+ render(<Toggle>Bold</Toggle>);
127
+ expect(screen.getByRole("button", { name: "Bold" })).toBeInTheDocument();
128
+ });
129
+
130
+ it("renders all variants without error", () => {
131
+ const variants = ["default", "outline", "brand"] as const;
132
+ for (const variant of variants) {
133
+ const { unmount } = render(<Toggle variant={variant}>V</Toggle>);
134
+ expect(screen.getByRole("button")).toBeInTheDocument();
135
+ unmount();
136
+ }
137
+ });
138
+
139
+ it("renders all sizes without error", () => {
140
+ const sizes = ["sm", "default", "lg", "icon", "icon-sm", "icon-lg"] as const;
141
+ for (const size of sizes) {
142
+ const { unmount } = render(<Toggle size={size}>S</Toggle>);
143
+ expect(screen.getByRole("button")).toBeInTheDocument();
144
+ unmount();
145
+ }
146
+ });
147
+
148
+ it("toggles pressed state on click", async () => {
149
+ const user = userEvent.setup();
150
+ render(<Toggle>B</Toggle>);
151
+ const btn = screen.getByRole("button");
152
+ expect(btn).toHaveAttribute("data-state", "off");
153
+ await user.click(btn);
154
+ expect(btn).toHaveAttribute("data-state", "on");
155
+ });
156
+ });
157
+
158
+ /* ------------------------------------------------------------------ */
159
+ /* ToggleGroup */
160
+ /* ------------------------------------------------------------------ */
161
+
162
+ describe("ToggleGroup", () => {
163
+ // Radix ToggleGroup type="single" uses role="radio" per the ARIA toggle button pattern.
164
+ // type="multiple" uses role="button".
165
+
166
+ it("renders items (type=single → role=radio)", () => {
167
+ render(
168
+ <ToggleGroup type="single">
169
+ <ToggleGroupItem value="bold">Bold</ToggleGroupItem>
170
+ <ToggleGroupItem value="italic">Italic</ToggleGroupItem>
171
+ </ToggleGroup>,
172
+ );
173
+ expect(screen.getByText("Bold")).toBeInTheDocument();
174
+ expect(screen.getByText("Italic")).toBeInTheDocument();
175
+ });
176
+
177
+ it("activates item on click (type=single)", async () => {
178
+ const user = userEvent.setup();
179
+ render(
180
+ <ToggleGroup type="single">
181
+ <ToggleGroupItem value="a">Format A</ToggleGroupItem>
182
+ </ToggleGroup>,
183
+ );
184
+ const item = screen.getByRole("radio", { name: "Format A" });
185
+ await user.click(item);
186
+ expect(item).toHaveAttribute("data-state", "on");
187
+ });
188
+
189
+ it("supports multiple selection (type=multiple → role=button)", () => {
190
+ render(
191
+ <ToggleGroup type="multiple">
192
+ <ToggleGroupItem value="x">X</ToggleGroupItem>
193
+ <ToggleGroupItem value="y">Y</ToggleGroupItem>
194
+ </ToggleGroup>,
195
+ );
196
+ expect(screen.getAllByRole("button")).toHaveLength(2);
197
+ });
198
+ });