@devxcrew/react-ui 0.0.0-stage → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (326) hide show
  1. package/README.md +85 -2
  2. package/package.json +105 -4
  3. package/src/blocks/agent-chat-workspace/agent-chat-workspace.tsx +162 -0
  4. package/src/blocks/agent-chat-workspace/index.ts +2 -0
  5. package/src/blocks/agent-provider-settings/agent-provider-settings.tsx +183 -0
  6. package/src/blocks/agent-provider-settings/index.ts +2 -0
  7. package/src/blocks/agent-task-workspace/agent-task-workspace.tsx +48 -0
  8. package/src/blocks/agent-task-workspace/index.ts +2 -0
  9. package/src/blocks/analysis-context/analysis-context.tsx +6 -0
  10. package/src/blocks/analysis-context/index.ts +1 -0
  11. package/src/blocks/app-header/app-header.tsx +159 -0
  12. package/src/blocks/app-header/index.ts +2 -0
  13. package/src/blocks/archived-chat-workspace/archived-chat-workspace.tsx +29 -0
  14. package/src/blocks/archived-chat-workspace/index.ts +2 -0
  15. package/src/blocks/auth/README.md +32 -0
  16. package/src/blocks/auth/admin-login-page.tsx +15 -0
  17. package/src/blocks/auth/admin-password-forgot-page.tsx +5 -0
  18. package/src/blocks/auth/auth-page-layout.tsx +90 -0
  19. package/src/blocks/auth/client-login-page.tsx +20 -0
  20. package/src/blocks/auth/client-password-forgot-page.tsx +5 -0
  21. package/src/blocks/auth/identity-management-desk.tsx +530 -0
  22. package/src/blocks/auth/index.ts +12 -0
  23. package/src/blocks/auth/login-page.tsx +206 -0
  24. package/src/blocks/auth/password-forgot-page.tsx +60 -0
  25. package/src/blocks/auth/portal-pages.tsx +32 -0
  26. package/src/blocks/auth/privileged-desk.tsx +55 -0
  27. package/src/blocks/auth/register-page.tsx +117 -0
  28. package/src/blocks/auth/session-boundary.tsx +296 -0
  29. package/src/blocks/auth/super-admin-login-page.tsx +15 -0
  30. package/src/blocks/auth/super-admin-password-forgot-page.tsx +5 -0
  31. package/src/blocks/blog/blog-card.tsx +156 -0
  32. package/src/blocks/blog/blog-reader.tsx +175 -0
  33. package/src/blocks/blog/blog-types.ts +47 -0
  34. package/src/blocks/blog/index.ts +3 -0
  35. package/src/blocks/chat-composer/chat-composer.tsx +140 -0
  36. package/src/blocks/chat-composer/index.ts +1 -0
  37. package/src/blocks/chat-history/chat-history.tsx +59 -0
  38. package/src/blocks/chat-history/index.ts +1 -0
  39. package/src/blocks/chat-response-progress/chat-response-progress.tsx +34 -0
  40. package/src/blocks/chat-response-progress/index.ts +2 -0
  41. package/src/blocks/chat-runtime-controls/chat-runtime-controls.tsx +56 -0
  42. package/src/blocks/chat-runtime-controls/index.ts +2 -0
  43. package/src/blocks/chat-runtime-trace/chat-runtime-trace.tsx +71 -0
  44. package/src/blocks/chat-runtime-trace/index.ts +2 -0
  45. package/src/blocks/codex-connection-settings/codex-connection-settings.tsx +42 -0
  46. package/src/blocks/codex-connection-settings/index.ts +1 -0
  47. package/src/blocks/dropzone/dropzone-file-item.tsx +113 -0
  48. package/src/blocks/dropzone/dropzone-types.ts +32 -0
  49. package/src/blocks/dropzone/dropzone.tsx +216 -0
  50. package/src/blocks/dropzone/index.ts +3 -0
  51. package/src/blocks/ecommerce/cart/cart-drawer.tsx +228 -0
  52. package/src/blocks/ecommerce/cart/cart-types.ts +27 -0
  53. package/src/blocks/ecommerce/cart/index.ts +2 -0
  54. package/src/blocks/ecommerce/categories/categories-types.ts +23 -0
  55. package/src/blocks/ecommerce/categories/category-showcase.tsx +125 -0
  56. package/src/blocks/ecommerce/categories/index.ts +2 -0
  57. package/src/blocks/ecommerce/checkout/checkout-types.ts +48 -0
  58. package/src/blocks/ecommerce/checkout/checkout-wizard.tsx +468 -0
  59. package/src/blocks/ecommerce/checkout/index.ts +2 -0
  60. package/src/blocks/ecommerce/comparison/comparison-types.ts +28 -0
  61. package/src/blocks/ecommerce/comparison/index.ts +2 -0
  62. package/src/blocks/ecommerce/comparison/product-comparison.tsx +174 -0
  63. package/src/blocks/ecommerce/coupon-wallet/coupon-wallet-types.ts +21 -0
  64. package/src/blocks/ecommerce/coupon-wallet/coupon-wallet.tsx +154 -0
  65. package/src/blocks/ecommerce/coupon-wallet/index.ts +2 -0
  66. package/src/blocks/ecommerce/delivery-tracker/delivery-tracker-types.ts +20 -0
  67. package/src/blocks/ecommerce/delivery-tracker/delivery-tracker.tsx +169 -0
  68. package/src/blocks/ecommerce/delivery-tracker/index.ts +2 -0
  69. package/src/blocks/ecommerce/index.ts +12 -0
  70. package/src/blocks/ecommerce/payment-methods/index.ts +2 -0
  71. package/src/blocks/ecommerce/payment-methods/payment-method-selector.tsx +224 -0
  72. package/src/blocks/ecommerce/payment-methods/payment-methods-types.ts +21 -0
  73. package/src/blocks/ecommerce/price-history/index.ts +2 -0
  74. package/src/blocks/ecommerce/price-history/price-history-chart.tsx +217 -0
  75. package/src/blocks/ecommerce/price-history/price-history-types.ts +16 -0
  76. package/src/blocks/ecommerce/reviews/index.ts +2 -0
  77. package/src/blocks/ecommerce/reviews/reviews-section.tsx +309 -0
  78. package/src/blocks/ecommerce/reviews/reviews-types.ts +26 -0
  79. package/src/blocks/ecommerce/wishlist/index.ts +2 -0
  80. package/src/blocks/ecommerce/wishlist/wishlist-grid.tsx +149 -0
  81. package/src/blocks/ecommerce/wishlist/wishlist-types.ts +19 -0
  82. package/src/blocks/execution-status/README.md +25 -0
  83. package/src/blocks/execution-status/execution-status.tsx +135 -0
  84. package/src/blocks/execution-status/index.ts +2 -0
  85. package/src/blocks/file-tree/file-icons.tsx +84 -0
  86. package/src/blocks/file-tree/file-tree-node.tsx +158 -0
  87. package/src/blocks/file-tree/file-tree-types.ts +33 -0
  88. package/src/blocks/file-tree/file-tree.tsx +163 -0
  89. package/src/blocks/file-tree/index.ts +5 -0
  90. package/src/blocks/filter-builder/filter-builder.tsx +169 -0
  91. package/src/blocks/filter-builder/filter-row.tsx +164 -0
  92. package/src/blocks/filter-builder/filter-types.ts +61 -0
  93. package/src/blocks/filter-builder/index.ts +12 -0
  94. package/src/blocks/footer/footer-types.ts +35 -0
  95. package/src/blocks/footer/index.ts +2 -0
  96. package/src/blocks/footer/site-footer.tsx +212 -0
  97. package/src/blocks/form/form-block.tsx +113 -0
  98. package/src/blocks/form/form-lookup-field.tsx +60 -0
  99. package/src/blocks/form/index.ts +4 -0
  100. package/src/blocks/handover-stack/handover-stack.tsx +41 -0
  101. package/src/blocks/handover-stack/index.ts +1 -0
  102. package/src/blocks/idea-handover/idea-handover-readiness.ts +41 -0
  103. package/src/blocks/idea-handover/idea-handover-workspace.tsx +150 -0
  104. package/src/blocks/idea-handover/index.ts +4 -0
  105. package/src/blocks/kanban/index.ts +11 -0
  106. package/src/blocks/kanban/kanban-board.tsx +146 -0
  107. package/src/blocks/kanban/kanban-card.tsx +149 -0
  108. package/src/blocks/kanban/kanban-column.tsx +102 -0
  109. package/src/blocks/kanban/kanban-types.ts +38 -0
  110. package/src/blocks/loader/global-loader.tsx +90 -0
  111. package/src/blocks/loader/index.ts +1 -0
  112. package/src/blocks/mascot/README.md +21 -0
  113. package/src/blocks/mascot/index.ts +2 -0
  114. package/src/blocks/mascot/mascot.tsx +202 -0
  115. package/src/blocks/master-list/README.md +21 -0
  116. package/src/blocks/master-list/index.ts +12 -0
  117. package/src/blocks/master-list/master-form.tsx +157 -0
  118. package/src/blocks/master-list/master-list-desk.tsx +370 -0
  119. package/src/blocks/master-list/master-list.tsx +189 -0
  120. package/src/blocks/master-list/types.ts +18 -0
  121. package/src/blocks/mermaid-preview/index.ts +1 -0
  122. package/src/blocks/mermaid-preview/mermaid-preview.tsx +164 -0
  123. package/src/blocks/notifications/README.md +7 -0
  124. package/src/blocks/notifications/index.ts +1 -0
  125. package/src/blocks/notifications/notification-center-page.tsx +73 -0
  126. package/src/blocks/pricing/index.ts +7 -0
  127. package/src/blocks/pricing/pricing-table.tsx +173 -0
  128. package/src/blocks/pricing/pricing-types.ts +34 -0
  129. package/src/blocks/product-card/index.ts +2 -0
  130. package/src/blocks/product-card/product-card-types.ts +35 -0
  131. package/src/blocks/product-card/product-card.tsx +183 -0
  132. package/src/blocks/table/data-table-block.tsx +313 -0
  133. package/src/blocks/table/data-table-filter-menu.tsx +48 -0
  134. package/src/blocks/table/data-table-icon-menu-trigger.tsx +44 -0
  135. package/src/blocks/table/data-table-menu-content.tsx +32 -0
  136. package/src/blocks/table/data-table-pagination.tsx +149 -0
  137. package/src/blocks/table/data-table-row-actions.tsx +53 -0
  138. package/src/blocks/table/data-table-status.tsx +42 -0
  139. package/src/blocks/table/data-table-toolbar.tsx +124 -0
  140. package/src/blocks/table/data-table-totals.tsx +31 -0
  141. package/src/blocks/table/index.ts +13 -0
  142. package/src/blocks/workspace/index.ts +19 -0
  143. package/src/blocks/workspace/workspace-action-card.tsx +73 -0
  144. package/src/blocks/workspace/workspace-application-header.tsx +67 -0
  145. package/src/blocks/workspace/workspace-entity-card.tsx +48 -0
  146. package/src/blocks/workspace/workspace-health-summary.tsx +15 -0
  147. package/src/blocks/workspace/workspace-metric-card.tsx +97 -0
  148. package/src/blocks/workspace/workspace-page-header.tsx +43 -0
  149. package/src/blocks/workspace/workspace-publish-status.tsx +15 -0
  150. package/src/blocks/workspace/workspace-route-checklist.tsx +28 -0
  151. package/src/blocks/workspace/workspace-runtime-status.tsx +48 -0
  152. package/src/blocks/workspace/workspace-section-card.tsx +42 -0
  153. package/src/components/accordion.tsx +65 -0
  154. package/src/components/alert-dialog.tsx +160 -0
  155. package/src/components/alert.tsx +68 -0
  156. package/src/components/aspect-ratio.tsx +22 -0
  157. package/src/components/attachment.tsx +195 -0
  158. package/src/components/avatar.tsx +90 -0
  159. package/src/components/badge.tsx +55 -0
  160. package/src/components/breadcrumb.tsx +100 -0
  161. package/src/components/bubble.tsx +123 -0
  162. package/src/components/button-group.tsx +79 -0
  163. package/src/components/button.tsx +71 -0
  164. package/src/components/calendar.tsx +185 -0
  165. package/src/components/card.tsx +100 -0
  166. package/src/components/carousel.tsx +229 -0
  167. package/src/components/chart.tsx +338 -0
  168. package/src/components/checkbox.tsx +27 -0
  169. package/src/components/collapsible.tsx +15 -0
  170. package/src/components/combobox.tsx +269 -0
  171. package/src/components/command.tsx +181 -0
  172. package/src/components/compact-model-switcher.tsx +128 -0
  173. package/src/components/context-menu.tsx +244 -0
  174. package/src/components/dialog.tsx +138 -0
  175. package/src/components/direction.tsx +1 -0
  176. package/src/components/drawer.tsx +208 -0
  177. package/src/components/dropdown-menu.tsx +257 -0
  178. package/src/components/empty.tsx +93 -0
  179. package/src/components/field.tsx +232 -0
  180. package/src/components/hover-card.tsx +45 -0
  181. package/src/components/input-group.tsx +146 -0
  182. package/src/components/input-otp.tsx +83 -0
  183. package/src/components/input.tsx +19 -0
  184. package/src/components/item.tsx +188 -0
  185. package/src/components/kbd.tsx +26 -0
  186. package/src/components/label.tsx +17 -0
  187. package/src/components/markdown-content.tsx +113 -0
  188. package/src/components/marker.tsx +67 -0
  189. package/src/components/menubar.tsx +267 -0
  190. package/src/components/message-scroller.tsx +91 -0
  191. package/src/components/message.tsx +84 -0
  192. package/src/components/native-select.tsx +54 -0
  193. package/src/components/navigation-menu.tsx +160 -0
  194. package/src/components/pagination.tsx +118 -0
  195. package/src/components/popover.tsx +74 -0
  196. package/src/components/progress.tsx +82 -0
  197. package/src/components/questionnaire.tsx +150 -0
  198. package/src/components/radio-group.tsx +35 -0
  199. package/src/components/resizable.tsx +41 -0
  200. package/src/components/rich-text-editor.tsx +604 -0
  201. package/src/components/scroll-area.tsx +47 -0
  202. package/src/components/select.tsx +189 -0
  203. package/src/components/separator.tsx +18 -0
  204. package/src/components/sheet.tsx +125 -0
  205. package/src/components/sidebar.tsx +693 -0
  206. package/src/components/skeleton.tsx +13 -0
  207. package/src/components/slider.tsx +51 -0
  208. package/src/components/sonner.tsx +45 -0
  209. package/src/components/sparkline.tsx +231 -0
  210. package/src/components/spinner.tsx +20 -0
  211. package/src/components/status-badge.tsx +114 -0
  212. package/src/components/switch.tsx +31 -0
  213. package/src/components/table.tsx +91 -0
  214. package/src/components/tabs.tsx +72 -0
  215. package/src/components/textarea.tsx +17 -0
  216. package/src/components/toast.tsx +222 -0
  217. package/src/components/toggle-group.tsx +85 -0
  218. package/src/components/toggle.tsx +42 -0
  219. package/src/components/tooltip.tsx +53 -0
  220. package/src/design-system/README.md +55 -0
  221. package/src/design-system/action-intents.ts +21 -0
  222. package/src/design-system/agent-manifest.ts +83 -0
  223. package/src/design-system/block-registry.ts +272 -0
  224. package/src/design-system/component-registry.ts +142 -0
  225. package/src/design-system/contracts.ts +69 -0
  226. package/src/design-system/defaults.ts +3 -0
  227. package/src/design-system/index.ts +29 -0
  228. package/src/design-system/mcp-server.ts +156 -0
  229. package/src/design-system/page-registry.ts +74 -0
  230. package/src/design-system/selection.ts +67 -0
  231. package/src/design-system/template-registry.ts +33 -0
  232. package/src/features/interface-topology/index.ts +14 -0
  233. package/src/features/interface-topology/interface-topology.types.ts +40 -0
  234. package/src/features/interface-topology/topology-control.tsx +18 -0
  235. package/src/features/interface-topology/topology-inspector.tsx +228 -0
  236. package/src/features/interface-topology/topology-marker.tsx +47 -0
  237. package/src/features/interface-topology/topology-region.tsx +44 -0
  238. package/src/features/interface-topology/use-interface-topology.ts +178 -0
  239. package/src/hooks/use-mobile.ts +19 -0
  240. package/src/hooks/use-scroll-follow.ts +22 -0
  241. package/src/index.ts +11 -0
  242. package/src/layouts/README.md +65 -0
  243. package/src/layouts/agent-workspace/agent-activity-rail.tsx +101 -0
  244. package/src/layouts/agent-workspace/agent-workspace.tsx +27 -0
  245. package/src/layouts/agent-workspace/agent-workspace.types.ts +28 -0
  246. package/src/layouts/agent-workspace/index.ts +7 -0
  247. package/src/layouts/blog-header/blog-header-types.ts +25 -0
  248. package/src/layouts/blog-header/blog-header.tsx +172 -0
  249. package/src/layouts/blog-header/index.ts +2 -0
  250. package/src/layouts/documentation-workspace/README.md +8 -0
  251. package/src/layouts/documentation-workspace/documentation-workspace.tsx +34 -0
  252. package/src/layouts/documentation-workspace/index.ts +1 -0
  253. package/src/layouts/ecommerce-header/ecommerce-actions.tsx +89 -0
  254. package/src/layouts/ecommerce-header/ecommerce-announcement.tsx +43 -0
  255. package/src/layouts/ecommerce-header/ecommerce-drawer.tsx +97 -0
  256. package/src/layouts/ecommerce-header/ecommerce-header-types.ts +59 -0
  257. package/src/layouts/ecommerce-header/ecommerce-header.tsx +47 -0
  258. package/src/layouts/ecommerce-header/ecommerce-navigation.tsx +98 -0
  259. package/src/layouts/ecommerce-header/ecommerce-search.tsx +86 -0
  260. package/src/layouts/ecommerce-header/index.ts +2 -0
  261. package/src/layouts/main-workspace/index.ts +28 -0
  262. package/src/layouts/mdi/index.ts +1 -0
  263. package/src/layouts/mdi/mdi.tsx +20 -0
  264. package/src/layouts/mdi-main/index.ts +27 -0
  265. package/src/layouts/mdi-main/mdi-app-catalog.generated.ts +159 -0
  266. package/src/layouts/mdi-main/mdi-app-catalog.ts +76 -0
  267. package/src/layouts/mdi-main/mdi-app-switcher.tsx +107 -0
  268. package/src/layouts/mdi-main/mdi-feature-settings.tsx +135 -0
  269. package/src/layouts/mdi-main/mdi-global-search.tsx +192 -0
  270. package/src/layouts/mdi-main/mdi-main.tsx +241 -0
  271. package/src/layouts/mdi-main/mdi-notifications-menu.tsx +197 -0
  272. package/src/layouts/mdi-main/mdi-profile-menu.tsx +79 -0
  273. package/src/layouts/mdi-main/mdi-sidebar-navigation.tsx +226 -0
  274. package/src/layouts/mdi-main/mdi-sidebar.tsx +110 -0
  275. package/src/layouts/mdi-main/mdi-status-bar.tsx +26 -0
  276. package/src/layouts/mdi-main/mdi-top-menu-control.ts +4 -0
  277. package/src/layouts/mdi-main/mdi-top-menu.tsx +117 -0
  278. package/src/layouts/mdi-main/mdi-topology.tsx +297 -0
  279. package/src/layouts/mdi-main/mdi-tweak-panel.tsx +104 -0
  280. package/src/layouts/mdi-main/mdi-types.ts +131 -0
  281. package/src/layouts/mdi-main/use-mdi-features.ts +36 -0
  282. package/src/layouts/mdi-main/use-mdi-sidebar-state.ts +59 -0
  283. package/src/layouts/site-header/announcement-bar.tsx +55 -0
  284. package/src/layouts/site-header/category-strip.tsx +41 -0
  285. package/src/layouts/site-header/index.ts +16 -0
  286. package/src/layouts/site-header/main-navbar.tsx +179 -0
  287. package/src/layouts/site-header/mobile-drawer.tsx +140 -0
  288. package/src/layouts/site-header/site-header-types.ts +63 -0
  289. package/src/layouts/site-header/site-header.tsx +58 -0
  290. package/src/lib/random-id.ts +19 -0
  291. package/src/lib/utils.ts +6 -0
  292. package/src/providers/pdf-provider.ts +137 -0
  293. package/src/styles/globals.css +374 -0
  294. package/src/templates/README.md +12 -0
  295. package/src/templates/dashboard-01/components/app-sidebar.tsx +177 -0
  296. package/src/templates/dashboard-01/components/chart-area-interactive.tsx +276 -0
  297. package/src/templates/dashboard-01/components/data-table-cell-viewer.tsx +213 -0
  298. package/src/templates/dashboard-01/components/data-table.tsx +577 -0
  299. package/src/templates/dashboard-01/components/nav-documents.tsx +79 -0
  300. package/src/templates/dashboard-01/components/nav-main.tsx +55 -0
  301. package/src/templates/dashboard-01/components/nav-secondary.tsx +39 -0
  302. package/src/templates/dashboard-01/components/nav-user.tsx +97 -0
  303. package/src/templates/dashboard-01/components/section-cards.tsx +99 -0
  304. package/src/templates/dashboard-01/components/site-header.tsx +14 -0
  305. package/src/templates/dashboard-01/data.json +614 -0
  306. package/src/templates/dashboard-01/index.ts +7 -0
  307. package/src/templates/documentation-sidebar/app-sidebar.tsx +95 -0
  308. package/src/templates/documentation-sidebar/index.ts +2 -0
  309. package/src/templates/sidebar-07/components/app-sidebar.tsx +175 -0
  310. package/src/templates/sidebar-07/components/nav-main.tsx +64 -0
  311. package/src/templates/sidebar-07/components/nav-projects.tsx +79 -0
  312. package/src/templates/sidebar-07/components/nav-user.tsx +105 -0
  313. package/src/templates/sidebar-07/components/team-switcher.tsx +94 -0
  314. package/src/templates/sidebar-07/index.ts +1 -0
  315. package/src/templates/ui-page/index.ts +7 -0
  316. package/src/templates/ui-page/ui-browser-frame.tsx +27 -0
  317. package/src/templates/ui-page/ui-template-code.tsx +48 -0
  318. package/src/templates/ui-page/ui-template-header.tsx +87 -0
  319. package/src/templates/ui-page/ui-template-navigation.tsx +60 -0
  320. package/src/templates/ui-page/ui-template-page.tsx +97 -0
  321. package/src/theme/index.ts +12 -0
  322. package/src/theme/theme-config.ts +30 -0
  323. package/src/theme/theme-provider.tsx +142 -0
  324. package/src/theme/theme-selector.tsx +75 -0
  325. package/src/tokens/index.ts +15 -0
  326. package/src/vite-env.d.ts +3 -0
