basuicn 0.2.7 → 0.2.8

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.
package/README.md CHANGED
@@ -1,96 +1,220 @@
1
- # basuicn — UI Component CLI
1
+ # basuicn — React UI Component CLI
2
2
 
3
- Bộ sưu tập component React hiện đại, được phân phối trực tiếp vào dự án của bạn thông qua CLI. Không cần cài đặt dependencies cồng kềnh, bạn hoàn toàn kiểm soát nguồn của mình (tương tự shadcn/ui).
3
+ Bộ sưu tập component React hiện đại, phân phối trực tiếp vào dự án qua CLI. Bạn sở hữu hoàn toàn nguồn không runtime ẩn, không black box (tương tự shadcn/ui).
4
4
 
5
5
  ---
6
6
 
7
- ## 🚀 Yêu cầu hệ thống
7
+ ## Yêu cầu
8
8
 
9
- - **Node.js**: Phiên bản 18 trở lên.
10
- - **Framework**: React + Vite + TypeScript.
11
- - **Styling**: Tailwind CSS v4 (hoặc v3).
9
+ - **Node.js** 18+
10
+ - **React** 18+ / 19+
11
+ - **Framework**: Vite hoặc Next.js (App Router / Pages Router)
12
+ - **Styling**: Tailwind CSS v4
12
13
 
13
14
  ---
14
15
 
15
- ## 📦 Bắt đầu nhanh
16
+ ## Bắt đầu nhanh
16
17
 
17
18
  ### 1. Khởi tạo dự án
18
19
 
19
- Di chuyển đến thư mục gốc của dự án React và chạy lệnh:
20
-
21
20
  ```bash
22
21
  npx basuicn init
23
22
  ```
24
23
 
25
- Lệnh này sẽ thực hiện một chuỗi các thao tác tự động:
26
- - **Cài đặt thư viện**: `@base-ui/react`, `tailwind-variants`, `clsx`, `tailwind-merge`, `lucide-react`, ...
27
- - **Cấu hình Vite**: Tự động thêm alias `@/`, `@components/`, `@lib/`, ... vào `vite.config.ts`.
28
- - **Cấu hình TypeScript**: Thêm `paths` tương ứng vào `tsconfig.json`.
29
- - **Core Components**: Copy các file tiện ích như `cn.ts`, `ThemeProvider.tsx`, `themes.ts` và biến CSS theme vào dự án.
30
- - **Patching Entry**: Tự động bọc ứng dụng của bạn trong `<ThemeProvider>` tại file `src/main.tsx`.
24
+ Lệnh này tự động:
25
+ - Cài đặt runtime packages: `@base-ui/react`, `tailwind-variants`, `clsx`, `lucide-react`, ...
26
+ - Cấu hình `vite.config.ts` với Tailwind CSS v4 + path aliases (`@/`, `@components/`, `@lib/`, ...)
27
+ - Cập nhật `tsconfig.json` với `paths` tương ứng
28
+ - Copy core files: `cn.ts`, `ThemeProvider.tsx`, `themes.ts`, `index.css`
29
+ - Bọc `<App />` trong `<ThemeProvider>` tại `src/main.tsx`
31
30
 
32
- ---
31
+ > **Next.js**: CLI tự nhận diện App Router / Pages Router và cấu hình phù hợp (`postcss.config.mjs`, `app/layout.tsx`, `pages/_app.tsx`).
33
32
 
34
- ### 2. Thêm Component
33
+ ---
35
34
 
36
- Sử dụng lệnh `add` để tải component bạn cần:
35
+ ### 2. Thêm component
37
36
 
38
37
  ```bash
39
38
  npx basuicn add button
40
- npx basuicn add button input switch # Thêm nhiều component cùng lúc
41
- npx basuicn add toast # Tự động thêm <Toaster /> vào main.tsx
39
+ npx basuicn add button input card # Thêm nhiều component
40
+ npx basuicn add # Chế độ interactive chọn từ danh sách
42
41
  ```
43
42
 
44
- > **Lưu ý**: CLI sẽ tự động nhận diện và tải về các component phụ thuộc (ví dụ `add select` sẽ tự tải thêm `popover`).
43
+ Component được copy vào `src/components/ui/<name>/`. Bạn thoải mái chỉnh sửa sau.
45
44
 
46
- ---
45
+ > CLI tự động tải các component phụ thuộc nội bộ:
46
+ > - `select` → kéo theo `popover`
47
+ > - `table` → kéo theo `checkbox`, `spinner`
48
+ > - `sheet` → kéo theo `drawer`
49
+ > - `sidebar` → kéo theo `tooltip`
47
50
 
48
- ### 3. Cập nhật & So sánh (Update & Diff)
51
+ ---
49
52
 
50
- Nếu phiên bản mới của component từ thư viện gốc, bạn có thể kiểm tra và cập nhật:
53
+ ### 3. Cập nhật component
51
54
 
52
55
  ```bash
53
- npx basuicn diff button # Xem sự khác biệt giữa code local và bản gốc trên registry
54
- npx basuicn update button # Ghi đè phiên bản cục bộ bằng bản mới nhất
56
+ npx basuicn diff button # Xem thay đổi giữa code local và registry
57
+ npx basuicn update button # Ghi đè bằng phiên bản mới nhất
58
+ npx basuicn update button card dialog # Cập nhật nhiều cùng lúc
55
59
  ```
56
60
 
57
61
  ---
58
62
 
59
- ### 4. Kiểm tra sức khỏe dự án (Doctor)
63
+ ### 4. Xóa component
64
+
65
+ ```bash
66
+ npx basuicn remove button
67
+ npx basuicn remove dialog drawer sheet
68
+ ```
69
+
70
+ ---
60
71
 
61
- Nếu bạn gặp lỗi về import hoặc cấu hình, hãy chạy lệnh sau để CLI kiểm tra và gợi ý cách sửa lỗi:
72
+ ### 5. Kiểm tra cấu hình (Doctor)
62
73
 
63
- ```bash\nnpx basuicn doctor
74
+ ```bash
75
+ npx basuicn doctor
64
76
  ```
65
77
 
78
+ Kiểm tra toàn bộ: core files, ThemeProvider, CSS import, runtime packages, path aliases, Tailwind config — và gợi ý cách sửa nếu có vấn đề.
79
+
66
80
  ---
67
81
 
68
- ## 🛠 Danh sách các lệnh (Commands)
82
+ ## Danh sách lệnh
69
83
 
70
84
  | Lệnh | Mô tả |
71
85
  |------|-------|
72
- | `init` | Thiết lập môi trường dự án ban đầu. |
73
- | `add <name>` | Thêm component vào thư mục `src/components/ui/`. |
74
- | `update <name>` | Cập nhật component lên phiên bản mới nhất. |
75
- | `diff <name>` | So sánh code hiện tại với bản gốc. |
76
- | `remove <name>` | Xóa component khỏi dự án. |
77
- | `list` | Xem danh sách tất cả các component có sẵn. |
78
- | `doctor` | Kiểm tra cấu hình và các file core của dự án. |
86
+ | `init` | Thiết lập dự án: cài packages, config Vite/Next.js, core files, ThemeProvider |
87
+ | `add <name...>` | Thêm component(s) vào `src/components/ui/` |
88
+ | `update <name...>` | Cập nhật component(s) lên phiên bản registry mới nhất |
89
+ | `diff <name...>` | So sánh code local với bản gốc trên registry |
90
+ | `remove <name...>` | Xóa component(s) dọn thư mục trống |
91
+ | `list` | Liệt tất cả component có sẵn (hiển thị trạng thái installed/available) |
92
+ | `doctor` | Kiểm tra sức khỏe cấu hình dự án |
79
93
 
80
- ---
94
+ ### Options
81
95
 
82
- ## 📂 Tùy chọn (Options)
96
+ | Flag | tả |
97
+ |------|-------|
98
+ | `--force` | Ghi đè file đã tồn tại khi `add` / `update` |
99
+ | `--local` | Đọc `registry.json` local thay vì fetch từ GitHub |
100
+ | `--help, -h` | Hiển thị hướng dẫn (dùng với command để xem chi tiết: `add --help`) |
101
+ | `--version, -v` | Hiển thị phiên bản CLI |
83
102
 
84
- - `--force`: Ghi đè các file đã tồn tại nếu có xung đột khi `add` hoặc `init`.
85
- - `--local`: Chỉ dành cho phát triển — Đọc `registry.json` từ thư mục cục bộ thay vì từ GitHub.
103
+ ---
104
+
105
+ ## Danh sách component
106
+
107
+ ### Input & Form
108
+ | Component | Mô tả |
109
+ |-----------|-------|
110
+ | `button` | Button với nhiều variant, size, loading state |
111
+ | `input` | Text input cơ bản |
112
+ | `textarea` | Textarea với auto-resize |
113
+ | `checkbox` | Checkbox có label |
114
+ | `radio` / `radio-group` | Radio button và Radio Group |
115
+ | `switch` | Toggle switch |
116
+ | `toggle` | Toggle button |
117
+ | `select` | Dropdown select |
118
+ | `autocomplete` | Input với gợi ý tự động |
119
+ | `combobox` | Combobox tìm kiếm + chọn |
120
+ | `number-input` | Input số với nút tăng/giảm |
121
+ | `input-otp` | OTP input nhiều ô |
122
+ | `slider` | Range slider |
123
+ | `rate` | Đánh giá sao |
124
+ | `file-upload` | Upload file với drag & drop |
125
+ | `form` | Form wrapper tích hợp react-hook-form + zod |
126
+ | `calendar` | Bộ chọn ngày |
127
+ | `datepicker` | Input + calendar popup |
128
+
129
+ ### Overlay & Popup
130
+ | Component | Mô tả |
131
+ |-----------|-------|
132
+ | `dialog` | Modal dialog |
133
+ | `alert-dialog` | Dialog xác nhận hành động |
134
+ | `drawer` | Drawer trượt từ cạnh màn hình |
135
+ | `sheet` | Sheet (alias drawer mở rộng) |
136
+ | `popover` | Popup neo đến element |
137
+ | `tooltip` | Tooltip hiển thị khi hover |
138
+ | `dropdown-menu` | Menu dropdown |
139
+ | `context-menu` | Menu chuột phải |
140
+ | `preview-card` | Card preview khi hover |
141
+ | `command` | Command palette (Ctrl+K) |
142
+
143
+ ### Hiển thị dữ liệu
144
+ | Component | Mô tả |
145
+ |-----------|-------|
146
+ | `table` | Bảng dữ liệu với sort, filter, pagination |
147
+ | `chart` | Biểu đồ (line, bar, pie, ...) dựa trên Recharts |
148
+ | `carousel` | Carousel / slider ảnh |
149
+ | `timeline` | Timeline sự kiện |
150
+ | `accordion` | Accordion mở rộng/thu gọn |
151
+ | `collapsible` | Nội dung có thể ẩn/hiện |
152
+ | `tree-view` | Cây phân cấp |
153
+ | `table-contents` | Mục lục tự động từ headings |
154
+
155
+ ### Navigation
156
+ | Component | Mô tả |
157
+ |-----------|-------|
158
+ | `breadcrumb` | Đường dẫn điều hướng |
159
+ | `tabs` | Tabs navigation |
160
+ | `pagination` | Phân trang |
161
+ | `menu-bar` | Menu bar ngang |
162
+ | `sidebar` | Sidebar điều hướng |
163
+
164
+ ### Feedback & Trạng thái
165
+ | Component | Mô tả |
166
+ |-----------|-------|
167
+ | `toast` | Thông báo toast (Sonner) — tự thêm `<Toaster />` vào `main.tsx` |
168
+ | `alert` | Thông báo inline |
169
+ | `spinner` | Loading spinner |
170
+ | `progress` | Thanh tiến trình |
171
+ | `skeleton` | Skeleton loading placeholder |
172
+ | `empty` | Trạng thái trống (empty state) |
173
+
174
+ ### Layout & Misc
175
+ | Component | Mô tả |
176
+ |-----------|-------|
177
+ | `card` | Card container |
178
+ | `badge` | Badge / tag nhỏ |
179
+ | `avatar` | Avatar ảnh / initials |
180
+ | `separator` | Đường kẻ phân cách |
181
+ | `aspect-ratio` | Container giữ tỉ lệ khung hình |
182
+ | `resizable` | Panel chia tay có thể kéo resize |
183
+ | `scroll-area` | Vùng cuộn tùy chỉnh |
184
+ | `typography` | Các thẻ heading, paragraph chuẩn hóa style |
185
+ | `pretty-code` | Hiển thị code với syntax highlight (Shiki) |
86
186
 
87
187
  ---
88
188
 
89
- ## 👨‍💻 Dành cho Maintainers
189
+ ## Dành cho Maintainers
190
+
191
+ ```bash
192
+ # Chạy dev server (showcase)
193
+ npm run dev
194
+
195
+ # Build registry từ source components
196
+ npm run registry:build
197
+
198
+ # Đồng bộ theme CSS từ themes.ts
199
+ npm run theme:sync
200
+
201
+ # Build CLI binary
202
+ npm run build:cli
203
+
204
+ # Kích hoạt auto version bump sau mỗi commit
205
+ npm run setup-hooks
206
+
207
+ # Đặt version thủ công
208
+ npm run version:set 1.0.0 # Set thẳng
209
+ npm run version:set major # 0.x.x → 1.0.0
210
+ npm run version:set minor # x.2.x → x.3.0
211
+
212
+ # Publish lên npm
213
+ npm publish
214
+ ```
215
+
216
+ ---
90
217
 
91
- Nếu bạn muốn đóng góp hoặc tự xây dựng registry riêng:
218
+ ## License
92
219
 
93
- 1. **Biên dịch CLI**: `npm run build:cli`
94
- 2. **Đồng bộ Theme**: `npm run theme:sync` (Tạo file CSS theme từ `themes.ts`).
95
- 3. **Xây dựng Registry**: `npm run registry:build` (Gom toàn bộ code component vào `registry.json`).
96
- 4. **Publish**: `npm publish`
220
+ MIT
package/README_CLI.md CHANGED
@@ -1,44 +1,46 @@
1
- # basuicn CLI Documentation
1
+ # basuicn CLI
2
2
 
3
- Bộ công cụ dòng lệnh mạnh mẽ để quản lý các component UI. Tương thích hoàn toàn với dự án React + Vite + TypeScript + Tailwind CSS.
3
+ Công cụ dòng lệnh để thêm, cập nhật và quản lý các component UI trong dự án React.
4
4
 
5
- ## 🚀 Cài đặt & Khởi tạo
6
-
7
- Để bắt đầu sử dụng `basuicn` trong dự án của bạn, hãy chạy:
5
+ ## Cài đặt & Khởi tạo
8
6
 
9
7
  ```bash
10
8
  npx basuicn init
11
9
  ```
12
10
 
13
- Lệnh này sẽ chuẩn bị mọi thứ cần thiết: dependencies, path aliases, Tailwind v4 configuration, `ThemeProvider` để quản lý giao diện sáng/tối.
11
+ Tự động cài packages, cấu hình Vite/Next.js, Tailwind CSS v4, path aliases và ThemeProvider.
14
12
 
15
- ## 🛠 Lệnh thông dụng
13
+ ## Các lệnh
16
14
 
17
- ### Thêm Component
18
15
  ```bash
19
- npx basuicn add <component-name>
16
+ npx basuicn add <name...> # Thêm component
17
+ npx basuicn update <name...> # Cập nhật lên phiên bản mới nhất
18
+ npx basuicn diff <name...> # So sánh với bản gốc trên registry
19
+ npx basuicn remove <name...> # Xóa component
20
+ npx basuicn list # Danh sách tất cả component
21
+ npx basuicn doctor # Kiểm tra cấu hình dự án
20
22
  ```
21
- Ví dụ: `npx basuicn add button input`.
22
23
 
23
- ### Quản lý phiên bản
24
- - **So sánh**: `npx basuicn diff <component-name>` để xem các thay đổi bạn đã sửa so với bản gốc.
25
- - **Cập nhật**: `npx basuicn update <component-name>` để lấy bản mới nhất từ remote registry.
24
+ ### Options
26
25
 
27
- ### Kiểm tra lỗi cấu hình
28
- Nếu component không hiển thị đúng style hoặc lỗi import, hãy dùng:
29
- ```bash
30
- npx basuicn doctor
31
26
  ```
27
+ --force Ghi đè file đã tồn tại
28
+ --local Dùng registry.json local thay vì fetch từ GitHub
29
+ --help Hướng dẫn chi tiết (vd: npx basuicn add --help)
30
+ --version Hiển thị phiên bản
31
+ ```
32
+
33
+ ## Cơ chế hoạt động
34
+
35
+ 1. Fetch metadata từ `registry.json` trên GitHub
36
+ 2. Cài đặt npm packages cần thiết
37
+ 3. Tải về các component phụ thuộc nội bộ tự động
38
+ 4. Copy source code vào `src/components/ui/<name>/`
39
+ 5. Patch `main.tsx` / `layout.tsx` nếu component yêu cầu (vd: `toast`)
32
40
 
33
- ## ⚙️ Cơ chế hoạt động
41
+ ## Bảo mật
34
42
 
35
- CLI hoạt động dựa trên một file `registry.json` phân phối từ GitHub. Khi bạn thêm một component:
36
- 1. CLI tải Metadata của component đó.
37
- 2. Tự động cài đặt các thư viện `npm` tương ứng.
38
- 3. Kiểm tra và tải các component phụ thuộc nội bộ.
39
- 4. Copy source code trực tiếp vào thư mục dự án của bạn.
40
- 5. (Tùy chọn) Thêm code khởi tạo vào `src/main.tsx` nếu component cần (ví dụ: `Toaster`).
43
+ - Không runtime dependency sau khi cài đặt bạn sở hữu hoàn toàn nguồn
44
+ - nguồn mở 100%, thoải mái tùy chỉnh
41
45
 
42
- ## 🛡 Bảo mật & Tin cậy
43
- - Không phụ thuộc vào runtime sau khi cài đặt.
44
- - Mã nguồn mở 100%, bạn có thể thoải mái tùy chỉnh sau khi copy vào dự án.
46
+ Xem thêm tại [README.md](./README.md).
package/dist/ui-cli.cjs CHANGED
@@ -27,7 +27,7 @@ var import_fs = __toESM(require("fs"), 1);
27
27
  var import_path = __toESM(require("path"), 1);
28
28
  var import_child_process = require("child_process");
29
29
  var import_readline = __toESM(require("readline"), 1);
30
- var VERSION = "0.2.7";
30
+ var VERSION = "0.2.8";
31
31
  var REGISTRY_LOCAL = "./registry.json";
32
32
  var REGISTRY_REMOTE = "https://raw.githubusercontent.com/Basuicn/basuicn-core/main/registry.json";
33
33
  var c = {
package/package.json CHANGED
@@ -1,104 +1,106 @@
1
- {
2
- "name": "basuicn",
3
- "private": false,
4
- "version": "0.2.7",
5
- "type": "module",
6
- "bin": {
7
- "basuicn": "./dist/ui-cli.cjs"
8
- },
9
- "files": [
10
- "dist",
11
- "registry.json",
12
- "scripts",
13
- "README_CLI.md"
14
- ],
15
- "scripts": {
16
- "dev": "vite",
17
- "build": "tsc -b && vite build",
18
- "build:cli": "node scripts/build-cli.mjs",
19
- "lint": "eslint .",
20
- "preview": "vite preview",
21
- "test": "vitest",
22
- "test:ui": "vitest --ui",
23
- "test:coverage": "vitest run --coverage",
24
- "registry:build": "npx -y tsx scripts/build-registry.ts",
25
- "theme:sync": "npx -y tsx scripts/generate-theme-css.ts",
26
- "ui:add": "npx -y tsx scripts/ui-cli.ts add",
27
- "storybook": "storybook dev -p 6006",
28
- "build-storybook": "storybook build"
29
- },
30
- "devDependencies": {
31
- "@babel/core": "^7.29.0",
32
- "@base-ui/react": "^1.3.0",
33
- "@chromatic-com/storybook": "^5.1.1",
34
- "@codesandbox/sandpack-react": "^2.20.0",
35
- "@eslint/js": "^9.39.4",
36
- "@fontsource-variable/geist": "^5.2.8",
37
- "@hookform/resolvers": "^5.2.2",
38
- "@monaco-editor/react": "^4.7.0",
39
- "@rolldown/plugin-babel": "^0.2.1",
40
- "@storybook/addon-a11y": "^10.3.4",
41
- "@storybook/addon-docs": "^10.3.4",
42
- "@storybook/addon-onboarding": "^10.3.4",
43
- "@storybook/addon-vitest": "^10.3.4",
44
- "@storybook/react-vite": "^10.3.4",
45
- "@tailwindcss/vite": "^4.2.2",
46
- "@tanstack/react-table": "^8.21.3",
47
- "@tanstack/react-virtual": "^3.13.23",
48
- "@testing-library/jest-dom": "^6.9.1",
49
- "@testing-library/react": "^16.3.2",
50
- "@testing-library/user-event": "^14.6.1",
51
- "@types/babel__core": "^7.20.5",
52
- "@types/hast": "^3.0.4",
53
- "@types/node": "^24.12.0",
54
- "@types/react": "^19.2.14",
55
- "@types/react-dom": "^19.2.3",
56
- "@vitejs/plugin-react": "^6.0.1",
57
- "@vitest/browser-playwright": "^4.1.2",
58
- "@vitest/coverage-v8": "^4.1.2",
59
- "@vitest/ui": "^4.1.2",
60
- "autoprefixer": "^10.4.27",
61
- "babel-plugin-react-compiler": "^1.0.0",
62
- "clsx": "^2.1.1",
63
- "date-fns": "^4.1.0",
64
- "dayjs": "^1.11.20",
65
- "eslint": "^9.39.4",
66
- "eslint-plugin-react-hooks": "^7.0.1",
67
- "eslint-plugin-react-refresh": "^0.5.2",
68
- "eslint-plugin-storybook": "^10.3.4",
69
- "globals": "^17.4.0",
70
- "jsdom": "^29.0.1",
71
- "lucide-react": "^0.577.0",
72
- "playwright": "^1.59.1",
73
- "postcss": "^8.5.8",
74
- "react": "^19.2.4",
75
- "react-day-picker": "^9.14.0",
76
- "react-dom": "^19.2.4",
77
- "react-hook-form": "^7.72.0",
78
- "react-live": "^4.1.8",
79
- "react-resizable-panels": "^4.8.0",
80
- "react-router-dom": "^7.13.2",
81
- "rehype-parse": "^9.0.1",
82
- "rehype-pretty-code": "^0.14.3",
83
- "rehype-react": "^8.0.0",
84
- "shiki": "^4.0.2",
85
- "sonner": "^2.0.7",
86
- "storybook": "^10.3.4",
87
- "tailwind-merge": "^3.5.0",
88
- "tailwind-variants": "^3.2.2",
89
- "tailwindcss": "^4.2.2",
90
- "tailwindcss-animate": "^1.0.7",
91
- "tw-animate-css": "^1.4.0",
92
- "typescript": "~5.9.3",
93
- "typescript-eslint": "^8.57.0",
94
- "unified": "^11.0.5",
95
- "vite": "^8.0.1",
96
- "vitest": "^4.1.2",
97
- "zod": "^4.3.6"
98
- },
99
- "dependencies": {
100
- "@recharts/devtools": "^0.0.11",
101
- "keen-slider": "^6.8.6",
102
- "recharts": "^3.8.1"
103
- }
104
- }
1
+ {
2
+ "name": "basuicn",
3
+ "private": false,
4
+ "version": "0.2.8",
5
+ "type": "module",
6
+ "bin": {
7
+ "basuicn": "./dist/ui-cli.cjs"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "registry.json",
12
+ "scripts",
13
+ "README_CLI.md"
14
+ ],
15
+ "scripts": {
16
+ "setup-hooks": "git config core.hooksPath .githooks",
17
+ "version:set": "node scripts/set-version.mjs",
18
+ "dev": "vite",
19
+ "build": "tsc -b && vite build",
20
+ "build:cli": "node scripts/build-cli.mjs",
21
+ "lint": "eslint .",
22
+ "preview": "vite preview",
23
+ "test": "vitest",
24
+ "test:ui": "vitest --ui",
25
+ "test:coverage": "vitest run --coverage",
26
+ "registry:build": "npx -y tsx scripts/build-registry.ts",
27
+ "theme:sync": "npx -y tsx scripts/generate-theme-css.ts",
28
+ "ui:add": "npx -y tsx scripts/ui-cli.ts add",
29
+ "storybook": "storybook dev -p 6006",
30
+ "build-storybook": "storybook build"
31
+ },
32
+ "devDependencies": {
33
+ "@babel/core": "^7.29.0",
34
+ "@base-ui/react": "^1.3.0",
35
+ "@chromatic-com/storybook": "^5.1.1",
36
+ "@codesandbox/sandpack-react": "^2.20.0",
37
+ "@eslint/js": "^9.39.4",
38
+ "@fontsource-variable/geist": "^5.2.8",
39
+ "@hookform/resolvers": "^5.2.2",
40
+ "@monaco-editor/react": "^4.7.0",
41
+ "@rolldown/plugin-babel": "^0.2.1",
42
+ "@storybook/addon-a11y": "^10.3.4",
43
+ "@storybook/addon-docs": "^10.3.4",
44
+ "@storybook/addon-onboarding": "^10.3.4",
45
+ "@storybook/addon-vitest": "^10.3.4",
46
+ "@storybook/react-vite": "^10.3.4",
47
+ "@tailwindcss/vite": "^4.2.2",
48
+ "@tanstack/react-table": "^8.21.3",
49
+ "@tanstack/react-virtual": "^3.13.23",
50
+ "@testing-library/jest-dom": "^6.9.1",
51
+ "@testing-library/react": "^16.3.2",
52
+ "@testing-library/user-event": "^14.6.1",
53
+ "@types/babel__core": "^7.20.5",
54
+ "@types/hast": "^3.0.4",
55
+ "@types/node": "^24.12.0",
56
+ "@types/react": "^19.2.14",
57
+ "@types/react-dom": "^19.2.3",
58
+ "@vitejs/plugin-react": "^6.0.1",
59
+ "@vitest/browser-playwright": "^4.1.2",
60
+ "@vitest/coverage-v8": "^4.1.2",
61
+ "@vitest/ui": "^4.1.2",
62
+ "autoprefixer": "^10.4.27",
63
+ "babel-plugin-react-compiler": "^1.0.0",
64
+ "clsx": "^2.1.1",
65
+ "date-fns": "^4.1.0",
66
+ "dayjs": "^1.11.20",
67
+ "eslint": "^9.39.4",
68
+ "eslint-plugin-react-hooks": "^7.0.1",
69
+ "eslint-plugin-react-refresh": "^0.5.2",
70
+ "eslint-plugin-storybook": "^10.3.4",
71
+ "globals": "^17.4.0",
72
+ "jsdom": "^29.0.1",
73
+ "lucide-react": "^0.577.0",
74
+ "playwright": "^1.59.1",
75
+ "postcss": "^8.5.8",
76
+ "react": "^19.2.4",
77
+ "react-day-picker": "^9.14.0",
78
+ "react-dom": "^19.2.4",
79
+ "react-hook-form": "^7.72.0",
80
+ "react-live": "^4.1.8",
81
+ "react-resizable-panels": "^4.8.0",
82
+ "react-router-dom": "^7.13.2",
83
+ "rehype-parse": "^9.0.1",
84
+ "rehype-pretty-code": "^0.14.3",
85
+ "rehype-react": "^8.0.0",
86
+ "shiki": "^4.0.2",
87
+ "sonner": "^2.0.7",
88
+ "storybook": "^10.3.4",
89
+ "tailwind-merge": "^3.5.0",
90
+ "tailwind-variants": "^3.2.2",
91
+ "tailwindcss": "^4.2.2",
92
+ "tailwindcss-animate": "^1.0.7",
93
+ "tw-animate-css": "^1.4.0",
94
+ "typescript": "~5.9.3",
95
+ "typescript-eslint": "^8.57.0",
96
+ "unified": "^11.0.5",
97
+ "vite": "^8.0.1",
98
+ "vitest": "^4.1.2",
99
+ "zod": "^4.3.6"
100
+ },
101
+ "dependencies": {
102
+ "@recharts/devtools": "^0.0.11",
103
+ "keen-slider": "^6.8.6",
104
+ "recharts": "^3.8.1"
105
+ }
106
+ }
package/registry.json CHANGED
@@ -231,57 +231,6 @@
231
231
  }