@@ -0,0 +1,163 @@
1
+ import { ChevronsDownUp, ChevronsUpDown, Search } from 'lucide-react'
2
+ import { useMemo, useState } from 'react'
3
+ import { cn } from '../../lib/utils'
4
+ import { FileTreeNodeComponent } from './file-tree-node'
5
+ import type { FileTreeNode, FileTreeProps } from './file-tree-types'
6
+
7
+ function filterNodes(nodes: readonly FileTreeNode[], query: string): FileTreeNode[] {
8
+ const q = query.toLowerCase()
9
+ const result: FileTreeNode[] = []
10
+
11
+ for (const node of nodes) {
12
+ if (node.name.toLowerCase().includes(q)) {
13
+ result.push(node)
14
+ } else if (node.type === 'directory' && node.children) {
15
+ const filteredChildren = filterNodes(node.children, query)
16
+ if (filteredChildren.length > 0) {
17
+ result.push({
18
+ ...node,
19
+ children: filteredChildren,
20
+ })
21
+ }
22
+ }
23
+ }
24
+
25
+ return result
26
+ }
27
+
28
+ function getAllDirectoryIds(nodes: readonly FileTreeNode[]): string[] {
29
+ const ids: string[] = []
30
+ for (const node of nodes) {
31
+ if (node.type === 'directory') {
32
+ ids.push(node.id)
33
+ if (node.children) {
34
+ ids.push(...getAllDirectoryIds(node.children))
35
+ }
36
+ }
37
+ }
38
+ return ids
39
+ }
40
+
41
+ export function FileTree({
42
+ allowCreate = false,
43
+ allowDelete = false,
44
+ ariaLabel = 'File and workspace tree',
45
+ className,
46
+ defaultExpandedIds = [],
47
+ emptyMessage = 'No files found',
48
+ nodes,
49
+ onCreateFile,
50
+ onCreateFolder,
51
+ onDeleteNode,
52
+ onExpandChange,
53
+ onSelectNode,
54
+ renderNodeActions,
55
+ searchPlaceholder = 'Filter files...',
56
+ selectedNodeId,
57
+ showFilter = true,
58
+ }: FileTreeProps) {
59
+ const [expandedIds, setExpandedIds] = useState<Set<string>>(() => new Set(defaultExpandedIds))
60
+ const [searchQuery, setSearchQuery] = useState('')
61
+
62
+ const allDirIds = useMemo(() => getAllDirectoryIds(nodes), [nodes])
63
+
64
+ const filteredNodes = useMemo(() => {
65
+ if (!searchQuery.trim()) return nodes
66
+ return filterNodes(nodes, searchQuery.trim())
67
+ }, [nodes, searchQuery])
68
+
69
+ function handleToggleExpand(node: FileTreeNode) {
70
+ setExpandedIds((prev) => {
71
+ const next = new Set(prev)
72
+ const isExpanding = !next.has(node.id)
73
+ if (isExpanding) {
74
+ next.add(node.id)
75
+ } else {
76
+ next.delete(node.id)
77
+ }
78
+ onExpandChange?.(node, isExpanding)
79
+ return next
80
+ })
81
+ }
82
+
83
+ function handleExpandAll() {
84
+ setExpandedIds(new Set(allDirIds))
85
+ }
86
+
87
+ function handleCollapseAll() {
88
+ setExpandedIds(new Set())
89
+ }
90
+
91
+ return (
92
+ <div
93
+ aria-label={ariaLabel}
94
+ className={cn(
95
+ 'flex flex-col rounded-2xl border border-border/80 bg-card p-2 text-card-foreground shadow-2xs',
96
+ className,
97
+ )}
98
+ role="tree"
99
+ >
100
+ {showFilter && (
101
+ <div className="mb-2 flex items-center justify-between gap-1.5 border-b border-border/40 pb-2 px-1">
102
+ <div className="relative flex-1">
103
+ <Search className="absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground/60" />
104
+ <input
105
+ aria-label="Filter tree nodes"
106
+ className="h-8 w-full rounded-lg border border-border/60 bg-muted/40 pl-8 pr-2.5 text-xs text-foreground placeholder:text-muted-foreground/60 focus:border-ring focus:bg-background focus:outline-none focus:ring-2 focus:ring-ring/30"
107
+ placeholder={searchPlaceholder}
108
+ type="text"
109
+ value={searchQuery}
110
+ onChange={(e) => setSearchQuery(e.target.value)}
111
+ />
112
+ </div>
113
+
114
+ <div className="flex items-center gap-0.5">
115
+ <button
116
+ aria-label="Expand all folders"
117
+ className="rounded p-1 text-muted-foreground/70 transition-colors hover:bg-muted hover:text-foreground"
118
+ title="Expand all"
119
+ type="button"
120
+ onClick={handleExpandAll}
121
+ >
122
+ <ChevronsUpDown className="size-3.5" />
123
+ </button>
124
+ <button
125
+ aria-label="Collapse all folders"
126
+ className="rounded p-1 text-muted-foreground/70 transition-colors hover:bg-muted hover:text-foreground"
127
+ title="Collapse all"
128
+ type="button"
129
+ onClick={handleCollapseAll}
130
+ >
131
+ <ChevronsDownUp className="size-3.5" />
132
+ </button>
133
+ </div>
134
+ </div>
135
+ )}
136
+
137
+ <div className="flex-1 overflow-y-auto min-h-24">
138
+ {filteredNodes.length > 0 ? (
139
+ filteredNodes.map((node) => (
140
+ <FileTreeNodeComponent
141
+ allowCreate={allowCreate}
142
+ allowDelete={allowDelete}
143
+ expandedIds={searchQuery.trim() ? new Set(allDirIds) : expandedIds}
144
+ key={node.id}
145
+ node={node}
146
+ onCreateFile={onCreateFile}
147
+ onCreateFolder={onCreateFolder}
148
+ onDeleteNode={onDeleteNode}
149
+ onSelectNode={onSelectNode}
150
+ onToggleExpand={handleToggleExpand}
151
+ renderActions={renderNodeActions}
152
+ selectedNodeId={selectedNodeId}
153
+ />
154
+ ))
155
+ ) : (
156
+ <div className="flex items-center justify-center py-6 text-xs text-muted-foreground">
157
+ {emptyMessage}
158
+ </div>
159
+ )}
160
+ </div>
161
+ </div>
162
+ )
163
+ }
@@ -0,0 +1,5 @@
1
+ export { FileTree } from './file-tree'
2
+ export { FileTreeNodeComponent } from './file-tree-node'
3
+ export { FileTypeIcon, getFileExtension } from './file-icons'
4
+ export type { FileTreeNode, FileTreeProps } from './file-tree-types'
5
+ export type { FileTreeNodeProps } from './file-tree-node'
@@ -0,0 +1,169 @@
1
+ import { Filter, Plus, RotateCcw } from 'lucide-react'
2
+ import { Button } from '../../components/button'
3
+ import { cn } from '../../lib/utils'
4
+ import { FilterRow } from './filter-row'
5
+ import type { FilterBuilderProps, FilterCombinator, FilterRule } from './filter-types'
6
+
7
+ export function FilterBuilder({
8
+ allowClear = true,
9
+ ariaLabel = 'Filter builder',
10
+ className,
11
+ emptyMessage = 'No filter conditions applied. Add a rule to filter records.',
12
+ fields,
13
+ filter,
14
+ maxRules = 10,
15
+ onApply,
16
+ onChange,
17
+ onClear,
18
+ title = 'Filter rules',
19
+ }: FilterBuilderProps) {
20
+ function handleAddRule() {
21
+ if (filter.rules.length >= maxRules || fields.length === 0) return
22
+ const defaultField = fields[0]
23
+ const defaultOp =
24
+ defaultField.type === 'number'
25
+ ? 'equals'
26
+ : defaultField.type === 'select'
27
+ ? 'equals'
28
+ : defaultField.type === 'date'
29
+ ? 'equals'
30
+ : 'contains'
31
+
32
+ const newRule: FilterRule = {
33
+ fieldId: defaultField.id,
34
+ id: `rule-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`,
35
+ operator: defaultOp,
36
+ value: '',
37
+ }
38
+
39
+ onChange({
40
+ ...filter,
41
+ rules: [...filter.rules, newRule],
42
+ })
43
+ }
44
+
45
+ function handleUpdateRule(updatedRule: FilterRule) {
46
+ onChange({
47
+ ...filter,
48
+ rules: filter.rules.map((r) => (r.id === updatedRule.id ? updatedRule : r)),
49
+ })
50
+ }
51
+
52
+ function handleDeleteRule(ruleId: string) {
53
+ onChange({
54
+ ...filter,
55
+ rules: filter.rules.filter((r) => r.id !== ruleId),
56
+ })
57
+ }
58
+
59
+ function handleCombinatorToggle(combinator: FilterCombinator) {
60
+ onChange({
61
+ ...filter,
62
+ combinator,
63
+ })
64
+ }
65
+
66
+ function handleClear() {
67
+ onChange({
68
+ ...filter,
69
+ rules: [],
70
+ })
71
+ onClear?.()
72
+ }
73
+
74
+ return (
75
+ <div
76
+ aria-label={ariaLabel}
77
+ className={cn(
78
+ 'flex flex-col gap-3.5 rounded-2xl border border-border/80 bg-card p-4 text-card-foreground shadow-2xs',
79
+ className,
80
+ )}
81
+ >
82
+ {/* Header */}
83
+ <div className="flex flex-wrap items-center justify-between gap-2 border-b border-border/40 pb-3">
84
+ <div className="flex items-center gap-2">
85
+ <Filter className="size-4 text-muted-foreground" />
86
+ <h4 className="text-sm font-semibold tracking-tight text-foreground">{title}</h4>
87
+ <span className="rounded-full bg-muted px-2 py-0.5 text-[11px] font-medium text-muted-foreground">
88
+ {filter.rules.length}
89
+ {maxRules ? ` / ${maxRules}` : ''}
90
+ </span>
91
+ </div>
92
+
93
+ {filter.rules.length > 1 && (
94
+ <div className="flex items-center rounded-lg border border-border/80 bg-muted/40 p-0.5">
95
+ <button
96
+ className={cn(
97
+ 'rounded-md px-2.5 py-1 text-xs font-semibold transition-all',
98
+ filter.combinator === 'AND'
99
+ ? 'bg-background text-foreground shadow-2xs'
100
+ : 'text-muted-foreground hover:text-foreground',
101
+ )}
102
+ type="button"
103
+ onClick={() => handleCombinatorToggle('AND')}
104
+ >
105
+ Match ALL (AND)
106
+ </button>
107
+ <button
108
+ className={cn(
109
+ 'rounded-md px-2.5 py-1 text-xs font-semibold transition-all',
110
+ filter.combinator === 'OR'
111
+ ? 'bg-background text-foreground shadow-2xs'
112
+ : 'text-muted-foreground hover:text-foreground',
113
+ )}
114
+ type="button"
115
+ onClick={() => handleCombinatorToggle('OR')}
116
+ >
117
+ Match ANY (OR)
118
+ </button>
119
+ </div>
120
+ )}
121
+ </div>
122
+
123
+ {/* Rules list */}
124
+ <div className="flex flex-col gap-2 min-h-16">
125
+ {filter.rules.map((rule) => (
126
+ <FilterRow
127
+ fields={fields}
128
+ key={rule.id}
129
+ onDelete={handleDeleteRule}
130
+ onUpdate={handleUpdateRule}
131
+ rule={rule}
132
+ />
133
+ ))}
134
+
135
+ {filter.rules.length === 0 && (
136
+ <div className="flex items-center justify-center rounded-xl border border-dashed border-border/60 py-6 text-center text-xs text-muted-foreground">
137
+ {emptyMessage}
138
+ </div>
139
+ )}
140
+ </div>
141
+
142
+ {/* Footer controls */}
143
+ <div className="flex flex-wrap items-center justify-between gap-2 border-t border-border/40 pt-3">
144
+ <Button
145
+ disabled={filter.rules.length >= maxRules}
146
+ size="sm"
147
+ variant="outline"
148
+ onClick={handleAddRule}
149
+ >
150
+ <Plus className="size-3.5" /> Add condition
151
+ </Button>
152
+
153
+ <div className="flex items-center gap-2">
154
+ {allowClear && filter.rules.length > 0 && (
155
+ <Button size="sm" variant="ghost" onClick={handleClear}>
156
+ <RotateCcw className="size-3.5" /> Clear
157
+ </Button>
158
+ )}
159
+
160
+ {onApply && (
161
+ <Button size="sm" variant="default" onClick={() => onApply(filter)}>
162
+ Apply filter
163
+ </Button>
164
+ )}
165
+ </div>
166
+ </div>
167
+ </div>
168
+ )
169
+ }
@@ -0,0 +1,164 @@
1
+ import { Trash2 } from 'lucide-react'
2
+ import { cn } from '../../lib/utils'
3
+ import type { FilterFieldDefinition, FilterOperator, FilterRule } from './filter-types'
4
+
5
+ const operatorsByType: Record<string, { label: string; value: FilterOperator }[]> = {
6
+ text: [
7
+ { label: 'contains', value: 'contains' },
8
+ { label: 'does not contain', value: 'not_contains' },
9
+ { label: 'equals', value: 'equals' },
10
+ { label: 'starts with', value: 'starts_with' },
11
+ { label: 'ends with', value: 'ends_with' },
12
+ { label: 'is empty', value: 'is_empty' },
13
+ { label: 'is not empty', value: 'is_not_empty' },
14
+ ],
15
+ number: [
16
+ { label: '=', value: 'equals' },
17
+ { label: '≠', value: 'not_equals' },
18
+ { label: '>', value: 'greater_than' },
19
+ { label: '<', value: 'less_than' },
20
+ { label: '≥', value: 'greater_or_equal' },
21
+ { label: '≤', value: 'less_or_equal' },
22
+ { label: 'between', value: 'between' },
23
+ ],
24
+ date: [
25
+ { label: 'is on', value: 'equals' },
26
+ { label: 'is after', value: 'greater_than' },
27
+ { label: 'is before', value: 'less_than' },
28
+ { label: 'between', value: 'between' },
29
+ ],
30
+ select: [
31
+ { label: 'is', value: 'equals' },
32
+ { label: 'is not', value: 'not_equals' },
33
+ ],
34
+ boolean: [
35
+ { label: 'is true', value: 'is_true' },
36
+ { label: 'is false', value: 'is_false' },
37
+ ],
38
+ }
39
+
40
+ export function FilterRow({
41
+ fields,
42
+ onDelete,
43
+ onUpdate,
44
+ rule,
45
+ }: {
46
+ fields: readonly FilterFieldDefinition[]
47
+ onDelete: (id: string) => void
48
+ onUpdate: (updated: FilterRule) => void
49
+ rule: FilterRule
50
+ }) {
51
+ const currentField = fields.find((f) => f.id === rule.fieldId) || fields[0]
52
+ const fieldType = currentField?.type || 'text'
53
+ const operators = operatorsByType[fieldType] || operatorsByType.text
54
+
55
+ const isUnaryOperator =
56
+ rule.operator === 'is_empty' ||
57
+ rule.operator === 'is_not_empty' ||
58
+ rule.operator === 'is_true' ||
59
+ rule.operator === 'is_false'
60
+
61
+ const isBetween = rule.operator === 'between'
62
+
63
+ function handleFieldChange(newFieldId: string) {
64
+ const newField = fields.find((f) => f.id === newFieldId)
65
+ const newType = newField?.type || 'text'
66
+ const defaultOp = operatorsByType[newType]?.[0]?.value || 'contains'
67
+ onUpdate({
68
+ ...rule,
69
+ fieldId: newFieldId,
70
+ operator: defaultOp,
71
+ value: '',
72
+ valueTo: '',
73
+ })
74
+ }
75
+
76
+ return (
77
+ <div className="flex flex-wrap items-center gap-2 rounded-xl border border-border/60 bg-background/50 p-2.5 transition-colors hover:border-border">
78
+ {/* Field selector */}
79
+ <select
80
+ aria-label="Filter field"
81
+ className="h-8.5 rounded-lg border border-border/80 bg-background px-2.5 text-xs font-medium text-foreground focus:border-ring focus:outline-none focus:ring-2 focus:ring-ring/30"
82
+ value={rule.fieldId}
83
+ onChange={(e) => handleFieldChange(e.target.value)}
84
+ >
85
+ {fields.map((f) => (
86
+ <option key={f.id} value={f.id}>
87
+ {f.label}
88
+ </option>
89
+ ))}
90
+ </select>
91
+
92
+ {/* Operator selector */}
93
+ <select
94
+ aria-label="Filter operator"
95
+ className="h-8.5 rounded-lg border border-border/80 bg-background px-2.5 text-xs font-medium text-foreground focus:border-ring focus:outline-none focus:ring-2 focus:ring-ring/30"
96
+ value={rule.operator}
97
+ onChange={(e) => onUpdate({ ...rule, operator: e.target.value as FilterOperator })}
98
+ >
99
+ {operators.map((op) => (
100
+ <option key={op.value} value={op.value}>
101
+ {op.label}
102
+ </option>
103
+ ))}
104
+ </select>
105
+
106
+ {/* Dynamic value input */}
107
+ {!isUnaryOperator && (
108
+ <div className="flex flex-1 items-center gap-2 min-w-36">
109
+ {fieldType === 'select' && currentField.options ? (
110
+ <select
111
+ aria-label="Filter value"
112
+ className="h-8.5 w-full rounded-lg border border-border/80 bg-background px-2.5 text-xs text-foreground focus:border-ring focus:outline-none focus:ring-2 focus:ring-ring/30"
113
+ value={String(rule.value ?? '')}
114
+ onChange={(e) => onUpdate({ ...rule, value: e.target.value })}
115
+ >
116
+ <option value="">Select an option...</option>
117
+ {currentField.options.map((opt) => (
118
+ <option key={opt.value} value={opt.value}>
119
+ {opt.label}
120
+ </option>
121
+ ))}
122
+ </select>
123
+ ) : (
124
+ <input
125
+ aria-label="Filter value"
126
+ className={cn(
127
+ 'h-8.5 w-full rounded-lg border border-border/80 bg-background px-2.5 text-xs text-foreground placeholder:text-muted-foreground/60 focus:border-ring focus:outline-none focus:ring-2 focus:ring-ring/30',
128
+ isBetween && 'w-1/2',
129
+ )}
130
+ placeholder={currentField?.placeholder || 'Enter value...'}
131
+ type={fieldType === 'number' ? 'number' : fieldType === 'date' ? 'date' : 'text'}
132
+ value={String(rule.value ?? '')}
133
+ onChange={(e) => onUpdate({ ...rule, value: e.target.value })}
134
+ />
135
+ )}
136
+
137
+ {isBetween && (
138
+ <>
139
+ <span className="text-xs text-muted-foreground">and</span>
140
+ <input
141
+ aria-label="Filter value to"
142
+ className="h-8.5 w-1/2 rounded-lg border border-border/80 bg-background px-2.5 text-xs text-foreground placeholder:text-muted-foreground/60 focus:border-ring focus:outline-none focus:ring-2 focus:ring-ring/30"
143
+ placeholder="To..."
144
+ type={fieldType === 'number' ? 'number' : fieldType === 'date' ? 'date' : 'text'}
145
+ value={String(rule.valueTo ?? '')}
146
+ onChange={(e) => onUpdate({ ...rule, valueTo: e.target.value })}
147
+ />
148
+ </>
149
+ )}
150
+ </div>
151
+ )}
152
+
153
+ {/* Delete button */}
154
+ <button
155
+ aria-label="Remove filter rule"
156
+ className="size-8.5 shrink-0 rounded-lg p-2 text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
157
+ type="button"
158
+ onClick={() => onDelete(rule.id)}
159
+ >
160
+ <Trash2 className="size-4" />
161
+ </button>
162
+ </div>
163
+ )
164
+ }
@@ -0,0 +1,61 @@
1
+ export type FilterFieldType = 'text' | 'number' | 'date' | 'select' | 'boolean'
2
+
3
+ export type FilterOperator =
4
+ | 'equals'
5
+ | 'not_equals'
6
+ | 'contains'
7
+ | 'not_contains'
8
+ | 'starts_with'
9
+ | 'ends_with'
10
+ | 'greater_than'
11
+ | 'less_than'
12
+ | 'greater_or_equal'
13
+ | 'less_or_equal'
14
+ | 'between'
15
+ | 'is_true'
16
+ | 'is_false'
17
+ | 'is_empty'
18
+ | 'is_not_empty'
19
+ | 'in'
20
+
21
+ export interface FilterFieldOption {
22
+ label: string
23
+ value: string
24
+ }
25
+
26
+ export interface FilterFieldDefinition {
27
+ id: string
28
+ label: string
29
+ options?: readonly FilterFieldOption[]
30
+ placeholder?: string
31
+ type: FilterFieldType
32
+ }
33
+
34
+ export interface FilterRule {
35
+ fieldId: string
36
+ id: string
37
+ operator: FilterOperator
38
+ value?: unknown
39
+ valueTo?: unknown
40
+ }
41
+
42
+ export type FilterCombinator = 'AND' | 'OR'
43
+
44
+ export interface FilterGroup {
45
+ combinator: FilterCombinator
46
+ rules: readonly FilterRule[]
47
+ }
48
+
49
+ export interface FilterBuilderProps {
50
+ allowClear?: boolean
51
+ ariaLabel?: string
52
+ className?: string
53
+ emptyMessage?: string
54
+ fields: readonly FilterFieldDefinition[]
55
+ filter: FilterGroup
56
+ maxRules?: number
57
+ onApply?: (filter: FilterGroup) => void
58
+ onChange: (filter: FilterGroup) => void
59
+ onClear?: () => void
60
+ title?: string
61
+ }
@@ -0,0 +1,12 @@
1
+ export { FilterBuilder } from './filter-builder'
2
+ export { FilterRow } from './filter-row'
3
+ export type {
4
+ FilterBuilderProps,
5
+ FilterCombinator,
6
+ FilterFieldDefinition,
7
+ FilterFieldOption,
8
+ FilterFieldType,
9
+ FilterGroup,
10
+ FilterOperator,
11
+ FilterRule,
12
+ } from './filter-types'
@@ -0,0 +1,35 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ export type FooterLink = {
4
+ readonly badge?: string
5
+ readonly href: string
6
+ readonly label: string
7
+ }
8
+
9
+ export type FooterColumn = {
10
+ readonly links: readonly FooterLink[]
11
+ readonly title: string
12
+ }
13
+
14
+ export type SiteFooterProps = {
15
+ readonly brand: {
16
+ readonly description?: string
17
+ readonly logo?: ReactNode
18
+ readonly title: string
19
+ }
20
+ readonly className?: string
21
+ readonly columns?: readonly FooterColumn[]
22
+ readonly copyright?: string
23
+ readonly newsletter?: {
24
+ readonly description?: string
25
+ readonly onSubscribe?: (email: string) => void
26
+ readonly placeholder?: string
27
+ readonly title?: string
28
+ }
29
+ readonly paymentBadges?: readonly string[]
30
+ readonly socialLinks?: readonly {
31
+ readonly href: string
32
+ readonly icon: ReactNode
33
+ readonly label: string
34
+ }[]
35
+ }
@@ -0,0 +1,2 @@
1
+ export * from './site-footer'
2
+ export * from './footer-types'