232
232
  ]
233
233
  },
234
- "code-sandbox": {
235
- "name": "code-sandbox",
236
- "dependencies": [
237
- "@codesandbox/sandpack-react",
238
- "lucide-react",
239
- "react-resizable-panels"
240
- ],
241
- "internalDependencies": [],
242
- "files": [
243
- {
244
- "path": "src/components/ui/code-sandbox/CodeSandbox.tsx",
245
- "content": "import React, { useState, useEffect } from 'react';\r\nimport { SandpackProvider } from '@codesandbox/sandpack-react';\r\nimport { cn } from '@/lib/utils/cn';\r\nimport { SandboxLayout } from './SandboxLayout';\r\nimport { SANDBOX_TEMPLATES } from './templates';\r\nimport type { SandboxTemplate } from './templates';\r\n\r\n// ─── Dark mode detection ─────────────────────────────────────\r\n\r\nfunction useIsDark() {\r\n const [isDark, setIsDark] = useState(\r\n () => document.documentElement.classList.contains('dark'),\r\n );\r\n\r\n useEffect(() => {\r\n const observer = new MutationObserver(() => {\r\n setIsDark(document.documentElement.classList.contains('dark'));\r\n });\r\n observer.observe(document.documentElement, {\r\n attributes: true,\r\n attributeFilter: ['class'],\r\n });\r\n return () => observer.disconnect();\r\n }, []);\r\n\r\n return isDark;\r\n}\r\n\r\n// ─── Sandpack themes ─────────────────────────────────────────\r\n\r\nconst lightTheme = {\r\n colors: {\r\n surface1: '#ffffff',\r\n surface2: '#f8fafc',\r\n surface3: '#f1f5f9',\r\n clickable: '#64748b',\r\n base: '#0f172a',\r\n disabled: '#94a3b8',\r\n hover: '#0f172a',\r\n accent: '#2f27ce',\r\n error: '#ef4444',\r\n errorSurface: '#fef2f2',\r\n },\r\n syntax: {\r\n plain: '#0f172a',\r\n comment: { color: '#94a3b8', fontStyle: 'italic' as const },\r\n keyword: '#7c3aed',\r\n tag: '#2563eb',\r\n punctuation: '#64748b',\r\n definition: '#059669',\r\n property: '#0891b2',\r\n static: '#c2410c',\r\n string: '#16a34a',\r\n },\r\n font: {\r\n body: 'Inter, system-ui, sans-serif',\r\n mono: '\"Fira Code\", Consolas, \"Courier New\", monospace',\r\n size: '13px',\r\n lineHeight: '20px',\r\n },\r\n};\r\n\r\nconst darkTheme = {\r\n colors: {\r\n surface1: '#0f172a',\r\n surface2: '#1e293b',\r\n surface3: '#334155',\r\n clickable: '#94a3b8',\r\n base: '#e2e8f0',\r\n disabled: '#475569',\r\n hover: '#f8fafc',\r\n accent: '#6366f1',\r\n error: '#ef4444',\r\n errorSurface: '#450a0a',\r\n },\r\n syntax: {\r\n plain: '#e2e8f0',\r\n comment: { color: '#64748b', fontStyle: 'italic' as const },\r\n keyword: '#c084fc',\r\n tag: '#60a5fa',\r\n punctuation: '#94a3b8',\r\n definition: '#34d399',\r\n property: '#22d3ee',\r\n static: '#fb923c',\r\n string: '#4ade80',\r\n },\r\n font: {\r\n body: 'Inter, system-ui, sans-serif',\r\n mono: '\"Fira Code\", Consolas, \"Courier New\", monospace',\r\n size: '13px',\r\n lineHeight: '20px',\r\n },\r\n};\r\n\r\n// ─── Props ───────────────────────────────────────────────────\r\n\r\nexport interface CodeSandboxProps {\r\n /** Starting template id */\r\n defaultTemplate?: string;\r\n /** Custom initial files (overrides template) */\r\n files?: Record<string, string>;\r\n /** Extra dependencies */\r\n dependencies?: Record<string, string>;\r\n /** Container className */\r\n className?: string;\r\n}\r\n\r\n// ─── Component ───────────────────────────────────────────────\r\n\r\nexport function CodeSandbox({\r\n defaultTemplate = 'react-ts',\r\n files: customFiles,\r\n dependencies: extraDeps,\r\n className,\r\n}: CodeSandboxProps) {\r\n const isDark = useIsDark();\r\n const [templateId, setTemplateId] = useState(defaultTemplate);\r\n\r\n const template: SandboxTemplate =\r\n SANDBOX_TEMPLATES.find((t) => t.id === templateId) ??\r\n SANDBOX_TEMPLATES[0];\r\n\r\n const files = customFiles ?? template.files;\r\n const deps = {\r\n ...template.dependencies,\r\n ...extraDeps,\r\n };\r\n\r\n return (\r\n <div className={cn('h-full w-full', className)}>\r\n <SandpackProvider\r\n key={templateId}\r\n template={template.template}\r\n theme={isDark ? darkTheme : lightTheme}\r\n files={files}\r\n customSetup={{\r\n dependencies: {\r\n react: '^18.0.0',\r\n 'react-dom': '^18.0.0',\r\n 'react-scripts': '^5.0.0',\r\n ...deps,\r\n },\r\n }}\r\n options={{\r\n recompileMode: 'delayed',\r\n recompileDelay: 400,\r\n classes: {\r\n 'sp-wrapper': 'h-full w-full',\r\n },\r\n }}\r\n >\r\n <SandboxLayout\r\n templateId={templateId}\r\n onTemplateChange={setTemplateId}\r\n className=\"h-full w-full\"\r\n />\r\n </SandpackProvider>\r\n </div>\r\n );\r\n}\r\n"
246
- },
247
- {
248
- "path": "src/components/ui/code-sandbox/DependencyPanel.tsx",
249
- "content": "import React, { useState } from 'react';\r\nimport { useSandpack } from '@codesandbox/sandpack-react';\r\nimport { Package, Plus, X } from 'lucide-react';\r\n\r\nconst POPULAR_PACKAGES = [\r\n 'axios',\r\n 'framer-motion',\r\n 'zustand',\r\n 'react-router-dom',\r\n 'date-fns',\r\n 'clsx',\r\n 'zod',\r\n 'react-hook-form',\r\n 'swr',\r\n 'lodash',\r\n];\r\n\r\nexport function DependencyPanel() {\r\n const { sandpack } = useSandpack();\r\n const [newDep, setNewDep] = useState('');\r\n\r\n let deps: Record<string, string> = {};\r\n try {\r\n const pkg = JSON.parse(\r\n sandpack.files['/package.json']?.code || '{}',\r\n );\r\n deps = pkg.dependencies || {};\r\n } catch {\r\n /* ignore parse error */\r\n }\r\n\r\n const addDep = (name: string) => {\r\n if (!name.trim() || deps[name]) return;\r\n try {\r\n const pkg = JSON.parse(\r\n sandpack.files['/package.json']?.code || '{}',\r\n );\r\n pkg.dependencies = { ...pkg.dependencies, [name.trim()]: 'latest' };\r\n sandpack.updateFile(\r\n '/package.json',\r\n JSON.stringify(pkg, null, 2),\r\n );\r\n setNewDep('');\r\n } catch {\r\n /* ignore */\r\n }\r\n };\r\n\r\n const removeDep = (name: string) => {\r\n if (['react', 'react-dom', 'react-scripts'].includes(name)) return;\r\n try {\r\n const pkg = JSON.parse(\r\n sandpack.files['/package.json']?.code || '{}',\r\n );\r\n const { [name]: _, ...rest } = pkg.dependencies || {};\r\n pkg.dependencies = rest;\r\n sandpack.updateFile(\r\n '/package.json',\r\n JSON.stringify(pkg, null, 2),\r\n );\r\n } catch {\r\n /* ignore */\r\n }\r\n };\r\n\r\n const available = POPULAR_PACKAGES.filter((p) => !deps[p]);\r\n\r\n return (\r\n <div className=\"flex flex-col h-full text-foreground\">\r\n <div className=\"px-3 py-2 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground shrink-0\">\r\n Dependencies\r\n </div>\r\n\r\n <form\r\n onSubmit={(e) => {\r\n e.preventDefault();\r\n addDep(newDep);\r\n }}\r\n className=\"flex gap-1 px-3 pb-2\"\r\n >\r\n <input\r\n value={newDep}\r\n onChange={(e) => setNewDep(e.target.value)}\r\n placeholder=\"Package name...\"\r\n className=\"flex-1 min-w-0 bg-muted border border-border text-foreground text-[13px] px-2 py-1.5 rounded outline-none focus:border-primary transition-colors\"\r\n />\r\n <button\r\n type=\"submit\"\r\n className=\"px-2 py-1.5 bg-primary text-primary-foreground rounded text-xs font-medium hover:bg-primary/90 transition-colors shrink-0\"\r\n >\r\n <Plus className=\"w-3.5 h-3.5\" />\r\n </button>\r\n </form>\r\n\r\n {available.length > 0 && (\r\n <div className=\"px-3 pb-3\">\r\n <div className=\"text-[10px] text-muted-foreground mb-1.5\">\r\n Quick add:\r\n </div>\r\n <div className=\"flex flex-wrap gap-1\">\r\n {available.slice(0, 5).map((p) => (\r\n <button\r\n key={p}\r\n onClick={() => addDep(p)}\r\n className=\"text-[10px] px-1.5 py-0.5 bg-muted hover:bg-accent rounded border border-border transition-colors\"\r\n >\r\n + {p}\r\n </button>\r\n ))}\r\n </div>\r\n </div>\r\n )}\r\n\r\n <div className=\"flex-1 overflow-y-auto border-t border-border\">\r\n <div className=\"px-3 py-1.5 text-[10px] text-muted-foreground\">\r\n Installed ({Object.keys(deps).length})\r\n </div>\r\n {Object.entries(deps).map(([name, version]) => (\r\n <div\r\n key={name}\r\n className=\"flex items-center justify-between px-3 py-1.5 hover:bg-muted/50 text-[13px] group\"\r\n >\r\n <div className=\"flex items-center gap-1.5 truncate min-w-0\">\r\n <Package className=\"w-3 h-3 text-muted-foreground shrink-0\" />\r\n <span className=\"truncate\">{name}</span>\r\n <span className=\"text-[10px] text-muted-foreground shrink-0\">\r\n {version as string}\r\n </span>\r\n </div>\r\n {!['react', 'react-dom', 'react-scripts'].includes(name) && (\r\n <button\r\n onClick={() => removeDep(name)}\r\n className=\"opacity-0 group-hover:opacity-100 p-0.5 hover:text-danger transition-opacity shrink-0\"\r\n >\r\n <X className=\"w-3 h-3\" />\r\n </button>\r\n )}\r\n </div>\r\n ))}\r\n </div>\r\n </div>\r\n );\r\n}\r\n"
250
- },
251
- {
252
- "path": "src/components/ui/code-sandbox/FileTree.tsx",
253
- "content": "import React, { useState, useMemo } from 'react';\r\nimport { useSandpack } from '@codesandbox/sandpack-react';\r\nimport {\r\n ChevronRight,\r\n ChevronDown,\r\n FilePlus,\r\n FolderPlus,\r\n Trash2,\r\n Folder,\r\n} from 'lucide-react';\r\nimport { cn } from '@/lib/utils/cn';\r\n\r\n// ─── Tree data structure ─────────────────────────────────────\r\n\r\ninterface TreeNode {\r\n name: string;\r\n path: string;\r\n type: 'file' | 'folder';\r\n children?: TreeNode[];\r\n}\r\n\r\nfunction buildFileTree(paths: string[]): TreeNode[] {\r\n const root: TreeNode[] = [];\r\n\r\n for (const filePath of paths) {\r\n const parts = filePath.split('/').filter(Boolean);\r\n let level = root;\r\n let currentPath = '';\r\n\r\n for (let i = 0; i < parts.length; i++) {\r\n currentPath += '/' + parts[i];\r\n const isLast = i === parts.length - 1;\r\n\r\n if (isLast) {\r\n level.push({ name: parts[i], path: currentPath, type: 'file' });\r\n } else {\r\n let folder = level.find(\r\n (n) => n.type === 'folder' && n.name === parts[i],\r\n );\r\n if (!folder) {\r\n folder = {\r\n name: parts[i],\r\n path: currentPath,\r\n type: 'folder',\r\n children: [],\r\n };\r\n level.push(folder);\r\n }\r\n level = folder.children!;\r\n }\r\n }\r\n }\r\n\r\n function sortNodes(nodes: TreeNode[]): TreeNode[] {\r\n return nodes\r\n .sort((a, b) => {\r\n if (a.type !== b.type) return a.type === 'folder' ? -1 : 1;\r\n return a.name.localeCompare(b.name);\r\n })\r\n .map((n) => {\r\n if (n.children) n.children = sortNodes(n.children);\r\n return n;\r\n });\r\n }\r\n\r\n return sortNodes(root);\r\n}\r\n\r\n// ─── File icon by extension ──────────────────────────────────\r\n\r\nfunction getFileIcon(name: string): { label: string; color: string } {\r\n const ext = name.split('.').pop()?.toLowerCase();\r\n switch (ext) {\r\n case 'js':\r\n case 'mjs':\r\n return { label: 'JS', color: '#f7df1e' };\r\n case 'jsx':\r\n return { label: 'JSX', color: '#61dafb' };\r\n case 'ts':\r\n return { label: 'TS', color: '#3178c6' };\r\n case 'tsx':\r\n return { label: 'TSX', color: '#3178c6' };\r\n case 'css':\r\n case 'scss':\r\n return { label: '#', color: '#264de4' };\r\n case 'html':\r\n return { label: '<>', color: '#e34f26' };\r\n case 'json':\r\n return { label: '{ }', color: '#5b5b5b' };\r\n case 'md':\r\n return { label: 'M', color: '#083fa1' };\r\n case 'svg':\r\n return { label: 'SVG', color: '#ffb13b' };\r\n default:\r\n return { label: '\\u00B7', color: '#6b7280' };\r\n }\r\n}\r\n\r\n// ─── Single tree node ────────────────────────────────────────\r\n\r\nfunction TreeItem({\r\n node,\r\n depth,\r\n activeFile,\r\n openFile,\r\n deleteFile,\r\n expanded,\r\n toggleFolder,\r\n}: {\r\n node: TreeNode;\r\n depth: number;\r\n activeFile: string;\r\n openFile: (p: string) => void;\r\n deleteFile: (p: string) => void;\r\n expanded: Set<string>;\r\n toggleFolder: (p: string) => void;\r\n}) {\r\n const pl = 12 + depth * 16;\r\n\r\n if (node.type === 'folder') {\r\n const isOpen = expanded.has(node.path);\r\n return (\r\n <>\r\n <div\r\n className=\"flex items-center py-1 px-2 cursor-pointer hover:bg-muted/50 text-[13px] group\"\r\n style={{ paddingLeft: pl }}\r\n onClick={() => toggleFolder(node.path)}\r\n >\r\n {isOpen ? (\r\n <ChevronDown className=\"w-3.5 h-3.5 mr-1 shrink-0 text-muted-foreground\" />\r\n ) : (\r\n <ChevronRight className=\"w-3.5 h-3.5 mr-1 shrink-0 text-muted-foreground\" />\r\n )}\r\n <Folder className=\"w-3.5 h-3.5 mr-1.5 shrink-0 text-amber-500\" />\r\n <span className=\"truncate\">{node.name}</span>\r\n </div>\r\n {isOpen &&\r\n node.children?.map((child) => (\r\n <TreeItem\r\n key={child.path}\r\n node={child}\r\n depth={depth + 1}\r\n activeFile={activeFile}\r\n openFile={openFile}\r\n deleteFile={deleteFile}\r\n expanded={expanded}\r\n toggleFolder={toggleFolder}\r\n />\r\n ))}\r\n </>\r\n );\r\n }\r\n\r\n const icon = getFileIcon(node.name);\r\n const isActive = node.path === activeFile;\r\n\r\n return (\r\n <div\r\n className={cn(\r\n 'flex items-center py-1 px-2 cursor-pointer text-[13px] group',\r\n isActive\r\n ? 'bg-primary/10 text-primary font-medium'\r\n : 'hover:bg-muted/50',\r\n )}\r\n style={{ paddingLeft: pl }}\r\n onClick={() => openFile(node.path)}\r\n >\r\n <span\r\n className=\"w-5 mr-1.5 shrink-0 text-[9px] font-bold text-center leading-none\"\r\n style={{ color: icon.color }}\r\n >\r\n {icon.label}\r\n </span>\r\n <span className=\"truncate flex-1\">{node.name}</span>\r\n <button\r\n className=\"opacity-0 group-hover:opacity-100 p-0.5 hover:text-danger shrink-0 transition-opacity\"\r\n onClick={(e) => {\r\n e.stopPropagation();\r\n deleteFile(node.path);\r\n }}\r\n >\r\n <Trash2 className=\"w-3 h-3\" />\r\n </button>\r\n </div>\r\n );\r\n}\r\n\r\n// ─── File Tree ───────────────────────────────────────────────\r\n\r\nexport function FileTree() {\r\n const { sandpack } = useSandpack();\r\n const { files, activeFile, openFile, addFile, deleteFile } = sandpack;\r\n\r\n const [expanded, setExpanded] = useState<Set<string>>(\r\n () =>\r\n new Set([\r\n '/',\r\n '/src',\r\n '/components',\r\n '/examples',\r\n '/public',\r\n ]),\r\n );\r\n const [creating, setCreating] = useState<'file' | 'folder' | null>(null);\r\n const [newName, setNewName] = useState('');\r\n\r\n const filePaths = Object.keys(files).filter((p) => !p.endsWith('.gitkeep'));\r\n const tree = useMemo(() => buildFileTree(filePaths), [filePaths]);\r\n\r\n const toggleFolder = (path: string) => {\r\n setExpanded((prev) => {\r\n const next = new Set(prev);\r\n if (next.has(path)) next.delete(path);\r\n else next.add(path);\r\n return next;\r\n });\r\n };\r\n\r\n const handleCreate = (e: React.FormEvent) => {\r\n e.preventDefault();\r\n if (!newName.trim()) return;\r\n const path = newName.startsWith('/') ? newName : '/' + newName;\r\n if (creating === 'file') {\r\n addFile(path, '');\r\n openFile(path);\r\n } else {\r\n addFile(`${path}/.gitkeep`, '');\r\n setExpanded((prev) => new Set([...prev, path]));\r\n }\r\n setNewName('');\r\n setCreating(null);\r\n };\r\n\r\n return (\r\n <div className=\"flex flex-col h-full text-foreground select-none\">\r\n {/* Header */}\r\n <div className=\"flex items-center justify-between px-3 py-2 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground shrink-0 group\">\r\n <span>Explorer</span>\r\n <div className=\"flex gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity\">\r\n <button\r\n onClick={() => setCreating('file')}\r\n className=\"p-1 hover:bg-muted rounded\"\r\n title=\"New File\"\r\n >\r\n <FilePlus className=\"w-3.5 h-3.5\" />\r\n </button>\r\n <button\r\n onClick={() => setCreating('folder')}\r\n className=\"p-1 hover:bg-muted rounded\"\r\n title=\"New Folder\"\r\n >\r\n <FolderPlus className=\"w-3.5 h-3.5\" />\r\n </button>\r\n </div>\r\n </div>\r\n\r\n {/* Create input */}\r\n {creating && (\r\n <form onSubmit={handleCreate} className=\"px-3 pb-2\">\r\n <input\r\n autoFocus\r\n type=\"text\"\r\n placeholder={\r\n creating === 'file'\r\n ? '/src/NewFile.tsx'\r\n : '/src/newfolder'\r\n }\r\n className=\"w-full bg-muted border border-primary text-foreground text-[13px] px-2 py-1 rounded outline-none font-mono\"\r\n value={newName}\r\n onChange={(e) => setNewName(e.target.value)}\r\n onBlur={() => setCreating(null)}\r\n />\r\n </form>\r\n )}\r\n\r\n {/* Tree */}\r\n <div className=\"flex-1 overflow-y-auto\">\r\n {tree.map((node) => (\r\n <TreeItem\r\n key={node.path}\r\n node={node}\r\n depth={0}\r\n activeFile={activeFile}\r\n openFile={openFile}\r\n deleteFile={deleteFile}\r\n expanded={expanded}\r\n toggleFolder={toggleFolder}\r\n />\r\n ))}\r\n </div>\r\n </div>\r\n );\r\n}\r\n"
254
- },
255
- {
256
- "path": "src/components/ui/code-sandbox/SandboxActivityBar.tsx",
257
- "content": "import React from 'react';\r\nimport { Files, Search, Package, Settings } from 'lucide-react';\r\nimport { cn } from '@/lib/utils/cn';\r\n\r\nexport type SidebarTab = 'explorer' | 'search' | 'dependencies';\r\n\r\ninterface ActivityBarProps {\r\n activeTab: SidebarTab | null;\r\n onTabChange: (tab: SidebarTab | null) => void;\r\n}\r\n\r\nconst tabs: { id: SidebarTab; icon: React.ReactNode; label: string }[] = [\r\n { id: 'explorer', icon: <Files className=\"w-[18px] h-[18px]\" />, label: 'Explorer' },\r\n { id: 'search', icon: <Search className=\"w-[18px] h-[18px]\" />, label: 'Search' },\r\n { id: 'dependencies', icon: <Package className=\"w-[18px] h-[18px]\" />, label: 'Dependencies' },\r\n];\r\n\r\nexport function SandboxActivityBar({ activeTab, onTabChange }: ActivityBarProps) {\r\n return (\r\n <div className=\"w-11 bg-muted/30 border-r border-border flex flex-col items-center py-1.5 shrink-0\">\r\n {tabs.map((tab) => (\r\n <button\r\n key={tab.id}\r\n onClick={() => onTabChange(activeTab === tab.id ? null : tab.id)}\r\n className={cn(\r\n 'relative w-full h-9 flex items-center justify-center transition-colors',\r\n activeTab === tab.id\r\n ? 'text-foreground'\r\n : 'text-muted-foreground hover:text-foreground',\r\n )}\r\n title={tab.label}\r\n >\r\n {activeTab === tab.id && (\r\n <span className=\"absolute left-0 top-[20%] bottom-[20%] w-0.5 bg-primary rounded-r\" />\r\n )}\r\n {tab.icon}\r\n </button>\r\n ))}\r\n <div className=\"flex-1\" />\r\n <button\r\n className=\"w-full h-9 flex items-center justify-center text-muted-foreground hover:text-foreground transition-colors\"\r\n title=\"Settings\"\r\n >\r\n <Settings className=\"w-[18px] h-[18px]\" />\r\n </button>\r\n </div>\r\n );\r\n}\r\n"
258
- },
259
- {
260
- "path": "src/components/ui/code-sandbox/SandboxLayout.tsx",
261
- "content": "import React, { useState } from 'react';\r\nimport {\r\n SandpackCodeEditor,\r\n SandpackPreview,\r\n} from '@codesandbox/sandpack-react';\r\nimport { Group, Panel, Separator } from 'react-resizable-panels';\r\nimport { cn } from '@/lib/utils/cn';\r\nimport { FileTree } from './FileTree';\r\nimport { SearchPanel } from './SearchPanel';\r\nimport { DependencyPanel } from './DependencyPanel';\r\nimport { SandboxActivityBar, type SidebarTab } from './SandboxActivityBar';\r\nimport { TerminalPane } from './TerminalPane';\r\nimport { SandboxToolbar } from './SandboxToolbar';\r\nimport { SandboxStatusBar } from './SandboxStatusBar';\r\n\r\n// ─── Resize Handle ───────────────────────────────────────────\r\n\r\nfunction ResizeHandle({ horizontal }: { horizontal?: boolean }) {\r\n return (\r\n <Separator\r\n className={cn(\r\n 'transition-colors shrink-0',\r\n horizontal\r\n ? 'h-[3px] hover:bg-primary/40 cursor-row-resize bg-border/60'\r\n : 'w-[3px] hover:bg-primary/40 cursor-col-resize bg-border/60',\r\n )}\r\n />\r\n );\r\n}\r\n\r\n// ─── Main Layout ───────────────────────���─────────────────────\r\n\r\nexport interface SandboxLayoutProps {\r\n templateId: string;\r\n onTemplateChange: (id: string) => void;\r\n className?: string;\r\n}\r\n\r\nexport function SandboxLayout({\r\n templateId,\r\n onTemplateChange,\r\n className,\r\n}: SandboxLayoutProps) {\r\n const [sidebarTab, setSidebarTab] = useState<SidebarTab | null>('explorer');\r\n\r\n return (\r\n <div\r\n className={cn(\r\n 'flex flex-col h-full w-full bg-background text-foreground overflow-hidden',\r\n className,\r\n )}\r\n >\r\n <SandboxToolbar templateId={templateId} onTemplateChange={onTemplateChange} />\r\n\r\n <div className=\"flex flex-1 min-h-0\">\r\n <SandboxActivityBar activeTab={sidebarTab} onTabChange={setSidebarTab} />\r\n\r\n {sidebarTab && (\r\n <div className=\"w-56 border-r border-border shrink-0 overflow-hidden bg-background\">\r\n {sidebarTab === 'explorer' && <FileTree />}\r\n {sidebarTab === 'search' && <SearchPanel />}\r\n {sidebarTab === 'dependencies' && <DependencyPanel />}\r\n </div>\r\n )}\r\n\r\n <Group orientation=\"horizontal\" className=\"h-full flex-1 min-w-0\">\r\n <Panel defaultSize={55} minSize={25}>\r\n <Group orientation=\"vertical\">\r\n <Panel defaultSize={70} minSize={20}>\r\n <SandpackCodeEditor\r\n showTabs\r\n showLineNumbers\r\n showInlineErrors\r\n wrapContent\r\n closableTabs\r\n style={{ height: '100%' }}\r\n />\r\n </Panel>\r\n <ResizeHandle horizontal />\r\n <Panel defaultSize={30} minSize={8}>\r\n <TerminalPane />\r\n </Panel>\r\n </Group>\r\n </Panel>\r\n\r\n <ResizeHandle />\r\n\r\n <Panel defaultSize={45} minSize={20}>\r\n <SandpackPreview\r\n showNavigator\r\n showRefreshButton\r\n showOpenInCodeSandbox={false}\r\n style={{ height: '100%' }}\r\n />\r\n </Panel>\r\n </Group>\r\n </div>\r\n\r\n <SandboxStatusBar />\r\n </div>\r\n );\r\n}\r\n"
262
- },
263
- {
264
- "path": "src/components/ui/code-sandbox/SandboxStatusBar.tsx",
265
- "content": "import React from 'react';\r\nimport { useSandpack } from '@codesandbox/sandpack-react';\r\nimport { cn } from '@/lib/utils/cn';\r\n\r\nexport function SandboxStatusBar() {\r\n const { sandpack } = useSandpack();\r\n const fileName = sandpack.activeFile?.split('/').pop() || '';\r\n const ext = fileName.split('.').pop()?.toUpperCase() || '';\r\n\r\n return (\r\n <div className=\"h-6 bg-primary text-primary-foreground flex items-center px-3 text-[10px] font-medium shrink-0 gap-3\">\r\n <span className=\"flex items-center gap-1.5\">\r\n <span\r\n className={cn(\r\n 'w-1.5 h-1.5 rounded-full',\r\n sandpack.status === 'running'\r\n ? 'bg-green-400'\r\n : sandpack.status === 'idle'\r\n ? 'bg-yellow-400'\r\n : 'bg-white/50',\r\n )}\r\n />\r\n {sandpack.status === 'running'\r\n ? 'Running'\r\n : sandpack.status === 'idle'\r\n ? 'Ready'\r\n : 'Loading'}\r\n </span>\r\n\r\n {sandpack.error && (\r\n <span className=\"bg-white/20 px-1.5 py-0.5 rounded text-[9px]\">\r\n 1 Error\r\n </span>\r\n )}\r\n\r\n <div className=\"flex-1\" />\r\n\r\n <span className=\"opacity-75\">{fileName}</span>\r\n <span className=\"opacity-75\">UTF-8</span>\r\n {ext && <span className=\"opacity-75\">{ext}</span>}\r\n </div>\r\n );\r\n}\r\n"
266
- },
267
- {
268
- "path": "src/components/ui/code-sandbox/SandboxToolbar.tsx",
269
- "content": "import React, { useState } from 'react';\r\nimport { ChevronDown } from 'lucide-react';\r\nimport { cn } from '@/lib/utils/cn';\r\nimport { SANDBOX_TEMPLATES } from './templates';\r\n\r\ninterface ToolbarProps {\r\n templateId: string;\r\n onTemplateChange: (id: string) => void;\r\n}\r\n\r\nexport function SandboxToolbar({ templateId, onTemplateChange }: ToolbarProps) {\r\n const [open, setOpen] = useState(false);\r\n const current = SANDBOX_TEMPLATES.find((t) => t.id === templateId);\r\n\r\n return (\r\n <div className=\"h-10 bg-muted/30 border-b border-border flex items-center px-3 shrink-0 gap-3\">\r\n <div className=\"relative\">\r\n <button\r\n onClick={() => setOpen(!open)}\r\n className=\"flex items-center gap-2 bg-background border border-border px-2.5 py-1.5 rounded-md hover:bg-muted transition-colors text-sm\"\r\n >\r\n <span>{current?.icon}</span>\r\n <span className=\"font-medium text-foreground\">{current?.label}</span>\r\n <ChevronDown className=\"w-3.5 h-3.5 text-muted-foreground\" />\r\n </button>\r\n\r\n {open && (\r\n <>\r\n <div className=\"fixed inset-0 z-40\" onClick={() => setOpen(false)} />\r\n <div className=\"absolute top-full left-0 mt-1 bg-background border border-border rounded-lg shadow-xl z-50 min-w-[260px] py-1 animate-in fade-in slide-in-from-top-2 duration-200\">\r\n {SANDBOX_TEMPLATES.map((t) => (\r\n <button\r\n key={t.id}\r\n onClick={() => {\r\n onTemplateChange(t.id);\r\n setOpen(false);\r\n }}\r\n className={cn(\r\n 'w-full text-left px-3 py-2.5 hover:bg-muted flex items-center gap-3 transition-colors',\r\n t.id === templateId && 'bg-primary/5',\r\n )}\r\n >\r\n <span className=\"text-lg shrink-0\">{t.icon}</span>\r\n <div className=\"min-w-0\">\r\n <div className=\"text-sm font-medium text-foreground\">{t.label}</div>\r\n <div className=\"text-xs text-muted-foreground\">{t.description}</div>\r\n </div>\r\n </button>\r\n ))}\r\n </div>\r\n </>\r\n )}\r\n </div>\r\n\r\n <div className=\"flex-1\" />\r\n\r\n <div className=\"text-xs text-muted-foreground bg-muted px-2.5 py-1 rounded hidden sm:block\">\r\n react-playground\r\n </div>\r\n </div>\r\n );\r\n}\r\n"
270
- },
271
- {
272
- "path": "src/components/ui/code-sandbox/SearchPanel.tsx",
273
- "content": "import React, { useState, useMemo } from 'react';\r\nimport { useSandpack } from '@codesandbox/sandpack-react';\r\n\r\nexport function SearchPanel() {\r\n const { sandpack } = useSandpack();\r\n const [query, setQuery] = useState('');\r\n\r\n const results = useMemo(() => {\r\n if (!query.trim()) return [];\r\n const q = query.toLowerCase();\r\n const matches: { path: string; line: number; text: string }[] = [];\r\n for (const [path, file] of Object.entries(sandpack.files)) {\r\n file.code.split('\\n').forEach((line, i) => {\r\n if (line.toLowerCase().includes(q)) {\r\n matches.push({ path, line: i + 1, text: line.trim() });\r\n }\r\n });\r\n }\r\n return matches.slice(0, 100);\r\n }, [query, sandpack.files]);\r\n\r\n return (\r\n <div className=\"flex flex-col h-full text-foreground\">\r\n <div className=\"px-3 py-2 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground shrink-0\">\r\n Search\r\n </div>\r\n <div className=\"px-3 pb-2\">\r\n <input\r\n value={query}\r\n onChange={(e) => setQuery(e.target.value)}\r\n placeholder=\"Search in files...\"\r\n className=\"w-full bg-muted border border-border text-foreground text-[13px] px-2 py-1.5 rounded outline-none focus:border-primary transition-colors\"\r\n />\r\n </div>\r\n <div className=\"flex-1 overflow-y-auto\">\r\n {results.length > 0 && (\r\n <div className=\"px-3 pb-1 text-[10px] text-muted-foreground\">\r\n {results.length} result{results.length !== 1 ? 's' : ''}\r\n </div>\r\n )}\r\n {results.map((r, i) => (\r\n <div\r\n key={`${r.path}:${r.line}:${i}`}\r\n className=\"px-3 py-1.5 cursor-pointer hover:bg-muted/50 text-xs border-b border-border/30\"\r\n onClick={() => sandpack.openFile(r.path)}\r\n >\r\n <div className=\"font-medium truncate\">\r\n {r.path.split('/').pop()}\r\n <span className=\"text-muted-foreground ml-1 font-normal\">\r\n {r.path}\r\n </span>\r\n </div>\r\n <div className=\"text-muted-foreground truncate font-mono\">\r\n <span className=\"text-primary mr-1\">{r.line}:</span>\r\n {r.text}\r\n </div>\r\n </div>\r\n ))}\r\n {query && results.length === 0 && (\r\n <div className=\"px-3 py-6 text-xs text-muted-foreground text-center\">\r\n No results found\r\n </div>\r\n )}\r\n </div>\r\n </div>\r\n );\r\n}\r\n"
274
- },
275
- {
276
- "path": "src/components/ui/code-sandbox/templates.ts",
277
- "content": "export interface SandboxTemplate {\r\n id: string;\r\n label: string;\r\n description: string;\r\n icon: string;\r\n template: 'react' | 'react-ts';\r\n files: Record<string, string>;\r\n dependencies?: Record<string, string>;\r\n}\r\n\r\nexport const SANDBOX_TEMPLATES: SandboxTemplate[] = [\r\n {\r\n id: 'react',\r\n label: 'React',\r\n description: 'React with JavaScript',\r\n icon: '\\u269B\\uFE0F',\r\n template: 'react',\r\n files: {\r\n '/App.js': `import React, { useState } from 'react';\r\nimport './styles.css';\r\n\r\nexport default function App() {\r\n const [count, setCount] = useState(0);\r\n\r\n return (\r\n <div className=\"app\">\r\n <h1>React Playground</h1>\r\n <p className=\"count\">{count}</p>\r\n <div className=\"actions\">\r\n <button onClick={() => setCount(c => c - 1)}>-</button>\r\n <button onClick={() => setCount(0)}>Reset</button>\r\n <button onClick={() => setCount(c => c + 1)}>+</button>\r\n </div>\r\n </div>\r\n );\r\n}\r\n`,\r\n '/styles.css': `* { margin: 0; padding: 0; box-sizing: border-box; }\r\nbody { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f8fafc; color: #0f172a; }\r\n.app { max-width: 600px; margin: 40px auto; padding: 24px; text-align: center; }\r\nh1 { font-size: 24px; margin-bottom: 16px; }\r\n.count { font-size: 48px; font-weight: 700; color: #3b82f6; margin: 16px 0; }\r\n.actions { display: flex; gap: 8px; justify-content: center; }\r\n.actions button { width: 64px; padding: 8px; border-radius: 8px; border: 1px solid #e2e8f0; background: white; cursor: pointer; font-size: 16px; transition: all 0.15s; }\r\n.actions button:hover { background: #f1f5f9; border-color: #3b82f6; }\r\n`,\r\n },\r\n },\r\n {\r\n id: 'react-ts',\r\n label: 'React + TypeScript',\r\n description: 'React with TypeScript strict mode',\r\n icon: '\\uD83D\\uDC8E',\r\n template: 'react-ts',\r\n files: {\r\n '/App.tsx': `import React, { useState } from 'react';\r\nimport './styles.css';\r\n\r\ninterface AppProps {\r\n title?: string;\r\n}\r\n\r\nconst App: React.FC<AppProps> = ({ title = 'React + TypeScript' }) => {\r\n const [count, setCount] = useState<number>(0);\r\n\r\n return (\r\n <div className=\"app\">\r\n <h1>{title}</h1>\r\n <p className=\"count\">{count}</p>\r\n <div className=\"actions\">\r\n <button onClick={() => setCount(c => c - 1)}>-</button>\r\n <button onClick={() => setCount(0)}>Reset</button>\r\n <button onClick={() => setCount(c => c + 1)}>+</button>\r\n </div>\r\n </div>\r\n );\r\n};\r\n\r\nexport default App;\r\n`,\r\n '/styles.css': `* { margin: 0; padding: 0; box-sizing: border-box; }\r\nbody { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f8fafc; color: #0f172a; }\r\n.app { max-width: 600px; margin: 40px auto; padding: 24px; text-align: center; }\r\nh1 { font-size: 24px; margin-bottom: 16px; }\r\n.count { font-size: 48px; font-weight: 700; color: #3b82f6; margin: 16px 0; }\r\n.actions { display: flex; gap: 8px; justify-content: center; }\r\n.actions button { width: 64px; padding: 8px; border-radius: 8px; border: 1px solid #e2e8f0; background: white; cursor: pointer; font-size: 16px; transition: all 0.15s; }\r\n.actions button:hover { background: #f1f5f9; border-color: #3b82f6; }\r\n`,\r\n },\r\n },\r\n {\r\n id: 'component',\r\n label: 'Component Workshop',\r\n description: 'Build & test React components',\r\n icon: '\\uD83E\\uDDE9',\r\n template: 'react-ts',\r\n files: {\r\n '/App.tsx': `import React from 'react';\r\nimport { Button } from './components/Button';\r\nimport { Card } from './components/Card';\r\nimport './styles.css';\r\n\r\nexport default function App() {\r\n return (\r\n <div className=\"app\">\r\n <h1>Component Workshop</h1>\r\n <div className=\"grid\">\r\n <Card title=\"Button Variants\">\r\n <div className=\"row\">\r\n <Button variant=\"primary\">Primary</Button>\r\n <Button variant=\"secondary\">Secondary</Button>\r\n <Button variant=\"outline\">Outline</Button>\r\n </div>\r\n </Card>\r\n <Card title=\"Button Sizes\">\r\n <div className=\"row\">\r\n <Button size=\"sm\">Small</Button>\r\n <Button size=\"md\">Medium</Button>\r\n <Button size=\"lg\">Large</Button>\r\n </div>\r\n </Card>\r\n <Card title=\"Your Component\">\r\n <p style={{ color: '#64748b' }}>Create your own component in /components!</p>\r\n </Card>\r\n </div>\r\n </div>\r\n );\r\n}\r\n`,\r\n '/components/Button.tsx': `import React from 'react';\r\n\r\ninterface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\r\n variant?: 'primary' | 'secondary' | 'outline';\r\n size?: 'sm' | 'md' | 'lg';\r\n}\r\n\r\nconst styles: Record<string, React.CSSProperties> = {\r\n primary: { background: '#3b82f6', color: 'white', border: 'none' },\r\n secondary: { background: '#e2e8f0', color: '#334155', border: 'none' },\r\n outline: { background: 'transparent', color: '#3b82f6', border: '1.5px solid #3b82f6' },\r\n sm: { padding: '6px 12px', fontSize: 12 },\r\n md: { padding: '8px 16px', fontSize: 14 },\r\n lg: { padding: '12px 24px', fontSize: 16 },\r\n};\r\n\r\nexport const Button: React.FC<ButtonProps> = ({\r\n children,\r\n variant = 'primary',\r\n size = 'md',\r\n style,\r\n ...props\r\n}) => (\r\n <button\r\n style={{\r\n borderRadius: 8,\r\n fontWeight: 500,\r\n cursor: 'pointer',\r\n transition: 'opacity 0.15s',\r\n ...styles[variant],\r\n ...styles[size],\r\n ...style,\r\n }}\r\n {...props}\r\n >\r\n {children}\r\n </button>\r\n);\r\n`,\r\n '/components/Card.tsx': `import React from 'react';\r\n\r\ninterface CardProps {\r\n title: string;\r\n children: React.ReactNode;\r\n}\r\n\r\nexport const Card: React.FC<CardProps> = ({ title, children }) => (\r\n <div style={{\r\n background: 'white',\r\n border: '1px solid #e2e8f0',\r\n borderRadius: 12,\r\n padding: 20,\r\n }}>\r\n <h3 style={{ fontSize: 16, marginBottom: 12, color: '#334155' }}>{title}</h3>\r\n {children}\r\n </div>\r\n);\r\n`,\r\n '/styles.css': `* { margin: 0; padding: 0; box-sizing: border-box; }\r\nbody { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f8fafc; color: #0f172a; }\r\n.app { max-width: 800px; margin: 24px auto; padding: 20px; }\r\nh1 { font-size: 22px; margin-bottom: 20px; }\r\n.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }\r\n@media (max-width: 600px) { .grid { grid-template-columns: 1fr; } }\r\n.row { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }\r\n`,\r\n },\r\n },\r\n {\r\n id: 'hooks',\r\n label: 'Hooks Playground',\r\n description: 'Practice React Hooks patterns',\r\n icon: '\\uD83E\\uDE9D',\r\n template: 'react-ts',\r\n files: {\r\n '/App.tsx': `import React from 'react';\r\nimport { Counter } from './examples/Counter';\r\nimport { TodoList } from './examples/TodoList';\r\nimport { FetchData } from './examples/FetchData';\r\nimport './styles.css';\r\n\r\nexport default function App() {\r\n return (\r\n <div className=\"app\">\r\n <h1>React Hooks Playground</h1>\r\n <section>\r\n <h2>useState + useCallback</h2>\r\n <Counter />\r\n </section>\r\n <section>\r\n <h2>useState + useRef</h2>\r\n <TodoList />\r\n </section>\r\n <section>\r\n <h2>useEffect + fetch</h2>\r\n <FetchData />\r\n </section>\r\n </div>\r\n );\r\n}\r\n`,\r\n '/examples/Counter.tsx': `import React, { useState, useCallback } from 'react';\r\n\r\nexport const Counter = () => {\r\n const [count, setCount] = useState(0);\r\n\r\n const increment = useCallback(() => setCount(c => c + 1), []);\r\n const decrement = useCallback(() => setCount(c => c - 1), []);\r\n const reset = useCallback(() => setCount(0), []);\r\n\r\n return (\r\n <div className=\"card\">\r\n <p className=\"count\">{count}</p>\r\n <div className=\"row center\">\r\n <button onClick={decrement}>-</button>\r\n <button onClick={reset}>Reset</button>\r\n <button onClick={increment}>+</button>\r\n </div>\r\n </div>\r\n );\r\n};\r\n`,\r\n '/examples/TodoList.tsx': `import React, { useState, useRef } from 'react';\r\n\r\ninterface Todo {\r\n id: number;\r\n text: string;\r\n done: boolean;\r\n}\r\n\r\nexport const TodoList = () => {\r\n const [todos, setTodos] = useState<Todo[]>([]);\r\n const inputRef = useRef<HTMLInputElement>(null);\r\n\r\n const addTodo = () => {\r\n const text = inputRef.current?.value.trim();\r\n if (!text) return;\r\n setTodos(prev => [...prev, { id: Date.now(), text, done: false }]);\r\n inputRef.current!.value = '';\r\n inputRef.current!.focus();\r\n };\r\n\r\n const toggle = (id: number) =>\r\n setTodos(prev =>\r\n prev.map(t => (t.id === id ? { ...t, done: !t.done } : t))\r\n );\r\n\r\n return (\r\n <div className=\"card\">\r\n <div className=\"row\" style={{ marginBottom: 12 }}>\r\n <input\r\n ref={inputRef}\r\n placeholder=\"Add todo...\"\r\n onKeyDown={e => e.key === 'Enter' && addTodo()}\r\n style={{\r\n flex: 1, padding: '8px 12px', border: '1px solid #e2e8f0',\r\n borderRadius: 6, outline: 'none', fontSize: 14,\r\n }}\r\n />\r\n <button onClick={addTodo}>Add</button>\r\n </div>\r\n {todos.map(t => (\r\n <div\r\n key={t.id}\r\n onClick={() => toggle(t.id)}\r\n style={{\r\n padding: '8px 0', borderBottom: '1px solid #f1f5f9',\r\n cursor: 'pointer', textDecoration: t.done ? 'line-through' : 'none',\r\n color: t.done ? '#94a3b8' : 'inherit',\r\n }}\r\n >\r\n {t.text}\r\n </div>\r\n ))}\r\n {todos.length === 0 && (\r\n <p style={{ color: '#94a3b8', fontSize: 13, textAlign: 'center', padding: 16 }}>\r\n No todos yet. Add one above!\r\n </p>\r\n )}\r\n </div>\r\n );\r\n};\r\n`,\r\n '/examples/FetchData.tsx': `import React, { useState, useEffect } from 'react';\r\n\r\ninterface User {\r\n id: number;\r\n name: string;\r\n email: string;\r\n}\r\n\r\nexport const FetchData = () => {\r\n const [users, setUsers] = useState<User[]>([]);\r\n const [loading, setLoading] = useState(true);\r\n const [error, setError] = useState<string | null>(null);\r\n\r\n useEffect(() => {\r\n fetch('https://jsonplaceholder.typicode.com/users')\r\n .then(res => {\r\n if (!res.ok) throw new Error('Failed to fetch');\r\n return res.json();\r\n })\r\n .then(data => {\r\n setUsers(data.slice(0, 5));\r\n setLoading(false);\r\n })\r\n .catch(err => {\r\n setError(err.message);\r\n setLoading(false);\r\n });\r\n }, []);\r\n\r\n if (loading) return <div className=\"card\">Loading...</div>;\r\n if (error) return <div className=\"card\" style={{ color: '#ef4444' }}>Error: {error}</div>;\r\n\r\n return (\r\n <div className=\"card\">\r\n {users.map(u => (\r\n <div key={u.id} style={{\r\n display: 'flex', justifyContent: 'space-between', alignItems: 'center',\r\n padding: '8px 0', borderBottom: '1px solid #f1f5f9',\r\n }}>\r\n <strong style={{ fontSize: 14 }}>{u.name}</strong>\r\n <span style={{ fontSize: 13, color: '#64748b' }}>{u.email}</span>\r\n </div>\r\n ))}\r\n </div>\r\n );\r\n};\r\n`,\r\n '/styles.css': `* { margin: 0; padding: 0; box-sizing: border-box; }\r\nbody { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f8fafc; color: #0f172a; }\r\n.app { max-width: 680px; margin: 20px auto; padding: 20px; }\r\nh1 { font-size: 22px; margin-bottom: 20px; }\r\nh2 { font-size: 12px; color: #64748b; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 0.5px; }\r\nsection { margin-bottom: 24px; }\r\n.card { background: white; border: 1px solid #e2e8f0; border-radius: 12px; padding: 16px; }\r\n.count { font-size: 48px; font-weight: 700; text-align: center; color: #3b82f6; }\r\n.row { display: flex; gap: 8px; align-items: center; }\r\n.center { justify-content: center; margin-top: 12px; }\r\nbutton { padding: 8px 16px; border-radius: 6px; border: 1px solid #e2e8f0; background: white; cursor: pointer; font-size: 14px; transition: all 0.15s; }\r\nbutton:hover { background: #f1f5f9; border-color: #3b82f6; }\r\n`,\r\n },\r\n },\r\n {\r\n id: 'tailwind',\r\n label: 'React + Tailwind',\r\n description: 'React with Tailwind CSS via CDN',\r\n icon: '\\uD83C\\uDFA8',\r\n template: 'react',\r\n files: {\r\n '/public/index.html': `<!DOCTYPE html>\r\n<html lang=\"en\">\r\n<head>\r\n <meta charset=\"UTF-8\" />\r\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\r\n <title>React + Tailwind</title>\r\n <script src=\"https://cdn.tailwindcss.com\"></script>\r\n</head>\r\n<body>\r\n <div id=\"root\"></div>\r\n</body>\r\n</html>`,\r\n '/App.js': `import React, { useState } from 'react';\r\n\r\nexport default function App() {\r\n const [count, setCount] = useState(0);\r\n\r\n return (\r\n <div className=\"min-h-screen bg-gradient-to-br from-slate-50 to-blue-50 flex items-center justify-center p-4\">\r\n <div className=\"bg-white rounded-2xl shadow-xl p-8 w-full max-w-md text-center\">\r\n <h1 className=\"text-2xl font-bold text-slate-800 mb-2\">\r\n React + Tailwind\r\n </h1>\r\n <p className=\"text-slate-500 mb-6\">Edit App.js to get started</p>\r\n\r\n <div className=\"text-6xl font-bold text-blue-500 mb-6\">{count}</div>\r\n\r\n <div className=\"flex gap-3 justify-center\">\r\n <button\r\n onClick={() => setCount(c => c - 1)}\r\n className=\"w-12 h-12 rounded-xl bg-slate-100 hover:bg-slate-200 text-lg font-medium transition-colors\"\r\n >\r\n -\r\n </button>\r\n <button\r\n onClick={() => setCount(0)}\r\n className=\"px-6 h-12 rounded-xl bg-slate-100 hover:bg-slate-200 text-sm font-medium transition-colors\"\r\n >\r\n Reset\r\n </button>\r\n <button\r\n onClick={() => setCount(c => c + 1)}\r\n className=\"w-12 h-12 rounded-xl bg-blue-500 hover:bg-blue-600 text-white text-lg font-medium transition-colors\"\r\n >\r\n +\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n );\r\n}\r\n`,\r\n '/index.js': `import React, { StrictMode } from \"react\";\r\nimport { createRoot } from \"react-dom/client\";\r\nimport App from \"./App\";\r\n\r\nconst root = createRoot(document.getElementById(\"root\"));\r\nroot.render(\r\n <StrictMode>\r\n <App />\r\n </StrictMode>\r\n);\r\n`,\r\n },\r\n },\r\n];\r\n"
278
- },
279
- {
280
- "path": "src/components/ui/code-sandbox/TerminalPane.tsx",
281
- "content": "import React, { useState } from 'react';\r\nimport { SandpackConsole, useSandpack } from '@codesandbox/sandpack-react';\r\nimport { cn } from '@/lib/utils/cn';\r\n\r\ntype TerminalTab = 'console' | 'problems';\r\n\r\nexport function TerminalPane() {\r\n const { sandpack } = useSandpack();\r\n const [tab, setTab] = useState<TerminalTab>('console');\r\n\r\n return (\r\n <div className=\"flex flex-col h-full bg-background\">\r\n <div className=\"h-7 shrink-0 bg-muted/30 border-t border-b border-border flex items-center px-3 gap-3 text-[10px] font-semibold tracking-wider\">\r\n {(['console', 'problems'] as TerminalTab[]).map((t) => (\r\n <button\r\n key={t}\r\n onClick={() => setTab(t)}\r\n className={cn(\r\n 'uppercase transition-colors pb-0.5 -mb-px border-b',\r\n tab === t\r\n ? 'text-primary border-primary'\r\n : 'text-muted-foreground border-transparent hover:text-foreground',\r\n )}\r\n >\r\n {t}\r\n {t === 'problems' && sandpack.error && (\r\n <span className=\"ml-1 text-danger font-bold\">1</span>\r\n )}\r\n </button>\r\n ))}\r\n </div>\r\n\r\n <div className=\"flex-1 overflow-auto min-h-0\">\r\n {tab === 'console' && (\r\n <SandpackConsole standalone style={{ height: '100%' }} />\r\n )}\r\n {tab === 'problems' && (\r\n <div className=\"p-3 text-xs\">\r\n {sandpack.error ? (\r\n <div className=\"text-danger font-mono whitespace-pre-wrap break-words\">\r\n {sandpack.error.message}\r\n </div>\r\n ) : (\r\n <div className=\"text-muted-foreground text-center py-4\">\r\n No problems detected\r\n </div>\r\n )}\r\n </div>\r\n )}\r\n </div>\r\n </div>\r\n );\r\n}\r\n"
282
- }
283
- ]
284
- },
285
234
  "collapsible": {
286
235
  "name": "collapsible",
287
236
  "dependencies": [
@@ -323,7 +272,7 @@
323
272
  "files": [
324
273
  {
325
274
  "path": "src/components/ui/command/Command.tsx",
326
- "content": "import * as React from 'react';\r\nimport { Dialog as BaseDialog } from '@base-ui/react';\r\nimport { tv } from 'tailwind-variants';\r\nimport { cn } from '@/lib/utils/cn';\r\nimport { Search } from 'lucide-react';\r\n\r\n// ─── Variants ────────────────────────────────────────────────────────────────\r\n\r\nconst commandVariants = tv({\r\n slots: {\r\n overlay:\r\n 'fixed inset-0 z-50 bg-black/50 backdrop-blur-sm data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0',\r\n content: [\r\n 'fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2',\r\n 'w-full max-w-lg rounded-xl border border-border bg-background shadow-2xl',\r\n 'data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 ',\r\n 'data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',\r\n 'overflow-hidden flex flex-col max-h-[min(80vh,460px)]',\r\n ].join(' '),\r\n input: [\r\n 'flex h-12 w-full bg-transparent px-4 text-sm text-foreground outline-none',\r\n 'placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50',\r\n ].join(' '),\r\n list: 'overflow-y-auto overflow-x-hidden flex-1',\r\n group: 'p-1',\r\n groupLabel: 'px-3 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider',\r\n item: [\r\n 'relative flex cursor-pointer select-none items-center gap-3 rounded-md px-3 py-2.5 text-sm outline-none',\r\n 'transition-colors',\r\n 'data-[highlighted=true]:bg-accent data-[highlighted=true]:text-accent-foreground',\r\n '[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground',\r\n ].join(' '),\r\n separator: '-mx-1 my-1 h-px bg-border',\r\n empty: 'py-8 text-center text-sm text-muted-foreground',\r\n shortcut: 'ml-auto text-xs tracking-widest text-muted-foreground/70',\r\n },\r\n});\r\n\r\nconst styles = commandVariants();\r\n\r\n// ─── Context ─────────────────────────────────────────────────────────────────\r\n\r\ninterface CommandContextValue {\r\n search: string;\r\n setSearch: React.Dispatch<React.SetStateAction<string>>;\r\n highlightedIndex: number;\r\n setHighlightedIndex: React.Dispatch<React.SetStateAction<number>>;\r\n}\r\n\r\nconst CommandContext = React.createContext<CommandContextValue | null>(null);\r\n\r\nfunction useCommand() {\r\n const ctx = React.useContext(CommandContext);\r\n if (!ctx) throw new Error('useCommand must be used within <Command>');\r\n return ctx;\r\n}\r\n\r\n// ─── Command (Root) ──────────────────────────────────────────────────────────\r\n\r\nexport interface CommandProps {\r\n open?: boolean;\r\n onOpenChange?: (open: boolean) => void;\r\n children: React.ReactNode;\r\n className?: string;\r\n}\r\n\r\nconst Command: React.FC<CommandProps> = ({ open, onOpenChange, children, className }) => {\r\n const [search, setSearch] = React.useState('');\r\n const [highlightedIndex, setHighlightedIndex] = React.useState(0);\r\n\r\n React.useEffect(() => {\r\n if (open) {\r\n setSearch('');\r\n setHighlightedIndex(0);\r\n }\r\n }, [open]);\r\n\r\n return (\r\n <CommandContext.Provider value={{ search, setSearch, highlightedIndex, setHighlightedIndex }}>\r\n <BaseDialog.Root open={open} onOpenChange={onOpenChange}>\r\n <BaseDialog.Portal>\r\n <BaseDialog.Backdrop className={styles.overlay()} />\r\n <BaseDialog.Popup className={cn(styles.content(), className)}>\r\n {children}\r\n </BaseDialog.Popup>\r\n </BaseDialog.Portal>\r\n </BaseDialog.Root>\r\n </CommandContext.Provider>\r\n );\r\n};\r\nCommand.displayName = 'Command';\r\n\r\n// ─── CommandInput ────────────────────────────────────────────────────────────\r\n\r\nexport interface CommandInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'> {\r\n onValueChange?: (value: string) => void;\r\n}\r\n\r\nconst CommandInput = React.forwardRef<HTMLInputElement, CommandInputProps>(\r\n ({ className, placeholder = 'Type a command or search...', onValueChange, ...props }, ref) => {\r\n const { search, setSearch, setHighlightedIndex } = useCommand();\r\n\r\n return (\r\n <div className=\"flex items-center border-b border-border px-3 shrink-0\">\r\n <Search className=\"mr-2 h-4 w-4 shrink-0 text-muted-foreground\" />\r\n <input\r\n ref={ref}\r\n value={search}\r\n onChange={(e) => {\r\n setSearch(e.target.value);\r\n setHighlightedIndex(0);\r\n onValueChange?.(e.target.value);\r\n }}\r\n placeholder={placeholder}\r\n className={cn(styles.input(), className)}\r\n {...props}\r\n />\r\n </div>\r\n );\r\n },\r\n);\r\nCommandInput.displayName = 'CommandInput';\r\n\r\n// ─── CommandList ─────────────────────────────────────────────────────────────\r\n\r\nconst CommandList = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\r\n ({ className, children, ...props }, ref) => (\r\n <div ref={ref} className={cn(styles.list(), className)} role=\"listbox\" {...props}>\r\n {children}\r\n </div>\r\n ),\r\n);\r\nCommandList.displayName = 'CommandList';\r\n\r\n// ─── CommandGroup ────────────────────────────────────────────────────────────\r\n\r\nexport interface CommandGroupProps extends React.HTMLAttributes<HTMLDivElement> {\r\n heading?: string;\r\n}\r\n\r\nconst CommandGroup = React.forwardRef<HTMLDivElement, CommandGroupProps>(\r\n ({ className, heading, children, ...props }, ref) => (\r\n <div ref={ref} className={cn(styles.group(), className)} role=\"group\" {...props}>\r\n {heading && <div className={styles.groupLabel()}>{heading}</div>}\r\n {children}\r\n </div>\r\n ),\r\n);\r\nCommandGroup.displayName = 'CommandGroup';\r\n\r\n// ─── CommandItem ─────────────────────────────────────────────────────────────\r\n\r\nexport interface CommandItemProps extends React.HTMLAttributes<HTMLDivElement> {\r\n disabled?: boolean;\r\n keywords?: string[];\r\n onSelect?: () => void;\r\n value?: string;\r\n}\r\n\r\nconst CommandItem = React.forwardRef<HTMLDivElement, CommandItemProps>(\r\n ({ className, disabled, keywords = [], onSelect, value, children, ...props }, ref) => {\r\n const { search, highlightedIndex, setHighlightedIndex } = useCommand();\r\n const [itemIndex] = React.useState(() => Math.random());\r\n\r\n // Filter: check value, text content, and keywords\r\n const searchable = [value ?? '', ...(typeof children === 'string' ? [children] : []), ...keywords]\r\n .join(' ')\r\n .toLowerCase();\r\n const isVisible = !search || searchable.includes(search.toLowerCase());\r\n\r\n if (!isVisible) return null;\r\n\r\n return (\r\n <div\r\n ref={ref}\r\n role=\"option\"\r\n aria-disabled={disabled || undefined}\r\n className={cn(\r\n styles.item(),\r\n disabled && 'opacity-50 pointer-events-none',\r\n className,\r\n )}\r\n onClick={() => {\r\n if (!disabled) onSelect?.();\r\n }}\r\n onKeyDown={(e) => {\r\n if ((e.key === 'Enter' || e.key === ' ') && !disabled) {\r\n e.preventDefault();\r\n onSelect?.();\r\n }\r\n }}\r\n {...props}\r\n >\r\n {children}\r\n </div>\r\n );\r\n },\r\n);\r\nCommandItem.displayName = 'CommandItem';\r\n\r\n// ─── CommandEmpty ────────────────────────────────────────────────────────────\r\n\r\nconst CommandEmpty = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\r\n ({ className, children = 'No results found.', ...props }, ref) => (\r\n <div ref={ref} className={cn(styles.empty(), className)} {...props}>\r\n {children}\r\n </div>\r\n ),\r\n);\r\nCommandEmpty.displayName = 'CommandEmpty';\r\n\r\n// ─── CommandSeparator ────────────────────────────────────────────────────────\r\n\r\nconst CommandSeparator = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\r\n ({ className, ...props }, ref) => (\r\n <div ref={ref} className={cn(styles.separator(), className)} {...props} />\r\n ),\r\n);\r\nCommandSeparator.displayName = 'CommandSeparator';\r\n\r\n// ─── CommandShortcut ─────────────────────────────────────────────────────────\r\n\r\nconst CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => (\r\n <span className={cn(styles.shortcut(), className)} {...props} />\r\n);\r\nCommandShortcut.displayName = 'CommandShortcut';\r\n\r\n// ─── Exports ─────────────────────────────────────────────────────────────────\r\n\r\nexport {\r\n Command,\r\n CommandInput,\r\n CommandList,\r\n CommandGroup,\r\n CommandItem,\r\n CommandEmpty,\r\n CommandSeparator,\r\n CommandShortcut,\r\n};\r\n"
275
+ "content": "import * as React from 'react';\r\nimport { Dialog as BaseDialog } from '@base-ui/react';\r\nimport { tv } from 'tailwind-variants';\r\nimport { cn } from '@/lib/utils/cn';\r\nimport { Search } from 'lucide-react';\r\n\r\n// ─── Variants ────────────────────────────────────────────────────────────────\r\n\r\nconst commandVariants = tv({\r\n slots: {\r\n overlay:\r\n 'fixed inset-0 z-50 bg-black/50 backdrop-blur-sm data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0',\r\n content: [\r\n 'fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2',\r\n 'w-full max-w-lg rounded-xl border border-border bg-background shadow-2xl',\r\n 'data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 ',\r\n 'data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',\r\n 'overflow-hidden flex flex-col max-h-[min(80vh,460px)]',\r\n ].join(' '),\r\n input: [\r\n 'flex h-12 w-full bg-transparent px-4 text-sm text-foreground outline-none',\r\n 'placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50',\r\n ].join(' '),\r\n list: 'overflow-y-auto overflow-x-hidden flex-1',\r\n group: 'p-1',\r\n groupLabel: 'px-3 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider',\r\n item: [\r\n 'relative flex cursor-pointer select-none items-center gap-3 rounded-md px-3 py-2.5 text-sm outline-none',\r\n 'transition-colors',\r\n 'data-[highlighted=true]:bg-accent data-[highlighted=true]:text-accent-foreground',\r\n '[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground',\r\n ].join(' '),\r\n separator: '-mx-1 my-1 h-px bg-border',\r\n empty: 'py-8 text-center text-sm text-muted-foreground',\r\n shortcut: 'ml-auto text-xs tracking-widest text-muted-foreground/70',\r\n },\r\n});\r\n\r\nconst styles = commandVariants();\r\n\r\n// ─── Context ─────────────────────────────────────────────────────────────────\r\n\r\ninterface CommandContextValue {\r\n search: string;\r\n setSearch: React.Dispatch<React.SetStateAction<string>>;\r\n highlightedIndex: number;\r\n setHighlightedIndex: React.Dispatch<React.SetStateAction<number>>;\r\n visibleItemCount: number;\r\n setVisibleItemCount: React.Dispatch<React.SetStateAction<number>>;\r\n}\r\n\r\nconst CommandContext = React.createContext<CommandContextValue | null>(null);\r\n\r\nfunction useCommand() {\r\n const ctx = React.useContext(CommandContext);\r\n if (!ctx) throw new Error('useCommand must be used within <Command>');\r\n return ctx;\r\n}\r\n\r\ninterface GroupContextValue {\r\n groupId: string;\r\n visibleCount: number;\r\n setVisibleCount: React.Dispatch<React.SetStateAction<number>>;\r\n}\r\n\r\nconst GroupContext = React.createContext<GroupContextValue | null>(null);\r\n\r\nfunction useGroup() {\r\n return React.useContext(GroupContext);\r\n}\r\n\r\n// ─── Command (Root) ──────────────────────────────────────────────────────────\r\n\r\nexport interface CommandProps {\r\n open?: boolean;\r\n onOpenChange?: (open: boolean) => void;\r\n children: React.ReactNode;\r\n className?: string;\r\n}\r\n\r\nconst Command: React.FC<CommandProps> = ({ open, onOpenChange, children, className }) => {\r\n const [search, setSearch] = React.useState('');\r\n const [highlightedIndex, setHighlightedIndex] = React.useState(0);\r\n const [visibleItemCount, setVisibleItemCount] = React.useState(0);\r\n\r\n React.useEffect(() => {\r\n if (open) {\r\n setSearch('');\r\n setHighlightedIndex(0);\r\n setVisibleItemCount(0);\r\n }\r\n }, [open]);\r\n\r\n return (\r\n <CommandContext.Provider value={{ search, setSearch, highlightedIndex, setHighlightedIndex, visibleItemCount, setVisibleItemCount }}>\r\n <BaseDialog.Root open={open} onOpenChange={onOpenChange}>\r\n <BaseDialog.Portal>\r\n <BaseDialog.Backdrop className={styles.overlay()} />\r\n <BaseDialog.Popup className={cn(styles.content(), className)}>\r\n {children}\r\n </BaseDialog.Popup>\r\n </BaseDialog.Portal>\r\n </BaseDialog.Root>\r\n </CommandContext.Provider>\r\n );\r\n};\r\nCommand.displayName = 'Command';\r\n\r\n// ─── CommandInput ────────────────────────────────────────────────────────────\r\n\r\nexport interface CommandInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'> {\r\n onValueChange?: (value: string) => void;\r\n}\r\n\r\nconst CommandInput = React.forwardRef<HTMLInputElement, CommandInputProps>(\r\n ({ className, placeholder = 'Type a command or search...', onValueChange, ...props }, ref) => {\r\n const { search, setSearch, setHighlightedIndex } = useCommand();\r\n\r\n return (\r\n <div className=\"flex items-center border-b border-border px-3 shrink-0\">\r\n <Search className=\"mr-2 h-4 w-4 shrink-0 text-muted-foreground\" />\r\n <input\r\n ref={ref}\r\n value={search}\r\n onChange={(e) => {\r\n setSearch(e.target.value);\r\n setHighlightedIndex(0);\r\n onValueChange?.(e.target.value);\r\n }}\r\n placeholder={placeholder}\r\n className={cn(styles.input(), className)}\r\n {...props}\r\n />\r\n </div>\r\n );\r\n },\r\n);\r\nCommandInput.displayName = 'CommandInput';\r\n\r\n// ─── CommandList ─────────────────────────────────────────────────────────────\r\n\r\nconst CommandList = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\r\n ({ className, children, ...props }, ref) => (\r\n <div ref={ref} className={cn(styles.list(), className)} role=\"listbox\" {...props}>\r\n {children}\r\n </div>\r\n ),\r\n);\r\nCommandList.displayName = 'CommandList';\r\n\r\n// ─── CommandGroup ────────────────────────────────────────────────────────────\r\n\r\nexport interface CommandGroupProps extends React.HTMLAttributes<HTMLDivElement> {\r\n heading?: string;\r\n}\r\n\r\nconst CommandGroup = React.forwardRef<HTMLDivElement, CommandGroupProps>(\r\n ({ className, heading, children, ...props }, ref) => {\r\n const [groupId] = React.useState(() => Math.random().toString(36));\r\n const [visibleCount, setVisibleCount] = React.useState(0);\r\n\r\n return (\r\n <GroupContext.Provider value={{ groupId, visibleCount, setVisibleCount }}>\r\n <div ref={ref} className={cn(styles.group(), className)} role=\"group\" {...props}>\r\n {heading && visibleCount > 0 && <div className={styles.groupLabel()}>{heading}</div>}\r\n {children}\r\n </div>\r\n </GroupContext.Provider>\r\n );\r\n }\r\n);\r\nCommandGroup.displayName = 'CommandGroup';\r\n\r\n// ─── CommandItem ─────────────────────────────────────────────────────────────\r\n\r\nexport interface CommandItemProps extends React.HTMLAttributes<HTMLDivElement> {\r\n disabled?: boolean;\r\n keywords?: string[];\r\n onSelect?: () => void;\r\n value?: string;\r\n}\r\n\r\nconst CommandItem = React.forwardRef<HTMLDivElement, CommandItemProps>(\r\n ({ className, disabled, keywords = [], onSelect, value, children, ...props }, ref) => {\r\n const { search, highlightedIndex, setHighlightedIndex, setVisibleItemCount } = useCommand();\r\n const [itemIndex] = React.useState(() => Math.random());\r\n\r\n // Filter: check value, text content, and keywords\r\n const searchable = [value ?? '', ...(typeof children === 'string' ? [children] : []), ...keywords]\r\n .join(' ')\r\n .toLowerCase();\r\n const isVisible = !search || searchable.includes(search.toLowerCase());\r\n\r\n const group = useGroup();\r\n\r\n React.useEffect(() => {\r\n setVisibleItemCount((prev) => prev + (isVisible ? 1 : 0));\r\n group?.setVisibleCount((prev) => prev + (isVisible ? 1 : 0));\r\n return () => {\r\n setVisibleItemCount((prev) => prev - (isVisible ? 1 : 0));\r\n group?.setVisibleCount((prev) => prev - (isVisible ? 1 : 0));\r\n };\r\n }, [isVisible, setVisibleItemCount, group]);\r\n\r\n if (!isVisible) return null;\r\n\r\n return (\r\n <div\r\n ref={ref}\r\n role=\"option\"\r\n aria-disabled={disabled || undefined}\r\n className={cn(\r\n styles.item(),\r\n disabled && 'opacity-50 pointer-events-none',\r\n className,\r\n )}\r\n onClick={() => {\r\n if (!disabled) onSelect?.();\r\n }}\r\n onKeyDown={(e) => {\r\n if ((e.key === 'Enter' || e.key === ' ') && !disabled) {\r\n e.preventDefault();\r\n onSelect?.();\r\n }\r\n }}\r\n {...props}\r\n >\r\n {children}\r\n </div>\r\n );\r\n },\r\n);\r\nCommandItem.displayName = 'CommandItem';\r\n\r\n// ─── CommandEmpty ────────────────────────────────────────────────────────────\r\n\r\nconst CommandEmpty = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\r\n ({ className, children = 'No results found.', ...props }, ref) => {\r\n const { visibleItemCount } = useCommand();\r\n if (visibleItemCount > 0) return null;\r\n return (\r\n <div ref={ref} className={cn(styles.empty(), className)} {...props}>\r\n {children}\r\n </div>\r\n );\r\n },\r\n);\r\nCommandEmpty.displayName = 'CommandEmpty';\r\n\r\n// ─── CommandSeparator ────────────────────────────────────────────────────────\r\n\r\nconst CommandSeparator = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\r\n ({ className, ...props }, ref) => (\r\n <div ref={ref} className={cn(styles.separator(), className)} {...props} />\r\n ),\r\n);\r\nCommandSeparator.displayName = 'CommandSeparator';\r\n\r\n// ─── CommandShortcut ─────────────────────────────────────────────────────────\r\n\r\nconst CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => (\r\n <span className={cn(styles.shortcut(), className)} {...props} />\r\n);\r\nCommandShortcut.displayName = 'CommandShortcut';\r\n\r\n// ─── Exports ─────────────────────────────────────────────────────────────────\r\n\r\nexport {\r\n Command,\r\n CommandInput,\r\n CommandList,\r\n CommandGroup,\r\n CommandItem,\r\n CommandEmpty,\r\n CommandSeparator,\r\n CommandShortcut,\r\n};\r\n"
327
276
  }
328
277
  ]
329
278
  },
@@ -379,7 +328,7 @@
379
328
  "files": [
380
329
  {
381
330
  "path": "src/components/ui/dialog/Dialog.tsx",
382
- "content": "import * as React from 'react';\r\nimport { Dialog as BaseDialog } from '@base-ui/react';\r\nimport { X } from 'lucide-react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\n\r\nconst dialogVariants = tv({\r\n slots: {\r\n overlay:\r\n 'fixed inset-0 z-50 bg-black/50 backdrop-blur-sm data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0',\r\n content: [\r\n 'fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2 p-6',\r\n 'w-full max-w-lg rounded-xl border border-border bg-background shadow-2xl',\r\n 'data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 ',\r\n 'data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',\r\n ].join(' '),\r\n header: 'flex flex-col space-y-1.5 text-center sm:text-left',\r\n footer: 'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 mt-auto',\r\n title: 'text-lg font-semibold leading-none tracking-tight',\r\n description: 'text-sm text-muted-foreground',\r\n close:\r\n 'absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 disabled:pointer-events-none data-open:bg-accent data-open:text-muted-foreground',\r\n },\r\n variants: {\r\n size: {\r\n default: {\r\n content: 'max-w-lg sm:rounded-lg',\r\n },\r\n fullScreen: {\r\n content:\r\n 'inset-0 left-0 top-0 translate-x-0 translate-y-0 max-w-none h-full rounded-none border-none',\r\n },\r\n },\r\n },\r\n defaultVariants: {\r\n size: 'default',\r\n },\r\n});\r\n\r\n/* ─── Root ─── */\r\nconst Dialog = BaseDialog.Root;\r\n\r\n/* ─── Trigger ─── */\r\n// Hỗ trợ cả render={} (Base UI) lẫn children trực tiếp.\r\n// Nếu children là một React element (e.g. <Button>), tự động dùng làm render prop\r\n// để tránh nested button (<button><button>…</button></button>).\r\ntype BaseTriggerProps = React.ComponentPropsWithoutRef<typeof BaseDialog.Trigger>;\r\n\r\ninterface DialogTriggerProps extends Omit<BaseTriggerProps, 'render'> {\r\n render?: BaseTriggerProps['render'];\r\n children?: React.ReactNode;\r\n}\r\n\r\nconst DialogTrigger = React.forwardRef<HTMLElement, DialogTriggerProps>(\r\n ({ render: renderProp, children, ...props }, ref) => {\r\n const resolvedRender =\r\n renderProp ?? (React.isValidElement(children) ? children : undefined);\r\n\r\n return (\r\n <BaseDialog.Trigger\r\n ref={ref as React.Ref<HTMLButtonElement>}\r\n render={resolvedRender}\r\n {...props}\r\n >\r\n {resolvedRender ? undefined : children}\r\n </BaseDialog.Trigger>\r\n );\r\n },\r\n);\r\nDialogTrigger.displayName = 'DialogTrigger';\r\n\r\n\r\n/* ─── Close (re-export for custom close buttons) ─── */\r\nconst DialogClose = BaseDialog.Close;\r\n\r\n/* ─── Content (Portal + Backdrop + Popup + default X button) ─── */\r\ninterface DialogContentProps\r\n extends Omit<React.ComponentPropsWithoutRef<typeof BaseDialog.Popup>, 'className'>,\r\n VariantProps<typeof dialogVariants> {\r\n className?: string;\r\n}\r\n\r\nconst DialogContent = React.forwardRef<HTMLDivElement, DialogContentProps>(\r\n ({ className, children, size, ...props }, ref) => {\r\n const slots = dialogVariants({ size });\r\n return (\r\n <BaseDialog.Portal>\r\n <BaseDialog.Backdrop className={slots.overlay()} />\r\n <BaseDialog.Popup ref={ref} className={slots.content({ className })} {...props}>\r\n {children}\r\n <BaseDialog.Close className={slots.close()}>\r\n <X className=\"h-4 w-4\" />\r\n <span className=\"sr-only\">Close</span>\r\n </BaseDialog.Close>\r\n </BaseDialog.Popup>\r\n </BaseDialog.Portal>\r\n );\r\n },\r\n);\r\nDialogContent.displayName = 'DialogContent';\r\n\r\n/* ─── Header ─── */\r\nconst DialogHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\r\n ({ className, ...props }, ref) => {\r\n const slots = dialogVariants();\r\n return <div ref={ref} className={slots.header({ className })} {...props} />;\r\n },\r\n);\r\nDialogHeader.displayName = 'DialogHeader';\r\n\r\n/* ─── Footer ─── */\r\nconst DialogFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\r\n ({ className, ...props }, ref) => {\r\n const slots = dialogVariants();\r\n return <div ref={ref} className={slots.footer({ className })} {...props} />;\r\n },\r\n);\r\nDialogFooter.displayName = 'DialogFooter';\r\n\r\n/* ─── Title ─── */\r\nconst DialogTitle = React.forwardRef<\r\n HTMLHeadingElement,\r\n Omit<React.ComponentPropsWithoutRef<typeof BaseDialog.Title>, 'className'> & { className?: string }\r\n>(({ className, ...props }, ref) => {\r\n const slots = dialogVariants();\r\n return <BaseDialog.Title ref={ref} className={slots.title({ className })} {...props} />;\r\n});\r\nDialogTitle.displayName = 'DialogTitle';\r\n\r\n/* ─── Description ─── */\r\nconst DialogDescription = React.forwardRef<\r\n HTMLParagraphElement,\r\n Omit<React.ComponentPropsWithoutRef<typeof BaseDialog.Description>, 'className'> & { className?: string }\r\n>(({ className, ...props }, ref) => {\r\n const slots = dialogVariants();\r\n return (\r\n <BaseDialog.Description ref={ref} className={slots.description({ className })} {...props} />\r\n );\r\n});\r\nDialogDescription.displayName = 'DialogDescription';\r\n\r\nexport {\r\n Dialog,\r\n DialogTrigger,\r\n DialogContent,\r\n DialogHeader,\r\n DialogFooter,\r\n DialogTitle,\r\n DialogDescription,\r\n DialogClose,\r\n};\r\n"
331
+ "content": "import * as React from 'react';\r\nimport { Dialog as BaseDialog } from '@base-ui/react';\r\nimport { X } from 'lucide-react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\n\r\nconst dialogVariants = tv({\r\n slots: {\r\n overlay:\r\n 'fixed inset-0 z-50 bg-black/50 backdrop-blur-sm data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0',\r\n content: [\r\n 'fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2 p-6',\r\n 'w-full max-w-lg rounded-xl border border-border bg-background shadow-2xl',\r\n 'data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 ',\r\n 'data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',\r\n ].join(' '),\r\n header: 'flex flex-col space-y-1.5 text-center sm:text-left',\r\n footer: 'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 mt-auto',\r\n title: 'text-lg font-semibold leading-none tracking-tight',\r\n description: 'text-sm text-muted-foreground',\r\n close:\r\n 'absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 disabled:pointer-events-none data-open:bg-accent data-open:text-muted-foreground',\r\n },\r\n variants: {\r\n size: {\r\n default: {\r\n content: 'max-w-lg sm:rounded-lg',\r\n },\r\n fullScreen: {\r\n content:\r\n 'inset-0 left-0 top-0 translate-x-0 translate-y-0 max-w-none h-full rounded-none border-none',\r\n },\r\n },\r\n },\r\n defaultVariants: {\r\n size: 'default',\r\n },\r\n});\r\n\r\n/* ─── Root ─── */\r\nconst Dialog = BaseDialog.Root;\r\n\r\n/* ─── Trigger ─── */\r\n// Hỗ trợ cả render={} (Base UI) lẫn children trực tiếp.\r\n// Nếu children là một React element (e.g. <Button>), tự động dùng làm render prop\r\n// để tránh nested button (<button><button>…</button></button>).\r\ntype BaseTriggerProps = React.ComponentPropsWithoutRef<typeof BaseDialog.Trigger>;\r\n\r\ninterface DialogTriggerProps extends Omit<BaseTriggerProps, 'render'> {\r\n render?: BaseTriggerProps['render'];\r\n children?: React.ReactNode;\r\n}\r\n\r\nconst DialogTrigger = React.forwardRef<HTMLElement, DialogTriggerProps>(\r\n ({ render: renderProp, children, ...props }, ref) => {\r\n const resolvedRender =\r\n renderProp ?? (React.isValidElement(children) ? children : undefined);\r\n\r\n return (\r\n <BaseDialog.Trigger\r\n ref={ref as React.Ref<HTMLButtonElement>}\r\n render={resolvedRender}\r\n {...props}\r\n >\r\n {resolvedRender ? undefined : children}\r\n </BaseDialog.Trigger>\r\n );\r\n },\r\n);\r\nDialogTrigger.displayName = 'DialogTrigger';\r\n\r\n\r\n/* ─── Close (re-export for custom close buttons) ─── */\r\nconst DialogClose = React.forwardRef<\r\n HTMLButtonElement,\r\n React.ComponentPropsWithoutRef<typeof BaseDialog.Close>\r\n>(({ children, render: renderProp, ...props }, ref) => {\r\n const isElement = React.isValidElement(children);\r\n return (\r\n <BaseDialog.Close\r\n ref={ref}\r\n render={renderProp ?? (isElement ? (children as React.ReactElement) : undefined)}\r\n {...props}\r\n >\r\n {isElement ? undefined : children}\r\n </BaseDialog.Close>\r\n );\r\n});\r\nDialogClose.displayName = 'DialogClose';\r\n\r\n/* ─── Content (Portal + Backdrop + Popup + default X button) ─── */\r\ninterface DialogContentProps\r\n extends Omit<React.ComponentPropsWithoutRef<typeof BaseDialog.Popup>, 'className'>,\r\n VariantProps<typeof dialogVariants> {\r\n className?: string;\r\n}\r\n\r\nconst DialogContent = React.forwardRef<HTMLDivElement, DialogContentProps>(\r\n ({ className, children, size, ...props }, ref) => {\r\n const slots = dialogVariants({ size });\r\n return (\r\n <BaseDialog.Portal>\r\n <BaseDialog.Backdrop className={slots.overlay()} />\r\n <BaseDialog.Popup ref={ref} className={slots.content({ className })} {...props}>\r\n {children}\r\n <BaseDialog.Close className={slots.close()}>\r\n <X className=\"h-4 w-4\" />\r\n <span className=\"sr-only\">Close</span>\r\n </BaseDialog.Close>\r\n </BaseDialog.Popup>\r\n </BaseDialog.Portal>\r\n );\r\n },\r\n);\r\nDialogContent.displayName = 'DialogContent';\r\n\r\n/* ─── Header ─── */\r\nconst DialogHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\r\n ({ className, ...props }, ref) => {\r\n const slots = dialogVariants();\r\n return <div ref={ref} className={slots.header({ className })} {...props} />;\r\n },\r\n);\r\nDialogHeader.displayName = 'DialogHeader';\r\n\r\n/* ─── Footer ─── */\r\nconst DialogFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\r\n ({ className, ...props }, ref) => {\r\n const slots = dialogVariants();\r\n return <div ref={ref} className={slots.footer({ className })} {...props} />;\r\n },\r\n);\r\nDialogFooter.displayName = 'DialogFooter';\r\n\r\n/* ─── Title ─── */\r\nconst DialogTitle = React.forwardRef<\r\n HTMLHeadingElement,\r\n Omit<React.ComponentPropsWithoutRef<typeof BaseDialog.Title>, 'className'> & { className?: string }\r\n>(({ className, ...props }, ref) => {\r\n const slots = dialogVariants();\r\n return <BaseDialog.Title ref={ref} className={slots.title({ className })} {...props} />;\r\n});\r\nDialogTitle.displayName = 'DialogTitle';\r\n\r\n/* ─── Description ─── */\r\nconst DialogDescription = React.forwardRef<\r\n HTMLParagraphElement,\r\n Omit<React.ComponentPropsWithoutRef<typeof BaseDialog.Description>, 'className'> & { className?: string }\r\n>(({ className, ...props }, ref) => {\r\n const slots = dialogVariants();\r\n return (\r\n <BaseDialog.Description ref={ref} className={slots.description({ className })} {...props} />\r\n );\r\n});\r\nDialogDescription.displayName = 'DialogDescription';\r\n\r\nexport {\r\n Dialog,\r\n DialogTrigger,\r\n DialogContent,\r\n DialogHeader,\r\n DialogFooter,\r\n DialogTitle,\r\n DialogDescription,\r\n DialogClose,\r\n};\r\n"
383
332
  }
384
333
  ]
385
334
  },
@@ -513,7 +462,7 @@
513
462
  "files": [
514
463
  {
515
464
  "path": "src/components/ui/number-input/NumberInput.tsx",
516
- "content": "import * as React from 'react';\r\nimport { NumberField } from '@base-ui/react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\nimport { cn } from '@/lib/utils/cn';\r\nimport { Minus, Plus } from 'lucide-react';\r\n\r\n// ─── Variants ────────────────────────────────────────────────────────────────\r\n\r\nconst numberInputVariants = tv({\r\n slots: {\r\n root: 'flex flex-col gap-1.5',\r\n group: 'inline-flex items-center border border-border rounded-lg bg-background overflow-hidden transition-colors focus-within:border-primary focus-within:ring-2 focus-within:ring-primary/20',\r\n input: [\r\n 'h-full bg-transparent text-center text-sm font-medium text-foreground outline-none',\r\n 'placeholder:text-muted-foreground',\r\n '[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none',\r\n ].join(' '),\r\n button: [\r\n 'inline-flex items-center justify-center shrink-0 border-0 bg-transparent text-muted-foreground',\r\n 'transition-colors hover:bg-muted hover:text-foreground',\r\n 'disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-transparent',\r\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-inset',\r\n ].join(' '),\r\n label: 'text-sm font-medium text-foreground leading-none',\r\n description: 'text-[0.8rem] text-muted-foreground',\r\n error: 'text-[0.8rem] font-medium text-danger',\r\n },\r\n variants: {\r\n size: {\r\n sm: {\r\n group: 'h-8',\r\n input: 'w-12 text-xs',\r\n button: 'w-8 h-8',\r\n },\r\n md: {\r\n group: 'h-10',\r\n input: 'w-14 text-sm',\r\n button: 'w-10 h-10',\r\n },\r\n lg: {\r\n group: 'h-12',\r\n input: 'w-16 text-base',\r\n button: 'w-12 h-12',\r\n },\r\n },\r\n isError: {\r\n true: { group: 'border-danger focus-within:border-danger focus-within:ring-danger/20' },\r\n },\r\n disabled: {\r\n true: { group: 'opacity-50 cursor-not-allowed' },\r\n },\r\n },\r\n defaultVariants: {\r\n size: 'md',\r\n },\r\n});\r\n\r\n// ─── Types ───────────────────────────────────────────────────────────────────\r\n\r\nexport interface NumberInputProps extends VariantProps<typeof numberInputVariants> {\r\n value?: number | null;\r\n defaultValue?: number;\r\n onChange?: (value: number | null) => void;\r\n min?: number;\r\n max?: number;\r\n step?: number;\r\n disabled?: boolean;\r\n label?: string;\r\n description?: string;\r\n error?: string;\r\n placeholder?: string;\r\n className?: string;\r\n}\r\n\r\n// ─── Component ───────────────────────────────────────────────────────────────\r\n\r\nconst NumberInput = React.forwardRef<HTMLDivElement, NumberInputProps>(\r\n (\r\n {\r\n value,\r\n defaultValue = 0,\r\n onChange,\r\n min,\r\n max,\r\n step = 1,\r\n disabled = false,\r\n label,\r\n description,\r\n error,\r\n placeholder = '0',\r\n size = 'md',\r\n className,\r\n },\r\n ref,\r\n ) => {\r\n const styles = numberInputVariants({ size, isError: !!error, disabled });\r\n const rootId = React.useId();\r\n\r\n return (\r\n <NumberField.Root\r\n ref={ref}\r\n value={value ?? undefined}\r\n defaultValue={defaultValue}\r\n onValueChange={(val) => onChange?.(val)}\r\n min={min}\r\n max={max}\r\n step={step}\r\n disabled={disabled}\r\n className={cn(styles.root(), className)}\r\n >\r\n {label && (\r\n <label htmlFor={rootId} className={styles.label()}>\r\n {label}\r\n </label>\r\n )}\r\n\r\n <NumberField.Group className={styles.group()}>\r\n <NumberField.Decrement\r\n className={cn(styles.button(), 'border-r border-border')}\r\n aria-label=\"Decrease\"\r\n >\r\n <Minus className=\"h-3.5 w-3.5\" />\r\n </NumberField.Decrement>\r\n\r\n <NumberField.Input\r\n id={rootId}\r\n placeholder={placeholder}\r\n className={styles.input()}\r\n />\r\n\r\n <NumberField.Increment\r\n className={cn(styles.button(), 'border-l border-border')}\r\n aria-label=\"Increase\"\r\n >\r\n <Plus className=\"h-3.5 w-3.5\" />\r\n </NumberField.Increment>\r\n </NumberField.Group>\r\n\r\n {description && !error && (\r\n <p className={styles.description()}>{description}</p>\r\n )}\r\n {error && (\r\n <p className={styles.error()}>{error}</p>\r\n )}\r\n </NumberField.Root>\r\n );\r\n },\r\n);\r\n\r\nNumberInput.displayName = 'NumberInput';\r\n\r\nexport { NumberInput, numberInputVariants };\r\n"
465
+ "content": "import * as React from 'react';\r\nimport { NumberField } from '@base-ui/react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\nimport { cn } from '@/lib/utils/cn';\r\nimport { Minus, Plus } from 'lucide-react';\r\n\r\n// ─── Variants ────────────────────────────────────────────────────────────────\r\n\r\nconst numberInputVariants = tv({\r\n slots: {\r\n root: 'flex flex-col gap-1.5',\r\n group: 'inline-flex self-start items-center border border-border rounded-lg bg-background overflow-hidden transition-colors focus-within:border-primary focus-within:ring-2 focus-within:ring-primary/20',\r\n input: [\r\n 'h-full bg-transparent text-center text-sm font-medium text-foreground outline-none',\r\n 'placeholder:text-muted-foreground',\r\n '[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none',\r\n ].join(' '),\r\n button: [\r\n 'inline-flex items-center justify-center shrink-0 border-0 bg-transparent text-muted-foreground',\r\n 'transition-colors hover:bg-muted hover:text-foreground',\r\n 'disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-transparent',\r\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-inset',\r\n ].join(' '),\r\n label: 'text-sm font-medium text-foreground leading-none',\r\n description: 'text-[0.8rem] text-muted-foreground',\r\n error: 'text-[0.8rem] font-medium text-danger',\r\n },\r\n variants: {\r\n size: {\r\n sm: {\r\n group: 'h-8',\r\n input: 'w-12 text-xs',\r\n button: 'w-8 h-8',\r\n },\r\n md: {\r\n group: 'h-10',\r\n input: 'w-14 text-sm',\r\n button: 'w-10 h-10',\r\n },\r\n lg: {\r\n group: 'h-12',\r\n input: 'w-16 text-base',\r\n button: 'w-12 h-12',\r\n },\r\n },\r\n isError: {\r\n true: { group: 'border-danger focus-within:border-danger focus-within:ring-danger/20' },\r\n },\r\n disabled: {\r\n true: { group: 'opacity-50 cursor-not-allowed' },\r\n },\r\n },\r\n defaultVariants: {\r\n size: 'md',\r\n },\r\n});\r\n\r\n// ─── Types ───────────────────────────────────────────────────────────────────\r\n\r\nexport interface NumberInputProps extends VariantProps<typeof numberInputVariants> {\r\n value?: number | null;\r\n defaultValue?: number;\r\n onChange?: (value: number | null) => void;\r\n min?: number;\r\n max?: number;\r\n step?: number;\r\n disabled?: boolean;\r\n label?: string;\r\n description?: string;\r\n error?: string;\r\n placeholder?: string;\r\n className?: string;\r\n}\r\n\r\n// ─── Component ───────────────────────────────────────────────────────────────\r\n\r\nconst NumberInput = React.forwardRef<HTMLDivElement, NumberInputProps>(\r\n (\r\n {\r\n value,\r\n defaultValue = 0,\r\n onChange,\r\n min,\r\n max,\r\n step = 1,\r\n disabled = false,\r\n label,\r\n description,\r\n error,\r\n placeholder = '0',\r\n size = 'md',\r\n className,\r\n },\r\n ref,\r\n ) => {\r\n const styles = numberInputVariants({ size, isError: !!error, disabled });\r\n const rootId = React.useId();\r\n\r\n return (\r\n <NumberField.Root\r\n ref={ref}\r\n value={value ?? undefined}\r\n defaultValue={defaultValue}\r\n onValueChange={(val) => onChange?.(val)}\r\n min={min}\r\n max={max}\r\n step={step}\r\n disabled={disabled}\r\n className={cn(styles.root(), className)}\r\n >\r\n {label && (\r\n <label htmlFor={rootId} className={styles.label()}>\r\n {label}\r\n </label>\r\n )}\r\n\r\n <NumberField.Group className={styles.group()}>\r\n <NumberField.Decrement\r\n className={cn(styles.button(), 'border-r border-border')}\r\n aria-label=\"Decrease\"\r\n >\r\n <Minus className=\"h-3.5 w-3.5\" />\r\n </NumberField.Decrement>\r\n\r\n <NumberField.Input\r\n id={rootId}\r\n placeholder={placeholder}\r\n className={styles.input()}\r\n />\r\n\r\n <NumberField.Increment\r\n className={cn(styles.button(), 'border-l border-border')}\r\n aria-label=\"Increase\"\r\n >\r\n <Plus className=\"h-3.5 w-3.5\" />\r\n </NumberField.Increment>\r\n </NumberField.Group>\r\n\r\n {description && !error && (\r\n <p className={styles.description()}>{description}</p>\r\n )}\r\n {error && (\r\n <p className={styles.error()}>{error}</p>\r\n )}\r\n </NumberField.Root>\r\n );\r\n },\r\n);\r\n\r\nNumberInput.displayName = 'NumberInput';\r\n\r\nexport { NumberInput };\r\n"
517
466
  }
518
467
  ]
519
468
  },
@@ -808,15 +757,15 @@
808
757
  "files": [
809
758
  {
810
759
  "path": "src/components/ui/table/Table.tsx",
811
- "content": "import React, { useEffect, useRef, useState } from 'react';\r\nimport {\r\n useReactTable,\r\n getCoreRowModel,\r\n getSortedRowModel,\r\n getPaginationRowModel,\r\n getFilteredRowModel,\r\n getExpandedRowModel,\r\n type ColumnDef,\r\n type SortingState,\r\n type PaginationState,\r\n type RowSelectionState,\r\n type RowData,\r\n type ColumnResizeMode,\r\n} from '@tanstack/react-table';\r\nimport { useVirtualizer } from '@tanstack/react-virtual';\r\n\r\ndeclare module '@tanstack/react-table' {\r\n interface ColumnMeta<TData extends RowData, TValue> {\r\n align?: 'left' | 'center' | 'right';\r\n }\r\n}\r\nimport { cn } from '@lib/utils/cn';\r\nimport { ChevronDown, ChevronRight } from 'lucide-react';\r\nimport { Checkbox } from '../checkbox/Checkbox';\r\nimport { Spinner } from '../spinner/Spinner';\r\nimport { TableHeader } from './TableHeader';\r\nimport { TableEmpty, TableNormalRows, TableVirtualRows } from './TableBody';\r\nimport { TablePagination } from './TablePagination';\r\n\r\n// ─── Pagination Config ───────────────────────────────────────────────────────\r\n\r\nexport interface PaginationConfig {\r\n current?: number;\r\n pageSize?: number;\r\n total?: number;\r\n pageSizeOptions?: number[];\r\n showTotal?: (total: number, range: [number, number]) => React.ReactNode;\r\n showSizeChanger?: boolean;\r\n onChange?: (page: number, pageSize: number) => void;\r\n}\r\n\r\nexport interface TableLabels {\r\n page?: string;\r\n perPage?: string;\r\n empty?: string;\r\n}\r\n\r\nexport interface TableProps<TData, TValue = unknown> {\r\n data: TData[];\r\n columns: ColumnDef<TData, TValue>[];\r\n isLoading?: boolean;\r\n enableSorting?: boolean;\r\n enableRowSelection?: boolean;\r\n enableExpanding?: boolean;\r\n renderSubComponent?: (props: { row: TData }) => React.ReactNode;\r\n getRowCanExpand?: (row: TData) => boolean;\r\n onSelectionChange?: (selectedRows: TData[]) => void;\r\n className?: string;\r\n enableColumnResizing?: boolean;\r\n columnResizeMode?: ColumnResizeMode;\r\n pagination?: PaginationConfig | false;\r\n /** @deprecated Use `labels.empty` instead */\r\n emptyText?: string;\r\n labels?: TableLabels;\r\n virtualize?: boolean;\r\n virtualHeight?: number;\r\n estimatedRowHeight?: number;\r\n}\r\n\r\nexport function Table<TData, TValue = unknown>({\r\n data = [],\r\n columns,\r\n isLoading = false,\r\n enableSorting = true,\r\n enableRowSelection = false,\r\n enableExpanding = false,\r\n renderSubComponent,\r\n getRowCanExpand,\r\n onSelectionChange,\r\n className,\r\n enableColumnResizing = false,\r\n columnResizeMode = 'onChange',\r\n pagination: paginationProp = {},\r\n emptyText,\r\n labels,\r\n virtualize = false,\r\n virtualHeight = 400,\r\n estimatedRowHeight = 45,\r\n}: TableProps<TData>) {\r\n const resolvedEmptyText = emptyText ?? labels?.empty ?? 'No data';\r\n const scrollContainerRef = useRef<HTMLDivElement>(null);\r\n const paginationEnabled = paginationProp !== false;\r\n const cfg = paginationEnabled ? (paginationProp as PaginationConfig) : {};\r\n\r\n const isServerMode = paginationEnabled && cfg.total !== undefined;\r\n const [page, setPage] = useState(cfg.current ?? 1);\r\n const [pageSize, setPageSize] = useState(cfg.pageSize ?? 10);\r\n\r\n useEffect(() => {\r\n if (cfg.current !== undefined) setPage(cfg.current);\r\n }, [cfg.current]);\r\n\r\n useEffect(() => {\r\n if (cfg.pageSize !== undefined) setPageSize(cfg.pageSize);\r\n }, [cfg.pageSize]);\r\n\r\n const pageIndex = page - 1;\r\n const totalRows = isServerMode ? cfg.total! : data.length;\r\n const pageCount = isServerMode ? Math.ceil(totalRows / pageSize) : undefined;\r\n const tanstackPagination: PaginationState = { pageIndex, pageSize };\r\n\r\n const handlePaginationChange = (updater: PaginationState | ((prev: PaginationState) => PaginationState)) => {\r\n const next = typeof updater === 'function' ? updater(tanstackPagination) : updater;\r\n const newPage = next.pageIndex + 1;\r\n const newPageSize = next.pageSize;\r\n\r\n if (!isServerMode) {\r\n setPage(newPage);\r\n setPageSize(newPageSize);\r\n }\r\n cfg.onChange?.(newPage, newPageSize);\r\n };\r\n\r\n const [sorting, setSorting] = useState<SortingState>([]);\r\n const [rowSelection, setRowSelection] = useState<RowSelectionState>({});\r\n\r\n const finalColumns = React.useMemo(() => {\r\n const cols = [...columns];\r\n if (enableRowSelection) {\r\n cols.unshift({\r\n id: 'select',\r\n size: 10,\r\n minSize: 5,\r\n maxSize: 10,\r\n meta: { align: 'center' },\r\n header: ({ table }) => (\r\n <div className=\"flex items-center justify-center\">\r\n <Checkbox\r\n size=\"sm\"\r\n checked={table.getIsAllRowsSelected()}\r\n indeterminate={table.getIsSomeRowsSelected()}\r\n onCheckedChange={(checked) => table.toggleAllRowsSelected(!!checked)}\r\n />\r\n </div>\r\n ),\r\n cell: ({ row }) => (\r\n <div className=\"flex items-center justify-center\">\r\n <Checkbox\r\n size=\"sm\"\r\n checked={row.getIsSelected()}\r\n disabled={!row.getCanSelect()}\r\n onCheckedChange={(checked) => row.toggleSelected(!!checked)}\r\n />\r\n </div>\r\n ),\r\n enableSorting: false,\r\n });\r\n }\r\n if (enableExpanding) {\r\n cols.unshift({\r\n id: 'expander',\r\n header: () => null,\r\n size: 10,\r\n minSize: 10,\r\n maxSize: 10,\r\n meta: { align: 'center' },\r\n cell: ({ row }) => row.getCanExpand() ? (\r\n <div className=\"flex items-center justify-center\">\r\n <span\r\n onClick={row.getToggleExpandedHandler()}\r\n className=\"hover:bg-muted text-muted-foreground transition-colors cursor-pointer outline-none focus:ring-2 focus:ring-primary/50 p-1 rounded-md border border-border\"\r\n >\r\n {row.getIsExpanded() ? <ChevronDown className=\"w-3 h-3\" /> : <ChevronRight className=\"w-3 h-3\" />}\r\n </span>\r\n </div>\r\n ) : null,\r\n enableSorting: false,\r\n });\r\n }\r\n return cols;\r\n }, [columns, enableRowSelection, enableExpanding]);\r\n\r\n const table = useReactTable({\r\n data,\r\n columns: finalColumns,\r\n state: { sorting, rowSelection, pagination: tanstackPagination },\r\n onSortingChange: setSorting,\r\n onRowSelectionChange: setRowSelection,\r\n onPaginationChange: handlePaginationChange,\r\n getCoreRowModel: getCoreRowModel(),\r\n getSortedRowModel: enableSorting ? getSortedRowModel() : undefined,\r\n getPaginationRowModel: paginationEnabled ? getPaginationRowModel() : undefined,\r\n getFilteredRowModel: getFilteredRowModel(),\r\n getExpandedRowModel: getExpandedRowModel(),\r\n columnResizeMode,\r\n enableColumnResizing,\r\n enableRowSelection,\r\n manualPagination: isServerMode,\r\n pageCount: isServerMode ? pageCount : undefined,\r\n getRowCanExpand: getRowCanExpand ? (row) => getRowCanExpand(row.original) : () => !!renderSubComponent,\r\n });\r\n\r\n useEffect(() => {\r\n if (onSelectionChange) {\r\n const selected = table.getSelectedRowModel().rows.map(row => row.original);\r\n onSelectionChange(selected);\r\n }\r\n }, [rowSelection, onSelectionChange, table]);\r\n\r\n const totalPageCount = isServerMode ? (pageCount ?? 1) : (table.getPageCount() || 1);\r\n\r\n // Virtualizer\r\n const allRows = virtualize ? table.getCoreRowModel().rows : [];\r\n const rowVirtualizer = useVirtualizer({\r\n count: virtualize ? allRows.length : 0,\r\n getScrollElement: () => scrollContainerRef.current,\r\n estimateSize: () => estimatedRowHeight,\r\n overscan: 8,\r\n enabled: virtualize,\r\n });\r\n\r\n return (\r\n <div className={cn(\"relative w-full rounded-md border border-border bg-background flex flex-col overflow-hidden\", className)}>\r\n {isLoading && (\r\n <div className=\"absolute inset-0 bg-white/60 z-10 flex items-center justify-center backdrop-blur-[0.5px]\">\r\n <Spinner size=\"lg\" variant=\"primary\" />\r\n </div>\r\n )}\r\n\r\n <div\r\n ref={scrollContainerRef}\r\n className=\"overflow-x-auto w-full\"\r\n style={virtualize ? { overflowY: 'auto', maxHeight: virtualHeight } : undefined}\r\n >\r\n <table\r\n className=\"w-full text-sm text-left text-foreground whitespace-nowrap\"\r\n style={{\r\n width: enableColumnResizing ? table.getCenterTotalSize() : undefined,\r\n tableLayout: enableColumnResizing ? 'fixed' : 'auto'\r\n }}\r\n >\r\n <TableHeader\r\n headerGroups={table.getHeaderGroups()}\r\n enableSorting={enableSorting}\r\n enableColumnResizing={enableColumnResizing}\r\n />\r\n <tbody>\r\n {!isLoading && data.length === 0 ? (\r\n <TableEmpty colSpan={finalColumns.length} text={resolvedEmptyText} />\r\n ) : virtualize ? (\r\n <TableVirtualRows\r\n allRows={allRows}\r\n rowVirtualizer={rowVirtualizer}\r\n enableColumnResizing={enableColumnResizing}\r\n colSpan={finalColumns.length}\r\n />\r\n ) : (\r\n <TableNormalRows\r\n rows={table.getRowModel().rows}\r\n enableColumnResizing={enableColumnResizing}\r\n renderSubComponent={renderSubComponent}\r\n />\r\n )}\r\n </tbody>\r\n </table>\r\n </div>\r\n\r\n {!virtualize && paginationEnabled && (\r\n <TablePagination\r\n table={table}\r\n totalRows={totalRows}\r\n totalPageCount={totalPageCount}\r\n isServerMode={isServerMode}\r\n cfg={cfg}\r\n labels={labels}\r\n />\r\n )}\r\n </div>\r\n );\r\n}\r\n"
760
+ "content": "import React, { useEffect, useRef, useState } from 'react';\r\nimport {\r\n useReactTable,\r\n getCoreRowModel,\r\n getSortedRowModel,\r\n getPaginationRowModel,\r\n getFilteredRowModel,\r\n getExpandedRowModel,\r\n type ColumnDef,\r\n type SortingState,\r\n type PaginationState,\r\n type RowSelectionState,\r\n type RowData,\r\n type ColumnResizeMode,\r\n} from '@tanstack/react-table';\r\nimport { useVirtualizer } from '@tanstack/react-virtual';\r\n\r\ndeclare module '@tanstack/react-table' {\r\n interface ColumnMeta<TData extends RowData, TValue> {\r\n align?: 'left' | 'center' | 'right';\r\n }\r\n}\r\nimport { ChevronDown, ChevronRight } from 'lucide-react';\r\nimport { Checkbox } from '../checkbox/Checkbox';\r\nimport { Spinner } from '../spinner/Spinner';\r\nimport { TableHeader } from './TableHeader';\r\nimport { TableEmpty, TableNormalRows, TableVirtualRows } from './TableBody';\r\nimport { TablePagination } from './TablePagination';\r\nimport { cn } from '@/lib/utils/cn';\r\n\r\n// ─── Pagination Config ───────────────────────────────────────────────────────\r\n\r\nexport interface PaginationConfig {\r\n current?: number;\r\n pageSize?: number;\r\n total?: number;\r\n pageSizeOptions?: number[];\r\n showTotal?: (total: number, range: [number, number]) => React.ReactNode;\r\n showSizeChanger?: boolean;\r\n onChange?: (page: number, pageSize: number) => void;\r\n}\r\n\r\nexport interface TableLabels {\r\n page?: string;\r\n perPage?: string;\r\n empty?: string;\r\n}\r\n\r\nexport interface TableProps<TData, TValue = unknown> {\r\n data: TData[];\r\n columns: ColumnDef<TData, TValue>[];\r\n isLoading?: boolean;\r\n enableSorting?: boolean;\r\n enableRowSelection?: boolean;\r\n enableExpanding?: boolean;\r\n renderSubComponent?: (props: { row: TData }) => React.ReactNode;\r\n getRowCanExpand?: (row: TData) => boolean;\r\n onSelectionChange?: (selectedRows: TData[]) => void;\r\n className?: string;\r\n enableColumnResizing?: boolean;\r\n columnResizeMode?: ColumnResizeMode;\r\n pagination?: PaginationConfig | false;\r\n /** @deprecated Use `labels.empty` instead */\r\n emptyText?: string;\r\n labels?: TableLabels;\r\n virtualize?: boolean;\r\n virtualHeight?: number;\r\n estimatedRowHeight?: number;\r\n}\r\n\r\nexport function Table<TData, TValue = unknown>({\r\n data = [],\r\n columns,\r\n isLoading = false,\r\n enableSorting = true,\r\n enableRowSelection = false,\r\n enableExpanding = false,\r\n renderSubComponent,\r\n getRowCanExpand,\r\n onSelectionChange,\r\n className,\r\n enableColumnResizing = false,\r\n columnResizeMode = 'onChange',\r\n pagination: paginationProp = {},\r\n emptyText,\r\n labels,\r\n virtualize = false,\r\n virtualHeight = 400,\r\n estimatedRowHeight = 45,\r\n}: TableProps<TData>) {\r\n const resolvedEmptyText = emptyText ?? labels?.empty ?? 'No data';\r\n const scrollContainerRef = useRef<HTMLDivElement>(null);\r\n const paginationEnabled = paginationProp !== false;\r\n const cfg = paginationEnabled ? (paginationProp as PaginationConfig) : {};\r\n\r\n const isServerMode = paginationEnabled && cfg.total !== undefined;\r\n const [page, setPage] = useState(cfg.current ?? 1);\r\n const [pageSize, setPageSize] = useState(cfg.pageSize ?? 10);\r\n\r\n useEffect(() => {\r\n if (cfg.current !== undefined) setPage(cfg.current);\r\n }, [cfg.current]);\r\n\r\n useEffect(() => {\r\n if (cfg.pageSize !== undefined) setPageSize(cfg.pageSize);\r\n }, [cfg.pageSize]);\r\n\r\n const pageIndex = page - 1;\r\n const totalRows = isServerMode ? cfg.total! : data.length;\r\n const pageCount = isServerMode ? Math.ceil(totalRows / pageSize) : undefined;\r\n const tanstackPagination: PaginationState = { pageIndex, pageSize };\r\n\r\n const handlePaginationChange = (updater: PaginationState | ((prev: PaginationState) => PaginationState)) => {\r\n const next = typeof updater === 'function' ? updater(tanstackPagination) : updater;\r\n const newPage = next.pageIndex + 1;\r\n const newPageSize = next.pageSize;\r\n\r\n if (!isServerMode) {\r\n setPage(newPage);\r\n setPageSize(newPageSize);\r\n }\r\n cfg.onChange?.(newPage, newPageSize);\r\n };\r\n\r\n const [sorting, setSorting] = useState<SortingState>([]);\r\n const [rowSelection, setRowSelection] = useState<RowSelectionState>({});\r\n\r\n const finalColumns = React.useMemo(() => {\r\n const cols = [...columns];\r\n if (enableRowSelection) {\r\n cols.unshift({\r\n id: 'select',\r\n size: 10,\r\n minSize: 5,\r\n maxSize: 10,\r\n meta: { align: 'center' },\r\n header: ({ table }) => (\r\n <div className=\"flex items-center justify-center\">\r\n <Checkbox\r\n size=\"sm\"\r\n checked={table.getIsAllRowsSelected()}\r\n indeterminate={table.getIsSomeRowsSelected()}\r\n onCheckedChange={(checked) => table.toggleAllRowsSelected(!!checked)}\r\n />\r\n </div>\r\n ),\r\n cell: ({ row }) => (\r\n <div className=\"flex items-center justify-center\">\r\n <Checkbox\r\n size=\"sm\"\r\n checked={row.getIsSelected()}\r\n disabled={!row.getCanSelect()}\r\n onCheckedChange={(checked) => row.toggleSelected(!!checked)}\r\n />\r\n </div>\r\n ),\r\n enableSorting: false,\r\n });\r\n }\r\n if (enableExpanding) {\r\n cols.unshift({\r\n id: 'expander',\r\n header: () => null,\r\n size: 10,\r\n minSize: 10,\r\n maxSize: 10,\r\n meta: { align: 'center' },\r\n cell: ({ row }) => row.getCanExpand() ? (\r\n <div className=\"flex items-center justify-center\">\r\n <span\r\n onClick={row.getToggleExpandedHandler()}\r\n className=\"hover:bg-muted text-muted-foreground transition-colors cursor-pointer outline-none focus:ring-2 focus:ring-primary/50 p-1 rounded-md border border-border\"\r\n >\r\n {row.getIsExpanded() ? <ChevronDown className=\"w-3 h-3\" /> : <ChevronRight className=\"w-3 h-3\" />}\r\n </span>\r\n </div>\r\n ) : null,\r\n enableSorting: false,\r\n });\r\n }\r\n return cols;\r\n }, [columns, enableRowSelection, enableExpanding]);\r\n\r\n const table = useReactTable({\r\n data,\r\n columns: finalColumns,\r\n state: { sorting, rowSelection, pagination: tanstackPagination },\r\n onSortingChange: setSorting,\r\n onRowSelectionChange: setRowSelection,\r\n onPaginationChange: handlePaginationChange,\r\n getCoreRowModel: getCoreRowModel(),\r\n getSortedRowModel: enableSorting ? getSortedRowModel() : undefined,\r\n getPaginationRowModel: paginationEnabled ? getPaginationRowModel() : undefined,\r\n getFilteredRowModel: getFilteredRowModel(),\r\n getExpandedRowModel: getExpandedRowModel(),\r\n columnResizeMode,\r\n enableColumnResizing,\r\n enableRowSelection,\r\n manualPagination: isServerMode,\r\n pageCount: isServerMode ? pageCount : undefined,\r\n getRowCanExpand: getRowCanExpand ? (row) => getRowCanExpand(row.original) : () => !!renderSubComponent,\r\n });\r\n\r\n useEffect(() => {\r\n if (onSelectionChange) {\r\n const selected = table.getSelectedRowModel().rows.map(row => row.original);\r\n onSelectionChange(selected);\r\n }\r\n }, [rowSelection, onSelectionChange, table]);\r\n\r\n const totalPageCount = isServerMode ? (pageCount ?? 1) : (table.getPageCount() || 1);\r\n\r\n // Virtualizer\r\n const allRows = virtualize ? table.getCoreRowModel().rows : [];\r\n const rowVirtualizer = useVirtualizer({\r\n count: virtualize ? allRows.length : 0,\r\n getScrollElement: () => scrollContainerRef.current,\r\n estimateSize: () => estimatedRowHeight,\r\n overscan: 8,\r\n enabled: virtualize,\r\n });\r\n\r\n return (\r\n <div className={cn(\"relative w-full rounded-md border border-border bg-background flex flex-col overflow-hidden\", className)}>\r\n {isLoading && (\r\n <div className=\"absolute inset-0 bg-white/60 z-10 flex items-center justify-center backdrop-blur-[0.5px]\">\r\n <Spinner size=\"lg\" variant=\"primary\" />\r\n </div>\r\n )}\r\n\r\n <div\r\n ref={scrollContainerRef}\r\n className=\"overflow-x-auto w-full\"\r\n style={virtualize ? { overflowY: 'auto', maxHeight: virtualHeight } : undefined}\r\n >\r\n <table\r\n className=\"w-full text-sm text-left text-foreground whitespace-nowrap\"\r\n style={{\r\n width: enableColumnResizing ? table.getCenterTotalSize() : undefined,\r\n tableLayout: enableColumnResizing ? 'fixed' : 'auto'\r\n }}\r\n >\r\n <TableHeader\r\n headerGroups={table.getHeaderGroups()}\r\n enableSorting={enableSorting}\r\n enableColumnResizing={enableColumnResizing}\r\n />\r\n <tbody>\r\n {!isLoading && data.length === 0 ? (\r\n <TableEmpty colSpan={finalColumns.length} text={resolvedEmptyText} />\r\n ) : virtualize ? (\r\n <TableVirtualRows\r\n allRows={allRows}\r\n rowVirtualizer={rowVirtualizer}\r\n enableColumnResizing={enableColumnResizing}\r\n colSpan={finalColumns.length}\r\n />\r\n ) : (\r\n <TableNormalRows\r\n rows={table.getRowModel().rows}\r\n enableColumnResizing={enableColumnResizing}\r\n renderSubComponent={renderSubComponent}\r\n />\r\n )}\r\n </tbody>\r\n </table>\r\n </div>\r\n\r\n {!virtualize && paginationEnabled && (\r\n <TablePagination\r\n table={table}\r\n totalRows={totalRows}\r\n totalPageCount={totalPageCount}\r\n isServerMode={isServerMode}\r\n cfg={cfg}\r\n labels={labels}\r\n />\r\n )}\r\n </div>\r\n );\r\n}\r\n"
812
761
  },
813
762
  {
814
763
  "path": "src/components/ui/table/TableBody.tsx",
815
- "content": "import React from 'react';\r\nimport {\r\n flexRender,\r\n type Row,\r\n type ColumnDef,\r\n type RowData,\r\n} from '@tanstack/react-table';\r\nimport { useVirtualizer, type Virtualizer } from '@tanstack/react-virtual';\r\nimport { cn } from '@lib/utils/cn';\r\n\r\n// ─── Empty State ────────────────────────────────────────────────────────────\r\n\r\ninterface TableEmptyProps {\r\n colSpan: number;\r\n text: string;\r\n}\r\n\r\nexport function TableEmpty({ colSpan, text }: TableEmptyProps) {\r\n return (\r\n <tr>\r\n <td colSpan={colSpan} className=\"px-4 py-16 text-center text-muted-foreground\">\r\n <div className=\"flex flex-col items-center justify-center space-y-2\">\r\n <span className=\"text-muted-foreground/50\">\r\n <svg className=\"w-12 h-12\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\r\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={1} d=\"M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4\" />\r\n </svg>\r\n </span>\r\n <span>{text}</span>\r\n </div>\r\n </td>\r\n </tr>\r\n );\r\n}\r\n\r\n// ─── Row Cell Renderer ──────────────────────────────────────────────────────\r\n\r\ninterface TableRowCellsProps<TData extends RowData> {\r\n row: Row<TData>;\r\n enableColumnResizing: boolean;\r\n}\r\n\r\nfunction TableRowCells<TData extends RowData>({ row, enableColumnResizing }: TableRowCellsProps<TData>) {\r\n return (\r\n <>\r\n {row.getVisibleCells().map(cell => {\r\n const meta = cell.column.columnDef.meta;\r\n const align = meta?.align || 'left';\r\n return (\r\n <td\r\n key={cell.id}\r\n style={{ width: enableColumnResizing ? cell.column.getSize() : cell.column.columnDef.size }}\r\n className={cn(\r\n cell.column.id === 'select' || cell.column.id === 'expander' ? \"px-1\" : \"px-2\",\r\n \"py-3 border border-border align-middle\",\r\n align === 'center' ? \"text-center\" : align === 'right' ? \"text-right\" : \"text-left\"\r\n )}\r\n >\r\n <div className={cn(\r\n \"flex items-center\",\r\n align === 'center' ? \"justify-center\" : align === 'right' ? \"justify-end\" : \"justify-start\"\r\n )}>\r\n {flexRender(cell.column.columnDef.cell, cell.getContext())}\r\n </div>\r\n </td>\r\n );\r\n })}\r\n </>\r\n );\r\n}\r\n\r\n// ─── Normal Rows ────────────────────────────────────────────────────────────\r\n\r\ninterface TableNormalRowsProps<TData extends RowData> {\r\n rows: Row<TData>[];\r\n enableColumnResizing: boolean;\r\n renderSubComponent?: (props: { row: TData }) => React.ReactNode;\r\n}\r\n\r\nexport function TableNormalRows<TData extends RowData>({\r\n rows,\r\n enableColumnResizing,\r\n renderSubComponent,\r\n}: TableNormalRowsProps<TData>) {\r\n return (\r\n <>\r\n {rows.map(row => (\r\n <React.Fragment key={row.id}>\r\n <tr\r\n className={cn(\r\n \"hover:bg-muted/50 transition-colors group\",\r\n row.getIsSelected() ? \"bg-primary/5 hover:bg-primary/10\" : \"\",\r\n row.getIsExpanded() ? \"bg-primary/5\" : \"\"\r\n )}\r\n >\r\n <TableRowCells row={row} enableColumnResizing={enableColumnResizing} />\r\n </tr>\r\n {row.getIsExpanded() && renderSubComponent && (\r\n <tr>\r\n <td colSpan={row.getVisibleCells().length} className=\"p-0 border-b border-border whitespace-normal\">\r\n <div className=\"bg-muted/50 px-4 py-5 shadow-inner w-full border-l-4 border-l-primary/40 wrap-break-word\">\r\n {renderSubComponent({ row: row.original })}\r\n </div>\r\n </td>\r\n </tr>\r\n )}\r\n </React.Fragment>\r\n ))}\r\n </>\r\n );\r\n}\r\n\r\n// ─── Virtual Rows ───────────────────────────────────────────────────────────\r\n\r\ninterface TableVirtualRowsProps<TData extends RowData> {\r\n allRows: Row<TData>[];\r\n rowVirtualizer: Virtualizer<HTMLDivElement, Element>;\r\n enableColumnResizing: boolean;\r\n colSpan: number;\r\n}\r\n\r\nexport function TableVirtualRows<TData extends RowData>({\r\n allRows,\r\n rowVirtualizer,\r\n enableColumnResizing,\r\n colSpan,\r\n}: TableVirtualRowsProps<TData>) {\r\n const virtualItems = rowVirtualizer.getVirtualItems();\r\n\r\n return (\r\n <>\r\n {rowVirtualizer.getTotalSize() > 0 && (\r\n <tr aria-hidden=\"true\">\r\n <td style={{ height: virtualItems[0]?.start ?? 0, padding: 0, border: 0 }} colSpan={colSpan} />\r\n </tr>\r\n )}\r\n {virtualItems.map(virtualRow => {\r\n const row = allRows[virtualRow.index];\r\n if (!row) return null;\r\n return (\r\n <tr\r\n key={row.id}\r\n data-index={virtualRow.index}\r\n ref={rowVirtualizer.measureElement}\r\n className={cn(\r\n \"hover:bg-muted/50 transition-colors\",\r\n row.getIsSelected() ? \"bg-primary/5 hover:bg-primary/10\" : \"\"\r\n )}\r\n >\r\n <TableRowCells row={row} enableColumnResizing={enableColumnResizing} />\r\n </tr>\r\n );\r\n })}\r\n {rowVirtualizer.getTotalSize() > 0 && (\r\n <tr aria-hidden=\"true\">\r\n <td\r\n style={{\r\n height: rowVirtualizer.getTotalSize() - (virtualItems[virtualItems.length - 1]?.end ?? 0),\r\n padding: 0,\r\n border: 0,\r\n }}\r\n colSpan={colSpan}\r\n />\r\n </tr>\r\n )}\r\n </>\r\n );\r\n}\r\n"
764
+ "content": "import React from 'react';\r\nimport {\r\n flexRender,\r\n type Row,\r\n type ColumnDef,\r\n type RowData,\r\n} from '@tanstack/react-table';\r\nimport { useVirtualizer, type Virtualizer } from '@tanstack/react-virtual';\r\nimport { cn } from '@/lib/utils/cn';\r\n\r\n// ─── Empty State ────────────────────────────────────────────────────────────\r\n\r\ninterface TableEmptyProps {\r\n colSpan: number;\r\n text: string;\r\n}\r\n\r\nexport function TableEmpty({ colSpan, text }: TableEmptyProps) {\r\n return (\r\n <tr className=\"border-b border-border\">\r\n <td colSpan={colSpan} className=\"px-4 py-16 text-center text-muted-foreground\">\r\n <div className=\"flex flex-col items-center justify-center space-y-2\">\r\n <span className=\"text-muted-foreground/50\">\r\n <svg className=\"w-12 h-12\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\r\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={1} d=\"M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4\" />\r\n </svg>\r\n </span>\r\n <span>{text}</span>\r\n </div>\r\n </td>\r\n </tr>\r\n );\r\n}\r\n\r\n// ─── Row Cell Renderer ──────────────────────────────────────────────────────\r\n\r\ninterface TableRowCellsProps<TData extends RowData> {\r\n row: Row<TData>;\r\n enableColumnResizing: boolean;\r\n}\r\n\r\nfunction TableRowCells<TData extends RowData>({ row, enableColumnResizing }: TableRowCellsProps<TData>) {\r\n const visibleCells = row.getVisibleCells();\r\n return (\r\n <>\r\n {visibleCells.map((cell, index) => {\r\n const meta = cell.column.columnDef.meta;\r\n const align = meta?.align || 'left';\r\n const isLastColumn = index === visibleCells.length - 1;\r\n return (\r\n <td\r\n key={cell.id}\r\n style={{ width: enableColumnResizing ? cell.column.getSize() : cell.column.columnDef.size }}\r\n className={cn(\r\n cell.column.id === 'select' || cell.column.id === 'expander' ? \"px-1\" : \"px-2\",\r\n \"py-3 align-middle\",\r\n align === 'center' ? \"text-center\" : align === 'right' ? \"text-right\" : \"text-left\",\r\n !isLastColumn && \"border-r border-border\"\r\n )}\r\n >\r\n <div className={cn(\r\n \"flex items-center\",\r\n align === 'center' ? \"justify-center\" : align === 'right' ? \"justify-end\" : \"justify-start\"\r\n )}>\r\n {flexRender(cell.column.columnDef.cell, cell.getContext())}\r\n </div>\r\n </td>\r\n );\r\n })}\r\n </>\r\n );\r\n}\r\n\r\n// ─── Normal Rows ────────────────────────────────────────────────────────────\r\n\r\ninterface TableNormalRowsProps<TData extends RowData> {\r\n rows: Row<TData>[];\r\n enableColumnResizing: boolean;\r\n renderSubComponent?: (props: { row: TData }) => React.ReactNode;\r\n}\r\n\r\nexport function TableNormalRows<TData extends RowData>({\r\n rows,\r\n enableColumnResizing,\r\n renderSubComponent,\r\n}: TableNormalRowsProps<TData>) {\r\n return (\r\n <>\r\n {rows.map(row => (\r\n <React.Fragment key={row.id}>\r\n <tr\r\n className={cn(\r\n \"border-b border-border hover:bg-muted/50 transition-colors group\",\r\n row.getIsSelected() ? \"bg-primary/5 hover:bg-primary/10\" : \"\",\r\n row.getIsExpanded() ? \"bg-primary/5\" : \"\"\r\n )}\r\n >\r\n <TableRowCells row={row} enableColumnResizing={enableColumnResizing} />\r\n </tr>\r\n {row.getIsExpanded() && renderSubComponent && (\r\n <tr className=\"border-b border-border\">\r\n <td colSpan={row.getVisibleCells().length} className=\"p-0 whitespace-normal\">\r\n <div className=\"bg-muted/50 px-4 py-5 shadow-inner w-full border-l-4 border-l-primary/40 wrap-break-word\">\r\n {renderSubComponent({ row: row.original })}\r\n </div>\r\n </td>\r\n </tr>\r\n )}\r\n </React.Fragment>\r\n ))}\r\n </>\r\n );\r\n}\r\n\r\n// ─── Virtual Rows ───────────────────────────────────────────────────────────\r\n\r\ninterface TableVirtualRowsProps<TData extends RowData> {\r\n allRows: Row<TData>[];\r\n rowVirtualizer: Virtualizer<HTMLDivElement, Element>;\r\n enableColumnResizing: boolean;\r\n colSpan: number;\r\n}\r\n\r\nexport function TableVirtualRows<TData extends RowData>({\r\n allRows,\r\n rowVirtualizer,\r\n enableColumnResizing,\r\n colSpan,\r\n}: TableVirtualRowsProps<TData>) {\r\n const virtualItems = rowVirtualizer.getVirtualItems();\r\n\r\n return (\r\n <>\r\n {rowVirtualizer.getTotalSize() > 0 && (\r\n <tr aria-hidden=\"true\">\r\n <td style={{ height: virtualItems[0]?.start ?? 0, padding: 0, border: 0 }} colSpan={colSpan} />\r\n </tr>\r\n )}\r\n {virtualItems.map(virtualRow => {\r\n const row = allRows[virtualRow.index];\r\n if (!row) return null;\r\n return (\r\n <tr\r\n key={row.id}\r\n data-index={virtualRow.index}\r\n ref={rowVirtualizer.measureElement}\r\n className={cn(\r\n \"border-b border-border hover:bg-muted/50 transition-colors\",\r\n row.getIsSelected() ? \"bg-primary/5 hover:bg-primary/10\" : \"\"\r\n )}\r\n >\r\n <TableRowCells row={row} enableColumnResizing={enableColumnResizing} />\r\n </tr>\r\n );\r\n })}\r\n {rowVirtualizer.getTotalSize() > 0 && (\r\n <tr aria-hidden=\"true\">\r\n <td\r\n style={{\r\n height: rowVirtualizer.getTotalSize() - (virtualItems[virtualItems.length - 1]?.end ?? 0),\r\n padding: 0,\r\n border: 0,\r\n }}\r\n colSpan={colSpan}\r\n />\r\n </tr>\r\n )}\r\n </>\r\n );\r\n}\r\n"
816
765
  },
817
766
  {
818
767
  "path": "src/components/ui/table/TableHeader.tsx",
819
- "content": "import React from 'react';\r\nimport {\r\n flexRender,\r\n type HeaderGroup,\r\n type RowData,\r\n} from '@tanstack/react-table';\r\nimport { cn } from '@lib/utils/cn';\r\nimport { ChevronDown, ChevronUp } from 'lucide-react';\r\n\r\nexport interface TableHeaderProps<TData extends RowData> {\r\n headerGroups: HeaderGroup<TData>[];\r\n enableSorting: boolean;\r\n enableColumnResizing: boolean;\r\n}\r\n\r\nexport function TableHeader<TData extends RowData>({\r\n headerGroups,\r\n enableSorting,\r\n enableColumnResizing,\r\n}: TableHeaderProps<TData>) {\r\n return (\r\n <thead className=\"text-xs text-muted-foreground bg-muted/50 border-b border-border\">\r\n {headerGroups.map(headerGroup => (\r\n <tr key={headerGroup.id}>\r\n {headerGroup.headers.map(header => {\r\n const meta = header.column.columnDef.meta;\r\n const align = meta?.align || 'left';\r\n const canSort = header.column.getCanSort() && enableSorting && header.column.id !== 'select';\r\n\r\n return (\r\n <th\r\n key={header.id}\r\n colSpan={header.colSpan}\r\n style={{\r\n width: enableColumnResizing ? header.getSize() : header.column.columnDef.size,\r\n position: 'relative'\r\n }}\r\n aria-sort={\r\n canSort\r\n ? header.column.getIsSorted() === 'desc'\r\n ? 'descending'\r\n : header.column.getIsSorted() === 'asc'\r\n ? 'ascending'\r\n : 'none'\r\n : undefined\r\n }\r\n className={cn(\r\n header.column.id === 'select' || header.column.id === 'expander' ? \"px-1\" : \"px-2\",\r\n \"py-3 font-semibold tracking-wide border border-border transition-colors group/header\",\r\n canSort ? \"cursor-pointer select-none hover:bg-muted\" : \"\",\r\n align === 'center' ? \"text-center\" : align === 'right' ? \"text-right\" : \"text-left\"\r\n )}\r\n >\r\n <div\r\n className={cn(\"flex flex-col h-full\")}\r\n onClick={canSort ? header.column.getToggleSortingHandler() : undefined}\r\n >\r\n {header.isPlaceholder ? null : (\r\n <div className={cn(\r\n \"flex items-center gap-2\",\r\n align === 'center' ? \"justify-center\" : align === 'right' ? \"justify-end\" : \"justify-between\"\r\n )}>\r\n <span>{flexRender(header.column.columnDef.header, header.getContext())}</span>\r\n {canSort && (\r\n <span className=\"shrink-0\">\r\n {{\r\n asc: <ChevronUp className=\"w-4 h-4 text-primary\" />,\r\n desc: <ChevronDown className=\"w-4 h-4 text-primary\" />,\r\n }[header.column.getIsSorted() as string] ?? (\r\n <div className=\"flex flex-col opacity-30 -space-y-1 hover:opacity-100 transition-opacity\">\r\n <ChevronUp className=\"w-3 h-3\" />\r\n <ChevronDown className=\"w-3 h-3\" />\r\n </div>\r\n )}\r\n </span>\r\n )}\r\n </div>\r\n )}\r\n </div>\r\n\r\n {/* Resize Handle */}\r\n {enableColumnResizing && header.column.getCanResize() && (\r\n <div\r\n {...{\r\n onMouseDown: header.getResizeHandler(),\r\n onTouchStart: header.getResizeHandler(),\r\n className: cn(\r\n \"absolute right-0 top-0 h-full w-1 cursor-col-resize select-none touch-none hover:bg-primary/50 transition-colors\",\r\n header.column.getIsResizing() ? \"bg-primary w-1.5 z-10\" : \"bg-transparent\"\r\n ),\r\n }}\r\n />\r\n )}\r\n </th>\r\n );\r\n })}\r\n </tr>\r\n ))}\r\n </thead>\r\n );\r\n}\r\n"
768
+ "content": "import React from 'react';\r\nimport {\r\n flexRender,\r\n type HeaderGroup,\r\n type RowData,\r\n} from '@tanstack/react-table';\r\nimport { cn } from '@/lib/utils/cn';\r\nimport { ChevronDown, ChevronUp } from 'lucide-react';\r\n\r\nexport interface TableHeaderProps<TData extends RowData> {\r\n headerGroups: HeaderGroup<TData>[];\r\n enableSorting: boolean;\r\n enableColumnResizing: boolean;\r\n}\r\n\r\nexport function TableHeader<TData extends RowData>({\r\n headerGroups,\r\n enableSorting,\r\n enableColumnResizing,\r\n}: TableHeaderProps<TData>) {\r\n return (\r\n <thead className=\"text-xs text-muted-foreground bg-muted/50 border-b border-border\">\r\n {headerGroups.map(headerGroup => (\r\n <tr key={headerGroup.id} className=\"border-b border-border\">\r\n {headerGroup.headers.map((header, index) => {\r\n const meta = header.column.columnDef.meta;\r\n const align = meta?.align || 'left';\r\n const canSort = header.column.getCanSort() && enableSorting && header.column.id !== 'select';\r\n const isLastColumn = index === headerGroup.headers.length - 1;\r\n\r\n return (\r\n <th\r\n key={header.id}\r\n colSpan={header.colSpan}\r\n style={{\r\n width: enableColumnResizing ? header.getSize() : header.column.columnDef.size,\r\n position: 'relative'\r\n }}\r\n aria-sort={\r\n canSort\r\n ? header.column.getIsSorted() === 'desc'\r\n ? 'descending'\r\n : header.column.getIsSorted() === 'asc'\r\n ? 'ascending'\r\n : 'none'\r\n : undefined\r\n }\r\n className={cn(\r\n header.column.id === 'select' || header.column.id === 'expander' ? \"px-1\" : \"px-2\",\r\n \"py-3 font-semibold tracking-wide transition-colors group/header\",\r\n canSort ? \"cursor-pointer select-none hover:bg-muted\" : \"\",\r\n align === 'center' ? \"text-center\" : align === 'right' ? \"text-right\" : \"text-left\",\r\n !isLastColumn && \"border-r border-border\"\r\n )}\r\n >\r\n <div\r\n className={cn(\"flex flex-col h-full\")}\r\n onClick={canSort ? header.column.getToggleSortingHandler() : undefined}\r\n >\r\n {header.isPlaceholder ? null : (\r\n <div className={cn(\r\n \"flex items-center gap-2\",\r\n align === 'center' ? \"justify-center\" : align === 'right' ? \"justify-end\" : \"justify-between\"\r\n )}>\r\n <span>{flexRender(header.column.columnDef.header, header.getContext())}</span>\r\n {canSort && (\r\n <span className=\"shrink-0\">\r\n {{\r\n asc: <ChevronUp className=\"w-4 h-4 text-primary\" />,\r\n desc: <ChevronDown className=\"w-4 h-4 text-primary\" />,\r\n }[header.column.getIsSorted() as string] ?? (\r\n <div className=\"flex flex-col opacity-30 -space-y-1 hover:opacity-100 transition-opacity\">\r\n <ChevronUp className=\"w-3 h-3\" />\r\n <ChevronDown className=\"w-3 h-3\" />\r\n </div>\r\n )}\r\n </span>\r\n )}\r\n </div>\r\n )}\r\n </div>\r\n\r\n {/* Resize Handle */}\r\n {enableColumnResizing && header.column.getCanResize() && (\r\n <div\r\n {...{\r\n onMouseDown: header.getResizeHandler(),\r\n onTouchStart: header.getResizeHandler(),\r\n className: cn(\r\n \"absolute right-0 top-0 h-full w-1 cursor-col-resize select-none touch-none hover:bg-primary/50 transition-colors\",\r\n header.column.getIsResizing() ? \"bg-primary w-1.5 z-10\" : \"bg-transparent\"\r\n ),\r\n }}\r\n />\r\n )}\r\n </th>\r\n );\r\n })}\r\n </tr>\r\n ))}\r\n </thead>\r\n );\r\n}\r\n"
820
769
  },
821
770
  {
822
771
  "path": "src/components/ui/table/TablePagination.tsx",
@@ -872,7 +821,7 @@
872
821
  "files": [
873
822
  {
874
823
  "path": "src/components/ui/timeline/Timeline.tsx",
875
- "content": "import * as React from 'react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\nimport { cn } from '@/lib/utils/cn';\r\n\r\n// ─── Variants ────────────────────────────────────────────────────────────────\r\n\r\nconst timelineVariants = tv({\r\n slots: {\r\n root: 'relative flex flex-col',\r\n item: 'relative flex gap-4 pb-8 last:pb-0',\r\n indicator: [\r\n 'relative z-10 flex shrink-0 items-center justify-center rounded-full',\r\n 'border-2 border-background bg-muted ring-2 ring-background',\r\n ].join(' '),\r\n connector: 'absolute left-0 top-0 bottom-0 flex justify-center',\r\n connectorLine: 'w-px bg-border',\r\n content: 'flex-1 pt-0.5',\r\n title: 'text-sm font-semibold text-foreground leading-none',\r\n description: 'mt-1 text-sm text-muted-foreground leading-relaxed',\r\n time: 'mt-1.5 text-xs text-muted-foreground/70',\r\n },\r\n variants: {\r\n size: {\r\n sm: {\r\n indicator: 'h-6 w-6 [&_svg]:h-3 [&_svg]:w-3',\r\n connector: 'w-6',\r\n },\r\n md: {\r\n indicator: 'h-8 w-8 [&_svg]:h-4 [&_svg]:w-4',\r\n connector: 'w-8',\r\n },\r\n lg: {\r\n indicator: 'h-10 w-10 [&_svg]:h-5 [&_svg]:w-5',\r\n connector: 'w-10',\r\n },\r\n },\r\n variant: {\r\n default: { indicator: 'bg-muted text-muted-foreground' },\r\n primary: { indicator: 'bg-primary/15 text-primary border-primary/30' },\r\n success: { indicator: 'bg-success/15 text-success border-success/30' },\r\n warning: { indicator: 'bg-warning/15 text-warning border-warning/30' },\r\n danger: { indicator: 'bg-danger/15 text-danger border-danger/30' },\r\n },\r\n },\r\n defaultVariants: {\r\n size: 'md',\r\n variant: 'default',\r\n },\r\n});\r\n\r\n// ─── Types ───────────────────────────────────────────────────────────────────\r\n\r\nexport interface TimelineItemData {\r\n title: string;\r\n description?: string;\r\n time?: string;\r\n icon?: React.ReactNode;\r\n variant?: 'default' | 'primary' | 'success' | 'warning' | 'danger';\r\n}\r\n\r\nexport interface TimelineProps extends VariantProps<typeof timelineVariants> {\r\n items: TimelineItemData[];\r\n className?: string;\r\n}\r\n\r\n// ─── Component ───────────────────────────────────────────────────────────────\r\n\r\nconst Timeline = React.forwardRef<HTMLDivElement, TimelineProps>(\r\n ({ items, size = 'md', variant: defaultVariant = 'default', className }, ref) => {\r\n const styles = timelineVariants({ size });\r\n\r\n return (\r\n <div ref={ref} className={cn(styles.root(), className)} role=\"list\">\r\n {items.map((item, index) => {\r\n const itemVariant = item.variant ?? defaultVariant;\r\n const itemStyles = timelineVariants({ size, variant: itemVariant });\r\n const isLast = index === items.length - 1;\r\n\r\n return (\r\n <div key={index} className={styles.item()} role=\"listitem\">\r\n {/* Connector line */}\r\n {!isLast && (\r\n <div className={styles.connector()}>\r\n <div className={cn(styles.connectorLine(), 'mt-8')} />\r\n </div>\r\n )}\r\n\r\n {/* Indicator dot */}\r\n <div className={itemStyles.indicator()}>\r\n {item.icon}\r\n </div>\r\n\r\n {/* Content */}\r\n <div className={styles.content()}>\r\n <p className={styles.title()}>{item.title}</p>\r\n {item.description && (\r\n <p className={styles.description()}>{item.description}</p>\r\n )}\r\n {item.time && (\r\n <p className={styles.time()}>{item.time}</p>\r\n )}\r\n </div>\r\n </div>\r\n );\r\n })}\r\n </div>\r\n );\r\n },\r\n);\r\n\r\nTimeline.displayName = 'Timeline';\r\n\r\nexport { Timeline, timelineVariants };\r\n"
824
+ "content": "import * as React from 'react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\nimport { cn } from '@/lib/utils/cn';\r\n\r\n// ─── Variants ────────────────────────────────────────────────────────────────\r\n\r\nconst timelineVariants = tv({\r\n slots: {\r\n root: 'relative flex flex-col',\r\n item: 'group relative flex last:pb-0',\r\n indicator: [\r\n 'relative z-10 flex shrink-0 items-center justify-center rounded-full',\r\n 'border-[3px] border-background ring-2 ring-transparent',\r\n 'transition-all duration-400 ease-out',\r\n ' group-hover:ring-2',\r\n ].join(' '),\r\n connector: 'absolute left-0 top-0 bottom-0 flex justify-center',\r\n connectorLine: 'w-[2px] transition-all duration-400 ease-out origin-top rounded-b-full',\r\n contentWrapper: 'flex-1 transition-all duration-400 ease-out ',\r\n title: 'text-sm font-semibold tracking-tight text-foreground transition-colors duration-400',\r\n description: 'mt-1.5 text-sm text-muted-foreground/80 leading-relaxed',\r\n time: 'mt-2 text-[11px] font-medium text-muted-foreground/60 uppercase tracking-widest flex items-center transition-colors duration-400 group-hover:text-muted-foreground/90',\r\n },\r\n variants: {\r\n size: {\r\n sm: {\r\n indicator: 'h-7 w-7 [&_svg]:h-3.5 [&_svg]:w-3.5',\r\n connector: 'w-7',\r\n connectorLine: 'mt-7',\r\n item: 'gap-4 pb-6',\r\n contentWrapper: 'pt-1.5',\r\n },\r\n md: {\r\n indicator: 'h-9 w-9 [&_svg]:h-4 [&_svg]:w-4',\r\n connector: 'w-9',\r\n connectorLine: 'mt-9',\r\n item: 'gap-5 pb-8',\r\n contentWrapper: 'pt-2',\r\n },\r\n lg: {\r\n indicator: 'h-11 w-11 [&_svg]:h-5 [&_svg]:w-5',\r\n connector: 'w-11',\r\n connectorLine: 'mt-11',\r\n item: 'gap-6 pb-12',\r\n contentWrapper: 'pt-2.5',\r\n },\r\n },\r\n variant: {\r\n default: { \r\n indicator: 'bg-muted text-muted-foreground group-hover:bg-accent group-hover:text-accent-foreground group-hover:ring-foreground/5 group-hover:shadow-md group-hover:shadow-foreground/5', \r\n connectorLine: 'bg-gradient-to-b from-border/70 to-transparent group-hover:from-foreground/20' \r\n },\r\n primary: { \r\n indicator: 'bg-primary/10 text-primary group-hover:bg-primary/20 group-hover:text-primary group-hover:ring-primary/15 group-hover:shadow-md group-hover:shadow-primary/20', \r\n connectorLine: 'bg-gradient-to-b from-primary/30 to-transparent group-hover:from-primary/40' \r\n },\r\n success: { \r\n indicator: 'bg-success/10 text-success group-hover:bg-success/20 group-hover:text-success group-hover:ring-success/15 group-hover:shadow-md group-hover:shadow-success/20', \r\n connectorLine: 'bg-gradient-to-b from-success/30 to-transparent group-hover:from-success/40' \r\n },\r\n warning: { \r\n indicator: 'bg-warning/10 text-warning group-hover:bg-warning/20 group-hover:text-warning group-hover:ring-warning/15 group-hover:shadow-md group-hover:shadow-warning/20', \r\n connectorLine: 'bg-gradient-to-b from-warning/30 to-transparent group-hover:from-warning/40' \r\n },\r\n danger: { \r\n indicator: 'bg-danger/10 text-danger group-hover:bg-danger/20 group-hover:text-danger group-hover:ring-danger/15 group-hover:shadow-md group-hover:shadow-danger/20', \r\n connectorLine: 'bg-gradient-to-b from-danger/30 to-transparent group-hover:from-danger/40' \r\n },\r\n },\r\n },\r\n defaultVariants: {\r\n size: 'md',\r\n variant: 'default',\r\n },\r\n});\r\n\r\n// ─── Types ───────────────────────────────────────────────────────────────────\r\n\r\nexport interface TimelineItemData {\r\n title: string;\r\n description?: string;\r\n time?: string;\r\n icon?: React.ReactNode;\r\n variant?: 'default' | 'primary' | 'success' | 'warning' | 'danger';\r\n}\r\n\r\nexport interface TimelineProps extends VariantProps<typeof timelineVariants> {\r\n items: TimelineItemData[];\r\n className?: string;\r\n}\r\n\r\n// ─── Component ───────────────────────────────────────────────────────────────\r\n\r\nconst Timeline = React.forwardRef<HTMLDivElement, TimelineProps>(\r\n ({ items, size = 'md', variant: defaultVariant = 'default', className }, ref) => {\r\n const styles = timelineVariants({ size });\r\n\r\n return (\r\n <div ref={ref} className={cn(styles.root(), className)} role=\"list\">\r\n {items.map((item, index) => {\r\n const itemVariant = item.variant ?? defaultVariant;\r\n const itemStyles = timelineVariants({ size, variant: itemVariant });\r\n const isLast = index === items.length - 1;\r\n\r\n return (\r\n <div key={index} className={styles.item()} role=\"listitem\">\r\n {/* Connector line */}\r\n {!isLast && (\r\n <div className={styles.connector()}>\r\n <div className={itemStyles.connectorLine()} />\r\n </div>\r\n )}\r\n\r\n {/* Indicator dot */}\r\n <div className={itemStyles.indicator()}>\r\n {item.icon}\r\n </div>\r\n\r\n {/* Content */}\r\n <div className={styles.contentWrapper()}>\r\n <p className={styles.title()}>{item.title}</p>\r\n {item.description && (\r\n <p className={styles.description()}>{item.description}</p>\r\n )}\r\n {item.time && (\r\n <p className={styles.time()}>{item.time}</p>\r\n )}\r\n </div>\r\n </div>\r\n );\r\n })}\r\n </div>\r\n );\r\n },\r\n);\r\n\r\nTimeline.displayName = 'Timeline';\r\n\r\nexport { Timeline };\r\n"
876
825
  }
877
826
  ]
878
827
  },
@@ -927,7 +876,7 @@
927
876
  "files": [
928
877
  {
929
878
  "path": "src/components/ui/tree-view/TreeView.tsx",
930
- "content": "import * as React from 'react';\r\nimport { Collapsible } from '@base-ui/react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\nimport { cn } from '@/lib/utils/cn';\r\nimport { ChevronRight, FileIcon, FolderIcon, FolderOpen } from 'lucide-react';\r\n\r\n// ─── Variants ────────────────────────────────────────────────────────────────\r\n\r\nconst treeViewVariants = tv({\r\n slots: {\r\n root: 'flex flex-col text-sm',\r\n item: [\r\n 'flex items-center gap-1.5 rounded-md px-2 py-1.5 cursor-pointer',\r\n 'transition-colors hover:bg-muted/50',\r\n 'outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-inset',\r\n ].join(' '),\r\n itemActive: 'bg-primary/10 text-primary hover:bg-primary/15',\r\n chevron: 'h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200',\r\n chevronOpen: 'rotate-90',\r\n icon: 'h-4 w-4 shrink-0 text-muted-foreground',\r\n label: 'truncate select-none',\r\n children: 'pl-4',\r\n },\r\n});\r\n\r\nconst styles = treeViewVariants();\r\n\r\n// ─── Types ───────────────────────────────────────────────────────────────────\r\n\r\nexport interface TreeNode {\r\n id: string;\r\n label: string;\r\n icon?: React.ReactNode;\r\n children?: TreeNode[];\r\n disabled?: boolean;\r\n}\r\n\r\nexport interface TreeViewProps {\r\n data: TreeNode[];\r\n /** Currently selected node id */\r\n selectedId?: string;\r\n /** Called when a node is selected */\r\n onSelect?: (id: string) => void;\r\n /** Default expanded node ids */\r\n defaultExpanded?: string[];\r\n className?: string;\r\n}\r\n\r\n// ─── TreeItem (recursive) ──────────────────────────────────────���─────────────\r\n\r\ninterface TreeItemProps {\r\n node: TreeNode;\r\n level: number;\r\n selectedId?: string;\r\n onSelect?: (id: string) => void;\r\n expandedSet: Set<string>;\r\n toggleExpanded: (id: string) => void;\r\n}\r\n\r\nfunction TreeItem({ node, level, selectedId, onSelect, expandedSet, toggleExpanded }: TreeItemProps) {\r\n const hasChildren = node.children && node.children.length > 0;\r\n const isExpanded = expandedSet.has(node.id);\r\n const isSelected = selectedId === node.id;\r\n\r\n const handleClick = () => {\r\n if (node.disabled) return;\r\n if (hasChildren) {\r\n toggleExpanded(node.id);\r\n }\r\n onSelect?.(node.id);\r\n };\r\n\r\n const handleKeyDown = (e: React.KeyboardEvent) => {\r\n if (e.key === 'Enter' || e.key === ' ') {\r\n e.preventDefault();\r\n handleClick();\r\n }\r\n if (e.key === 'ArrowRight' && hasChildren && !isExpanded) {\r\n e.preventDefault();\r\n toggleExpanded(node.id);\r\n }\r\n if (e.key === 'ArrowLeft' && hasChildren && isExpanded) {\r\n e.preventDefault();\r\n toggleExpanded(node.id);\r\n }\r\n };\r\n\r\n const defaultIcon = hasChildren\r\n ? (isExpanded ? <FolderOpen className={styles.icon()} /> : <FolderIcon className={styles.icon()} />)\r\n : <FileIcon className={styles.icon()} />;\r\n\r\n return (\r\n <div role=\"treeitem\" aria-expanded={hasChildren ? isExpanded : undefined} aria-selected={isSelected}>\r\n <div\r\n className={cn(\r\n styles.item(),\r\n isSelected && styles.itemActive(),\r\n node.disabled && 'opacity-50 pointer-events-none',\r\n )}\r\n style={{ paddingLeft: `${level * 16 + 8}px` }}\r\n tabIndex={node.disabled ? undefined : 0}\r\n onClick={handleClick}\r\n onKeyDown={handleKeyDown}\r\n >\r\n {hasChildren ? (\r\n <ChevronRight className={cn(styles.chevron(), isExpanded && styles.chevronOpen())} />\r\n ) : (\r\n <span className=\"w-4 shrink-0\" />\r\n )}\r\n\r\n {node.icon ?? defaultIcon}\r\n <span className={styles.label()}>{node.label}</span>\r\n </div>\r\n\r\n {hasChildren && isExpanded && (\r\n <div role=\"group\">\r\n {node.children!.map((child) => (\r\n <TreeItem\r\n key={child.id}\r\n node={child}\r\n level={level + 1}\r\n selectedId={selectedId}\r\n onSelect={onSelect}\r\n expandedSet={expandedSet}\r\n toggleExpanded={toggleExpanded}\r\n />\r\n ))}\r\n </div>\r\n )}\r\n </div>\r\n );\r\n}\r\n\r\n// ─── TreeView ────────────────────────────────────────────────────────────────\r\n\r\nconst TreeView = React.forwardRef<HTMLDivElement, TreeViewProps>(\r\n ({ data, selectedId, onSelect, defaultExpanded = [], className }, ref) => {\r\n const [expandedSet, setExpandedSet] = React.useState<Set<string>>(\r\n () => new Set(defaultExpanded),\r\n );\r\n\r\n const toggleExpanded = React.useCallback((id: string) => {\r\n setExpandedSet((prev) => {\r\n const next = new Set(prev);\r\n if (next.has(id)) {\r\n next.delete(id);\r\n } else {\r\n next.add(id);\r\n }\r\n return next;\r\n });\r\n }, []);\r\n\r\n return (\r\n <div\r\n ref={ref}\r\n role=\"tree\"\r\n aria-label=\"Tree view\"\r\n className={cn(styles.root(), className)}\r\n >\r\n {data.map((node) => (\r\n <TreeItem\r\n key={node.id}\r\n node={node}\r\n level={0}\r\n selectedId={selectedId}\r\n onSelect={onSelect}\r\n expandedSet={expandedSet}\r\n toggleExpanded={toggleExpanded}\r\n />\r\n ))}\r\n </div>\r\n );\r\n },\r\n);\r\n\r\nTreeView.displayName = 'TreeView';\r\n\r\nexport { TreeView, treeViewVariants };\r\n"
879
+ "content": "import * as React from 'react';\r\nimport { Collapsible } from '@base-ui/react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\nimport { cn } from '@/lib/utils/cn';\r\nimport { ChevronRight, FileIcon, FolderIcon, FolderOpen } from 'lucide-react';\r\n\r\n// ─── Variants ────────────────────────────────────────────────────────────────\r\n\r\nconst treeViewVariants = tv({\r\n slots: {\r\n root: 'flex flex-col text-sm',\r\n item: [\r\n 'flex items-center gap-1.5 rounded-md px-2 py-1.5 cursor-pointer',\r\n 'transition-colors hover:bg-muted/50',\r\n 'outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-inset',\r\n ].join(' '),\r\n itemActive: 'bg-primary/10 text-primary hover:bg-primary/15',\r\n chevron: 'h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200',\r\n chevronOpen: 'rotate-90',\r\n icon: 'h-4 w-4 shrink-0 text-muted-foreground',\r\n label: 'truncate select-none',\r\n children: 'pl-4',\r\n },\r\n});\r\n\r\nconst styles = treeViewVariants();\r\n\r\n// ─── Types ───────────────────────────────────────────────────────────────────\r\n\r\nexport interface TreeNode {\r\n id: string;\r\n label: string;\r\n icon?: React.ReactNode;\r\n children?: TreeNode[];\r\n disabled?: boolean;\r\n}\r\n\r\nexport interface TreeViewProps {\r\n data: TreeNode[];\r\n /** Currently selected node id */\r\n selectedId?: string;\r\n /** Called when a node is selected */\r\n onSelect?: (id: string) => void;\r\n /** Default expanded node ids */\r\n defaultExpanded?: string[];\r\n className?: string;\r\n}\r\n\r\n// ─── TreeItem (recursive) ──────────────────────────────────────���─────────────\r\n\r\ninterface TreeItemProps {\r\n node: TreeNode;\r\n level: number;\r\n selectedId?: string;\r\n onSelect?: (id: string) => void;\r\n expandedSet: Set<string>;\r\n toggleExpanded: (id: string) => void;\r\n}\r\n\r\nfunction TreeItem({ node, level, selectedId, onSelect, expandedSet, toggleExpanded }: TreeItemProps) {\r\n const hasChildren = node.children && node.children.length > 0;\r\n const isExpanded = expandedSet.has(node.id);\r\n const isSelected = selectedId === node.id;\r\n\r\n const handleClick = () => {\r\n if (node.disabled) return;\r\n if (hasChildren) {\r\n toggleExpanded(node.id);\r\n }\r\n onSelect?.(node.id);\r\n };\r\n\r\n const handleKeyDown = (e: React.KeyboardEvent) => {\r\n if (e.key === 'Enter' || e.key === ' ') {\r\n e.preventDefault();\r\n handleClick();\r\n }\r\n if (e.key === 'ArrowRight' && hasChildren && !isExpanded) {\r\n e.preventDefault();\r\n toggleExpanded(node.id);\r\n }\r\n if (e.key === 'ArrowLeft' && hasChildren && isExpanded) {\r\n e.preventDefault();\r\n toggleExpanded(node.id);\r\n }\r\n };\r\n\r\n const defaultIcon = hasChildren\r\n ? (isExpanded ? <FolderOpen className={styles.icon()} /> : <FolderIcon className={styles.icon()} />)\r\n : <FileIcon className={styles.icon()} />;\r\n\r\n return (\r\n <div role=\"treeitem\" aria-expanded={hasChildren ? isExpanded : undefined} aria-selected={isSelected}>\r\n <div\r\n className={cn(\r\n styles.item(),\r\n isSelected && styles.itemActive(),\r\n node.disabled && 'opacity-50 pointer-events-none',\r\n )}\r\n style={{ paddingLeft: `${level * 16 + 8}px` }}\r\n tabIndex={node.disabled ? undefined : 0}\r\n onClick={handleClick}\r\n onKeyDown={handleKeyDown}\r\n >\r\n {hasChildren ? (\r\n <ChevronRight className={cn(styles.chevron(), isExpanded && styles.chevronOpen())} />\r\n ) : (\r\n <span className=\"w-4 shrink-0\" />\r\n )}\r\n\r\n {node.icon ?? defaultIcon}\r\n <span className={styles.label()}>{node.label}</span>\r\n </div>\r\n\r\n {hasChildren && isExpanded && (\r\n <div role=\"group\">\r\n {node.children!.map((child) => (\r\n <TreeItem\r\n key={child.id}\r\n node={child}\r\n level={level + 1}\r\n selectedId={selectedId}\r\n onSelect={onSelect}\r\n expandedSet={expandedSet}\r\n toggleExpanded={toggleExpanded}\r\n />\r\n ))}\r\n </div>\r\n )}\r\n </div>\r\n );\r\n}\r\n\r\n// ─── TreeView ────────────────────────────────────────────────────────────────\r\n\r\nconst TreeView = React.forwardRef<HTMLDivElement, TreeViewProps>(\r\n ({ data, selectedId, onSelect, defaultExpanded = [], className }, ref) => {\r\n const [expandedSet, setExpandedSet] = React.useState<Set<string>>(\r\n () => new Set(defaultExpanded),\r\n );\r\n\r\n const toggleExpanded = React.useCallback((id: string) => {\r\n setExpandedSet((prev) => {\r\n const next = new Set(prev);\r\n if (next.has(id)) {\r\n next.delete(id);\r\n } else {\r\n next.add(id);\r\n }\r\n return next;\r\n });\r\n }, []);\r\n\r\n return (\r\n <div\r\n ref={ref}\r\n role=\"tree\"\r\n aria-label=\"Tree view\"\r\n className={cn(styles.root(), className)}\r\n >\r\n {data.map((node) => (\r\n <TreeItem\r\n key={node.id}\r\n node={node}\r\n level={0}\r\n selectedId={selectedId}\r\n onSelect={onSelect}\r\n expandedSet={expandedSet}\r\n toggleExpanded={toggleExpanded}\r\n />\r\n ))}\r\n </div>\r\n );\r\n },\r\n);\r\n\r\nTreeView.displayName = 'TreeView';\r\n\r\nexport { TreeView };\r\n"
931
880
  }
932
881
  ]
933
882
  },
@@ -0,0 +1,22 @@
1
+ import fs from 'fs';
2
+ import { execSync } from 'child_process';
3
+
4
+ // Skip if triggered by a version bump commit (avoid infinite loop)
5
+ const lastMsg = execSync('git log -1 --pretty=%s').toString().trim();
6
+ if (lastMsg.startsWith('chore: bump version')) process.exit(0);
7
+
8
+ const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf-8'));
9
+ const [major, minor, patch] = pkg.version.split('.').map(Number);
10
+ const newVersion = `${major}.${minor}.${patch + 1}`;
11
+
12
+ pkg.version = newVersion;
13
+ fs.writeFileSync('./package.json', JSON.stringify(pkg, null, 2) + '\n');
14
+
15
+ let cli = fs.readFileSync('./scripts/ui-cli.ts', 'utf-8');
16
+ cli = cli.replace(/const VERSION = '[^']+';/, `const VERSION = '${newVersion}';`);
17
+ fs.writeFileSync('./scripts/ui-cli.ts', cli);
18
+
19
+ execSync('git add package.json scripts/ui-cli.ts');
20
+ execSync(`git commit --no-verify -m "chore: bump version to ${newVersion}"`);
21
+
22
+ console.log(`\x1b[32m✔\x1b[0m Bumped version → ${newVersion}`);
@@ -0,0 +1,31 @@
1
+ import fs from 'fs';
2
+
3
+ const arg = process.argv[2];
4
+ if (!arg) {
5
+ console.error('Usage: node scripts/set-version.mjs <version|major|minor|patch>');
6
+ console.error(' Examples: node scripts/set-version.mjs 1.0.0');
7
+ console.error(' node scripts/set-version.mjs major');
8
+ process.exit(1);
9
+ }
10
+
11
+ const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf-8'));
12
+ const [major, minor, patch] = pkg.version.split('.').map(Number);
13
+
14
+ let newVersion;
15
+ if (arg === 'major') newVersion = `${major + 1}.0.0`;
16
+ else if (arg === 'minor') newVersion = `${major}.${minor + 1}.0`;
17
+ else if (arg === 'patch') newVersion = `${major}.${minor}.${patch + 1}`;
18
+ else if (/^\d+\.\d+\.\d+$/.test(arg)) newVersion = arg;
19
+ else {
20
+ console.error(`Invalid version: "${arg}"`);
21
+ process.exit(1);
22
+ }
23
+
24
+ pkg.version = newVersion;
25
+ fs.writeFileSync('./package.json', JSON.stringify(pkg, null, 2) + '\n');
26
+
27
+ let cli = fs.readFileSync('./scripts/ui-cli.ts', 'utf-8');
28
+ cli = cli.replace(/const VERSION = '[^']+';/, `const VERSION = '${newVersion}';`);
29
+ fs.writeFileSync('./scripts/ui-cli.ts', cli);
30
+
31
+ console.log(`\x1b[32m✔\x1b[0m Version set → ${newVersion}`);
package/scripts/ui-cli.ts CHANGED
@@ -6,7 +6,7 @@ import readline from 'readline';
6
6
 
7
7
  // ─── Constants ────────────────────────────────────────────────────────────────
8
8
 
9
- const VERSION = '0.2.7';
9
+ const VERSION = '0.2.8';
10
10
  const REGISTRY_LOCAL = './registry.json';
11
11
  const REGISTRY_REMOTE = 'https://raw.githubusercontent.com/Basuicn/basuicn-core/main/registry.json';
12
12