@pylonsync/create-pylon 0.3.343 → 0.3.345

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 (422) hide show
  1. package/bin/create-pylon.js +35 -0
  2. package/package.json +1 -1
  3. package/templates/agency/components/ui/badge.tsx +37 -0
  4. package/templates/agency/components/ui/dialog.tsx +110 -0
  5. package/templates/agency/components/ui/input.tsx +30 -0
  6. package/templates/agency/components/ui/label.tsx +21 -0
  7. package/templates/agency/components/ui/select.tsx +39 -0
  8. package/templates/agency/components/ui/table.tsx +124 -0
  9. package/templates/agency/components/ui/textarea.tsx +24 -0
  10. package/templates/agency/package.json +1 -0
  11. package/templates/ai-chat/components/ui/badge.tsx +37 -0
  12. package/templates/ai-chat/components/ui/button.tsx +56 -0
  13. package/templates/ai-chat/components/ui/card.tsx +90 -0
  14. package/templates/ai-chat/components/ui/dialog.tsx +110 -0
  15. package/templates/ai-chat/components/ui/input.tsx +30 -0
  16. package/templates/ai-chat/components/ui/label.tsx +21 -0
  17. package/templates/ai-chat/components/ui/select.tsx +39 -0
  18. package/templates/ai-chat/components/ui/table.tsx +124 -0
  19. package/templates/ai-chat/components/ui/textarea.tsx +24 -0
  20. package/templates/ai-chat/package.json +1 -0
  21. package/templates/ai-studio/components/ui/badge.tsx +37 -0
  22. package/templates/ai-studio/components/ui/button.tsx +56 -0
  23. package/templates/ai-studio/components/ui/card.tsx +90 -0
  24. package/templates/ai-studio/components/ui/dialog.tsx +110 -0
  25. package/templates/ai-studio/components/ui/input.tsx +30 -0
  26. package/templates/ai-studio/components/ui/label.tsx +21 -0
  27. package/templates/ai-studio/components/ui/select.tsx +39 -0
  28. package/templates/ai-studio/components/ui/table.tsx +124 -0
  29. package/templates/ai-studio/components/ui/textarea.tsx +24 -0
  30. package/templates/ai-studio/package.json +1 -0
  31. package/templates/barebones/components/ui/badge.tsx +37 -0
  32. package/templates/barebones/components/ui/dialog.tsx +110 -0
  33. package/templates/barebones/components/ui/input.tsx +30 -0
  34. package/templates/barebones/components/ui/label.tsx +21 -0
  35. package/templates/barebones/components/ui/select.tsx +39 -0
  36. package/templates/barebones/components/ui/table.tsx +124 -0
  37. package/templates/barebones/components/ui/textarea.tsx +24 -0
  38. package/templates/barebones/package.json +1 -0
  39. package/templates/chat/components/ui/badge.tsx +37 -0
  40. package/templates/chat/components/ui/dialog.tsx +110 -0
  41. package/templates/chat/components/ui/input.tsx +30 -0
  42. package/templates/chat/components/ui/label.tsx +21 -0
  43. package/templates/chat/components/ui/select.tsx +39 -0
  44. package/templates/chat/components/ui/table.tsx +124 -0
  45. package/templates/chat/components/ui/textarea.tsx +24 -0
  46. package/templates/chat/package.json +1 -0
  47. package/templates/consumer/components/ui/badge.tsx +37 -0
  48. package/templates/consumer/components/ui/dialog.tsx +110 -0
  49. package/templates/consumer/components/ui/input.tsx +30 -0
  50. package/templates/consumer/components/ui/label.tsx +21 -0
  51. package/templates/consumer/components/ui/select.tsx +39 -0
  52. package/templates/consumer/components/ui/table.tsx +124 -0
  53. package/templates/consumer/components/ui/textarea.tsx +24 -0
  54. package/templates/consumer/package.json +1 -0
  55. package/templates/creator/components/ui/badge.tsx +37 -0
  56. package/templates/creator/components/ui/dialog.tsx +110 -0
  57. package/templates/creator/components/ui/input.tsx +30 -0
  58. package/templates/creator/components/ui/label.tsx +21 -0
  59. package/templates/creator/components/ui/select.tsx +39 -0
  60. package/templates/creator/components/ui/table.tsx +124 -0
  61. package/templates/creator/components/ui/textarea.tsx +24 -0
  62. package/templates/creator/package.json +1 -0
  63. package/templates/crm/AGENTS.md +116 -0
  64. package/templates/crm/CLAUDE.md +7 -0
  65. package/templates/crm/README.md +96 -0
  66. package/templates/crm/app/companies/companies-view.tsx +129 -0
  67. package/templates/crm/app/companies/page.tsx +17 -0
  68. package/templates/crm/app/contacts/contacts-view.tsx +139 -0
  69. package/templates/crm/app/contacts/page.tsx +17 -0
  70. package/templates/crm/app/deals/[id]/deal-view.tsx +189 -0
  71. package/templates/crm/app/deals/[id]/page.tsx +23 -0
  72. package/templates/crm/app/error.tsx +43 -0
  73. package/templates/crm/app/globals.css +201 -0
  74. package/templates/crm/app/layout.tsx +31 -0
  75. package/templates/crm/app/login/auth-form.tsx +122 -0
  76. package/templates/crm/app/login/page.tsx +38 -0
  77. package/templates/crm/app/not-found.tsx +29 -0
  78. package/templates/crm/app/page.tsx +38 -0
  79. package/templates/crm/app/pipeline-view.tsx +99 -0
  80. package/templates/crm/app/robots.ts +12 -0
  81. package/templates/crm/app/sitemap.ts +27 -0
  82. package/templates/crm/app/workspace.tsx +231 -0
  83. package/templates/crm/app.ts +171 -0
  84. package/templates/crm/bunfig +4 -0
  85. package/templates/crm/components/activity-timeline.tsx +133 -0
  86. package/templates/crm/components/avatar.tsx +40 -0
  87. package/templates/crm/components/board-skeleton.tsx +44 -0
  88. package/templates/crm/components/command-palette.tsx +162 -0
  89. package/templates/crm/components/data-table.tsx +90 -0
  90. package/templates/crm/components/deal-card.tsx +91 -0
  91. package/templates/crm/components/deal-dialog.tsx +169 -0
  92. package/templates/crm/components/empty-state.tsx +43 -0
  93. package/templates/crm/components/kbd.tsx +23 -0
  94. package/templates/crm/components/metrics-bar.tsx +46 -0
  95. package/templates/crm/components/page-header.tsx +36 -0
  96. package/templates/crm/components/pipeline-board.tsx +105 -0
  97. package/templates/crm/components/record-dialog.tsx +126 -0
  98. package/templates/crm/components/sidebar.tsx +112 -0
  99. package/templates/crm/components/stage-badge.tsx +33 -0
  100. package/templates/crm/components/ui/badge.tsx +37 -0
  101. package/templates/crm/components/ui/button.tsx +56 -0
  102. package/templates/crm/components/ui/card.tsx +90 -0
  103. package/templates/crm/components/ui/dialog.tsx +110 -0
  104. package/templates/crm/components/ui/input.tsx +30 -0
  105. package/templates/crm/components/ui/label.tsx +21 -0
  106. package/templates/crm/components/ui/select.tsx +39 -0
  107. package/templates/crm/components/ui/table.tsx +124 -0
  108. package/templates/crm/components/ui/textarea.tsx +24 -0
  109. package/templates/crm/components.json +20 -0
  110. package/templates/crm/functions/createDeal.ts +59 -0
  111. package/templates/crm/functions/logActivity.ts +49 -0
  112. package/templates/crm/functions/moveDeal.ts +53 -0
  113. package/templates/crm/functions/seedWorkspace.ts +63 -0
  114. package/templates/crm/gitignore +10 -0
  115. package/templates/crm/lib/pipeline.ts +221 -0
  116. package/templates/crm/lib/search.ts +78 -0
  117. package/templates/crm/lib/seed.ts +130 -0
  118. package/templates/crm/lib/utils.ts +10 -0
  119. package/templates/crm/package.json +39 -0
  120. package/templates/crm/tests/components.test.tsx +245 -0
  121. package/templates/crm/tests/pipeline.test.ts +219 -0
  122. package/templates/crm/tests/search.test.ts +83 -0
  123. package/templates/crm/tests/setup.ts +5 -0
  124. package/templates/crm/tsconfig.json +33 -0
  125. package/templates/default/components/ui/badge.tsx +37 -0
  126. package/templates/default/components/ui/dialog.tsx +110 -0
  127. package/templates/default/components/ui/input.tsx +30 -0
  128. package/templates/default/components/ui/label.tsx +21 -0
  129. package/templates/default/components/ui/select.tsx +39 -0
  130. package/templates/default/components/ui/table.tsx +124 -0
  131. package/templates/default/components/ui/textarea.tsx +24 -0
  132. package/templates/default/package.json +1 -0
  133. package/templates/directory/components/ui/badge.tsx +37 -0
  134. package/templates/directory/components/ui/dialog.tsx +110 -0
  135. package/templates/directory/components/ui/input.tsx +30 -0
  136. package/templates/directory/components/ui/label.tsx +21 -0
  137. package/templates/directory/components/ui/select.tsx +39 -0
  138. package/templates/directory/components/ui/table.tsx +124 -0
  139. package/templates/directory/components/ui/textarea.tsx +24 -0
  140. package/templates/directory/package.json +1 -0
  141. package/templates/helpdesk/AGENTS.md +116 -0
  142. package/templates/helpdesk/CLAUDE.md +7 -0
  143. package/templates/helpdesk/README.md +90 -0
  144. package/templates/helpdesk/app/customers/customers-view.tsx +140 -0
  145. package/templates/helpdesk/app/customers/page.tsx +17 -0
  146. package/templates/helpdesk/app/error.tsx +43 -0
  147. package/templates/helpdesk/app/globals.css +201 -0
  148. package/templates/helpdesk/app/inbox-view.tsx +147 -0
  149. package/templates/helpdesk/app/layout.tsx +31 -0
  150. package/templates/helpdesk/app/login/auth-form.tsx +122 -0
  151. package/templates/helpdesk/app/login/page.tsx +38 -0
  152. package/templates/helpdesk/app/not-found.tsx +29 -0
  153. package/templates/helpdesk/app/page.tsx +38 -0
  154. package/templates/helpdesk/app/robots.ts +12 -0
  155. package/templates/helpdesk/app/sitemap.ts +27 -0
  156. package/templates/helpdesk/app/tickets/[id]/page.tsx +23 -0
  157. package/templates/helpdesk/app/tickets/[id]/ticket-view.tsx +164 -0
  158. package/templates/helpdesk/app/workspace.tsx +210 -0
  159. package/templates/helpdesk/app.ts +142 -0
  160. package/templates/helpdesk/bunfig +4 -0
  161. package/templates/helpdesk/components/avatar.tsx +40 -0
  162. package/templates/helpdesk/components/command-palette.tsx +162 -0
  163. package/templates/helpdesk/components/data-table.tsx +90 -0
  164. package/templates/helpdesk/components/empty-state.tsx +43 -0
  165. package/templates/helpdesk/components/kbd.tsx +23 -0
  166. package/templates/helpdesk/components/new-ticket-dialog.tsx +147 -0
  167. package/templates/helpdesk/components/page-header.tsx +36 -0
  168. package/templates/helpdesk/components/priority-badge.tsx +61 -0
  169. package/templates/helpdesk/components/record-dialog.tsx +126 -0
  170. package/templates/helpdesk/components/sidebar.tsx +111 -0
  171. package/templates/helpdesk/components/sla-indicator.tsx +47 -0
  172. package/templates/helpdesk/components/thread.tsx +143 -0
  173. package/templates/helpdesk/components/ticket-list.tsx +78 -0
  174. package/templates/helpdesk/components/ui/badge.tsx +37 -0
  175. package/templates/helpdesk/components/ui/button.tsx +56 -0
  176. package/templates/helpdesk/components/ui/card.tsx +90 -0
  177. package/templates/helpdesk/components/ui/dialog.tsx +110 -0
  178. package/templates/helpdesk/components/ui/input.tsx +30 -0
  179. package/templates/helpdesk/components/ui/label.tsx +21 -0
  180. package/templates/helpdesk/components/ui/select.tsx +39 -0
  181. package/templates/helpdesk/components/ui/table.tsx +124 -0
  182. package/templates/helpdesk/components/ui/textarea.tsx +24 -0
  183. package/templates/helpdesk/components.json +20 -0
  184. package/templates/helpdesk/functions/createTicket.ts +62 -0
  185. package/templates/helpdesk/functions/replyToTicket.ts +55 -0
  186. package/templates/helpdesk/functions/seedWorkspace.ts +52 -0
  187. package/templates/helpdesk/functions/setTicketState.ts +58 -0
  188. package/templates/helpdesk/gitignore +10 -0
  189. package/templates/helpdesk/lib/format.ts +53 -0
  190. package/templates/helpdesk/lib/search.ts +78 -0
  191. package/templates/helpdesk/lib/seed.ts +185 -0
  192. package/templates/helpdesk/lib/tickets.ts +184 -0
  193. package/templates/helpdesk/lib/utils.ts +10 -0
  194. package/templates/helpdesk/package.json +39 -0
  195. package/templates/helpdesk/tests/components.test.tsx +202 -0
  196. package/templates/helpdesk/tests/search.test.ts +83 -0
  197. package/templates/helpdesk/tests/setup.ts +5 -0
  198. package/templates/helpdesk/tests/tickets.test.ts +215 -0
  199. package/templates/helpdesk/tsconfig.json +33 -0
  200. package/templates/inventory/AGENTS.md +116 -0
  201. package/templates/inventory/CLAUDE.md +7 -0
  202. package/templates/inventory/README.md +103 -0
  203. package/templates/inventory/app/error.tsx +43 -0
  204. package/templates/inventory/app/globals.css +201 -0
  205. package/templates/inventory/app/layout.tsx +31 -0
  206. package/templates/inventory/app/login/auth-form.tsx +122 -0
  207. package/templates/inventory/app/login/page.tsx +38 -0
  208. package/templates/inventory/app/movements/movements-view.tsx +103 -0
  209. package/templates/inventory/app/movements/page.tsx +17 -0
  210. package/templates/inventory/app/not-found.tsx +29 -0
  211. package/templates/inventory/app/page.tsx +34 -0
  212. package/templates/inventory/app/products/[id]/page.tsx +23 -0
  213. package/templates/inventory/app/products/[id]/product-view.tsx +178 -0
  214. package/templates/inventory/app/products-view.tsx +213 -0
  215. package/templates/inventory/app/robots.ts +12 -0
  216. package/templates/inventory/app/sitemap.ts +27 -0
  217. package/templates/inventory/app/workspace.tsx +172 -0
  218. package/templates/inventory/app.ts +154 -0
  219. package/templates/inventory/bunfig +4 -0
  220. package/templates/inventory/components/avatar.tsx +40 -0
  221. package/templates/inventory/components/command-palette.tsx +162 -0
  222. package/templates/inventory/components/data-table.tsx +90 -0
  223. package/templates/inventory/components/empty-state.tsx +43 -0
  224. package/templates/inventory/components/kbd.tsx +23 -0
  225. package/templates/inventory/components/movement-dialog.tsx +182 -0
  226. package/templates/inventory/components/page-header.tsx +36 -0
  227. package/templates/inventory/components/record-dialog.tsx +126 -0
  228. package/templates/inventory/components/sidebar.tsx +111 -0
  229. package/templates/inventory/components/stock-level.tsx +47 -0
  230. package/templates/inventory/components/summary-bar.tsx +56 -0
  231. package/templates/inventory/components/ui/badge.tsx +37 -0
  232. package/templates/inventory/components/ui/button.tsx +56 -0
  233. package/templates/inventory/components/ui/card.tsx +90 -0
  234. package/templates/inventory/components/ui/dialog.tsx +110 -0
  235. package/templates/inventory/components/ui/input.tsx +30 -0
  236. package/templates/inventory/components/ui/label.tsx +21 -0
  237. package/templates/inventory/components/ui/select.tsx +39 -0
  238. package/templates/inventory/components/ui/table.tsx +124 -0
  239. package/templates/inventory/components/ui/textarea.tsx +24 -0
  240. package/templates/inventory/components.json +20 -0
  241. package/templates/inventory/functions/recordMovement.ts +72 -0
  242. package/templates/inventory/functions/seedWorkspace.ts +40 -0
  243. package/templates/inventory/gitignore +10 -0
  244. package/templates/inventory/lib/format.ts +53 -0
  245. package/templates/inventory/lib/search.ts +78 -0
  246. package/templates/inventory/lib/seed.ts +102 -0
  247. package/templates/inventory/lib/stock.ts +207 -0
  248. package/templates/inventory/lib/utils.ts +10 -0
  249. package/templates/inventory/package.json +39 -0
  250. package/templates/inventory/tests/components.test.tsx +155 -0
  251. package/templates/inventory/tests/search.test.ts +83 -0
  252. package/templates/inventory/tests/setup.ts +5 -0
  253. package/templates/inventory/tests/stock.test.ts +255 -0
  254. package/templates/inventory/tsconfig.json +33 -0
  255. package/templates/invoices/AGENTS.md +116 -0
  256. package/templates/invoices/CLAUDE.md +7 -0
  257. package/templates/invoices/README.md +99 -0
  258. package/templates/invoices/app/clients/clients-view.tsx +130 -0
  259. package/templates/invoices/app/clients/page.tsx +17 -0
  260. package/templates/invoices/app/error.tsx +43 -0
  261. package/templates/invoices/app/globals.css +201 -0
  262. package/templates/invoices/app/invoices/[id]/invoice-view.tsx +253 -0
  263. package/templates/invoices/app/invoices/[id]/page.tsx +23 -0
  264. package/templates/invoices/app/invoices-view.tsx +183 -0
  265. package/templates/invoices/app/layout.tsx +31 -0
  266. package/templates/invoices/app/login/auth-form.tsx +122 -0
  267. package/templates/invoices/app/login/page.tsx +38 -0
  268. package/templates/invoices/app/not-found.tsx +29 -0
  269. package/templates/invoices/app/page.tsx +36 -0
  270. package/templates/invoices/app/robots.ts +12 -0
  271. package/templates/invoices/app/sitemap.ts +27 -0
  272. package/templates/invoices/app/workspace.tsx +174 -0
  273. package/templates/invoices/app.ts +169 -0
  274. package/templates/invoices/bunfig +4 -0
  275. package/templates/invoices/components/avatar.tsx +40 -0
  276. package/templates/invoices/components/command-palette.tsx +162 -0
  277. package/templates/invoices/components/data-table.tsx +90 -0
  278. package/templates/invoices/components/empty-state.tsx +43 -0
  279. package/templates/invoices/components/kbd.tsx +23 -0
  280. package/templates/invoices/components/line-items.tsx +172 -0
  281. package/templates/invoices/components/page-header.tsx +36 -0
  282. package/templates/invoices/components/payment-dialog.tsx +132 -0
  283. package/templates/invoices/components/record-dialog.tsx +126 -0
  284. package/templates/invoices/components/sidebar.tsx +111 -0
  285. package/templates/invoices/components/status-badge.tsx +35 -0
  286. package/templates/invoices/components/summary-bar.tsx +65 -0
  287. package/templates/invoices/components/totals-panel.tsx +70 -0
  288. package/templates/invoices/components/ui/badge.tsx +37 -0
  289. package/templates/invoices/components/ui/button.tsx +56 -0
  290. package/templates/invoices/components/ui/card.tsx +90 -0
  291. package/templates/invoices/components/ui/dialog.tsx +110 -0
  292. package/templates/invoices/components/ui/input.tsx +30 -0
  293. package/templates/invoices/components/ui/label.tsx +21 -0
  294. package/templates/invoices/components/ui/select.tsx +39 -0
  295. package/templates/invoices/components/ui/table.tsx +124 -0
  296. package/templates/invoices/components/ui/textarea.tsx +24 -0
  297. package/templates/invoices/components.json +20 -0
  298. package/templates/invoices/functions/createInvoice.ts +49 -0
  299. package/templates/invoices/functions/recordPayment.ts +72 -0
  300. package/templates/invoices/functions/seedWorkspace.ts +57 -0
  301. package/templates/invoices/functions/setInvoiceStatus.ts +35 -0
  302. package/templates/invoices/gitignore +10 -0
  303. package/templates/invoices/lib/billing.ts +259 -0
  304. package/templates/invoices/lib/format.ts +53 -0
  305. package/templates/invoices/lib/search.ts +78 -0
  306. package/templates/invoices/lib/seed.ts +190 -0
  307. package/templates/invoices/lib/utils.ts +10 -0
  308. package/templates/invoices/package.json +39 -0
  309. package/templates/invoices/tests/billing.test.ts +283 -0
  310. package/templates/invoices/tests/components.test.tsx +172 -0
  311. package/templates/invoices/tests/search.test.ts +83 -0
  312. package/templates/invoices/tests/setup.ts +5 -0
  313. package/templates/invoices/tsconfig.json +33 -0
  314. package/templates/local-service/components/ui/badge.tsx +37 -0
  315. package/templates/local-service/components/ui/dialog.tsx +110 -0
  316. package/templates/local-service/components/ui/input.tsx +30 -0
  317. package/templates/local-service/components/ui/label.tsx +21 -0
  318. package/templates/local-service/components/ui/select.tsx +39 -0
  319. package/templates/local-service/components/ui/table.tsx +124 -0
  320. package/templates/local-service/components/ui/textarea.tsx +24 -0
  321. package/templates/local-service/package.json +1 -0
  322. package/templates/marketplace/components/ui/badge.tsx +37 -0
  323. package/templates/marketplace/components/ui/button.tsx +56 -0
  324. package/templates/marketplace/components/ui/card.tsx +90 -0
  325. package/templates/marketplace/components/ui/dialog.tsx +110 -0
  326. package/templates/marketplace/components/ui/input.tsx +30 -0
  327. package/templates/marketplace/components/ui/label.tsx +21 -0
  328. package/templates/marketplace/components/ui/select.tsx +39 -0
  329. package/templates/marketplace/components/ui/table.tsx +124 -0
  330. package/templates/marketplace/components/ui/textarea.tsx +24 -0
  331. package/templates/marketplace/lib/utils.ts +10 -0
  332. package/templates/marketplace/package.json +1 -0
  333. package/templates/projects/AGENTS.md +116 -0
  334. package/templates/projects/CLAUDE.md +7 -0
  335. package/templates/projects/README.md +96 -0
  336. package/templates/projects/app/clients/clients-view.tsx +126 -0
  337. package/templates/projects/app/clients/page.tsx +17 -0
  338. package/templates/projects/app/error.tsx +43 -0
  339. package/templates/projects/app/globals.css +201 -0
  340. package/templates/projects/app/layout.tsx +31 -0
  341. package/templates/projects/app/login/auth-form.tsx +122 -0
  342. package/templates/projects/app/login/page.tsx +38 -0
  343. package/templates/projects/app/not-found.tsx +29 -0
  344. package/templates/projects/app/page.tsx +30 -0
  345. package/templates/projects/app/projects/[id]/page.tsx +23 -0
  346. package/templates/projects/app/projects/[id]/project-view.tsx +179 -0
  347. package/templates/projects/app/projects-view.tsx +181 -0
  348. package/templates/projects/app/robots.ts +12 -0
  349. package/templates/projects/app/sitemap.ts +27 -0
  350. package/templates/projects/app/workspace.tsx +190 -0
  351. package/templates/projects/app.ts +186 -0
  352. package/templates/projects/bunfig +4 -0
  353. package/templates/projects/components/avatar.tsx +40 -0
  354. package/templates/projects/components/board-skeleton.tsx +37 -0
  355. package/templates/projects/components/budget-bar.tsx +61 -0
  356. package/templates/projects/components/command-palette.tsx +162 -0
  357. package/templates/projects/components/data-table.tsx +90 -0
  358. package/templates/projects/components/empty-state.tsx +43 -0
  359. package/templates/projects/components/kbd.tsx +23 -0
  360. package/templates/projects/components/page-header.tsx +36 -0
  361. package/templates/projects/components/record-dialog.tsx +126 -0
  362. package/templates/projects/components/sidebar.tsx +111 -0
  363. package/templates/projects/components/task-board.tsx +109 -0
  364. package/templates/projects/components/task-card.tsx +77 -0
  365. package/templates/projects/components/time-dialog.tsx +114 -0
  366. package/templates/projects/components/ui/badge.tsx +37 -0
  367. package/templates/projects/components/ui/button.tsx +56 -0
  368. package/templates/projects/components/ui/card.tsx +90 -0
  369. package/templates/projects/components/ui/dialog.tsx +110 -0
  370. package/templates/projects/components/ui/input.tsx +30 -0
  371. package/templates/projects/components/ui/label.tsx +21 -0
  372. package/templates/projects/components/ui/select.tsx +39 -0
  373. package/templates/projects/components/ui/table.tsx +124 -0
  374. package/templates/projects/components/ui/textarea.tsx +24 -0
  375. package/templates/projects/components.json +20 -0
  376. package/templates/projects/functions/logTime.ts +49 -0
  377. package/templates/projects/functions/moveTask.ts +40 -0
  378. package/templates/projects/functions/seedWorkspace.ts +59 -0
  379. package/templates/projects/gitignore +10 -0
  380. package/templates/projects/lib/format.ts +53 -0
  381. package/templates/projects/lib/search.ts +78 -0
  382. package/templates/projects/lib/seed.ts +140 -0
  383. package/templates/projects/lib/utils.ts +10 -0
  384. package/templates/projects/lib/work.ts +233 -0
  385. package/templates/projects/package.json +39 -0
  386. package/templates/projects/tests/components.test.tsx +200 -0
  387. package/templates/projects/tests/search.test.ts +83 -0
  388. package/templates/projects/tests/setup.ts +5 -0
  389. package/templates/projects/tests/work.test.ts +238 -0
  390. package/templates/projects/tsconfig.json +33 -0
  391. package/templates/restaurant/components/ui/badge.tsx +37 -0
  392. package/templates/restaurant/components/ui/dialog.tsx +110 -0
  393. package/templates/restaurant/components/ui/input.tsx +30 -0
  394. package/templates/restaurant/components/ui/label.tsx +21 -0
  395. package/templates/restaurant/components/ui/select.tsx +39 -0
  396. package/templates/restaurant/components/ui/table.tsx +124 -0
  397. package/templates/restaurant/components/ui/textarea.tsx +24 -0
  398. package/templates/restaurant/package.json +1 -0
  399. package/templates/shop/components/ui/badge.tsx +37 -0
  400. package/templates/shop/components/ui/dialog.tsx +110 -0
  401. package/templates/shop/components/ui/input.tsx +30 -0
  402. package/templates/shop/components/ui/label.tsx +21 -0
  403. package/templates/shop/components/ui/select.tsx +39 -0
  404. package/templates/shop/components/ui/table.tsx +124 -0
  405. package/templates/shop/components/ui/textarea.tsx +24 -0
  406. package/templates/shop/package.json +1 -0
  407. package/templates/todo/components/ui/badge.tsx +37 -0
  408. package/templates/todo/components/ui/dialog.tsx +110 -0
  409. package/templates/todo/components/ui/input.tsx +30 -0
  410. package/templates/todo/components/ui/label.tsx +21 -0
  411. package/templates/todo/components/ui/select.tsx +39 -0
  412. package/templates/todo/components/ui/table.tsx +124 -0
  413. package/templates/todo/components/ui/textarea.tsx +24 -0
  414. package/templates/todo/package.json +1 -0
  415. package/templates/waitlist/components/ui/badge.tsx +37 -0
  416. package/templates/waitlist/components/ui/dialog.tsx +110 -0
  417. package/templates/waitlist/components/ui/input.tsx +30 -0
  418. package/templates/waitlist/components/ui/label.tsx +21 -0
  419. package/templates/waitlist/components/ui/select.tsx +39 -0
  420. package/templates/waitlist/components/ui/table.tsx +124 -0
  421. package/templates/waitlist/components/ui/textarea.tsx +24 -0
  422. package/templates/waitlist/package.json +1 -0
@@ -0,0 +1,162 @@
1
+ import React, { useEffect, useMemo, useRef, useState } from "react";
2
+ import { Building2, Plus, Search, User } from "lucide-react";
3
+ import { cn } from "@/lib/utils";
4
+ import { Kbd } from "@/components/kbd";
5
+ import { moveSelection, searchItems, type SearchItem } from "@/lib/search";
6
+
7
+ const TYPE_ICON: Record<SearchItem["type"], React.ReactNode> = {
8
+ deal: <Search />,
9
+ company: <Building2 />,
10
+ contact: <User />,
11
+ };
12
+
13
+ export interface CommandAction {
14
+ id: string;
15
+ label: string;
16
+ run: () => void;
17
+ }
18
+
19
+ /**
20
+ * ⌘K. Searches the synced replica in memory, so results land as you type with
21
+ * no request per keystroke — see lib/search.ts for the ranking.
22
+ *
23
+ * Presentational: items and actions come in as props, selection is delegated
24
+ * through onSelect. Keyboard handling lives here because it belongs to the
25
+ * widget, not to the page that opened it.
26
+ */
27
+ export function CommandPalette({
28
+ open,
29
+ items,
30
+ actions = [],
31
+ onClose,
32
+ onSelect,
33
+ }: {
34
+ open: boolean;
35
+ items: SearchItem[];
36
+ actions?: CommandAction[];
37
+ onClose: () => void;
38
+ onSelect: (item: SearchItem) => void;
39
+ }) {
40
+ const [query, setQuery] = useState("");
41
+ const [index, setIndex] = useState(0);
42
+ const inputRef = useRef<HTMLInputElement | null>(null);
43
+
44
+ const results = useMemo(() => searchItems(items, query), [items, query]);
45
+ const shownActions = query.trim() ? [] : actions;
46
+ const rows = useMemo(
47
+ () => [
48
+ ...shownActions.map((a) => ({ kind: "action" as const, action: a })),
49
+ ...results.map((item) => ({ kind: "item" as const, item })),
50
+ ],
51
+ [shownActions, results],
52
+ );
53
+
54
+ useEffect(() => {
55
+ if (!open) return;
56
+ setQuery("");
57
+ setIndex(0);
58
+ // Focus after paint so the dialog is mounted and the caret lands.
59
+ const id = requestAnimationFrame(() => inputRef.current?.focus());
60
+ return () => cancelAnimationFrame(id);
61
+ }, [open]);
62
+
63
+ useEffect(() => {
64
+ setIndex(0);
65
+ }, [query]);
66
+
67
+ if (!open) return null;
68
+
69
+ function commit(at: number) {
70
+ const row = rows[at];
71
+ if (!row) return;
72
+ if (row.kind === "action") row.action.run();
73
+ else onSelect(row.item);
74
+ onClose();
75
+ }
76
+
77
+ return (
78
+ <div
79
+ className="fixed inset-0 z-50 flex items-start justify-center bg-black/50 pt-[12vh]"
80
+ // Clicking the backdrop dismisses; clicks inside the panel must not.
81
+ onMouseDown={(event) => {
82
+ if (event.target === event.currentTarget) onClose();
83
+ }}
84
+ >
85
+ <div
86
+ role="dialog"
87
+ aria-modal="true"
88
+ aria-label="Search"
89
+ className="w-full max-w-lg overflow-hidden rounded-xl border border-border bg-popover shadow-2xl"
90
+ onKeyDown={(event) => {
91
+ if (event.key === "Escape") {
92
+ event.preventDefault();
93
+ onClose();
94
+ } else if (event.key === "ArrowDown") {
95
+ event.preventDefault();
96
+ setIndex((i) => moveSelection(i, 1, rows.length));
97
+ } else if (event.key === "ArrowUp") {
98
+ event.preventDefault();
99
+ setIndex((i) => moveSelection(i, -1, rows.length));
100
+ } else if (event.key === "Enter") {
101
+ event.preventDefault();
102
+ commit(index);
103
+ }
104
+ }}
105
+ >
106
+ <div className="flex items-center gap-2 border-b border-border px-3">
107
+ <Search className="size-4 shrink-0 text-muted-foreground" />
108
+ <input
109
+ ref={inputRef}
110
+ value={query}
111
+ onChange={(event) => setQuery(event.target.value)}
112
+ placeholder="Search deals, companies, contacts…"
113
+ aria-label="Search deals, companies and contacts"
114
+ className="h-11 flex-1 bg-transparent text-[13px] outline-none placeholder:text-muted-foreground"
115
+ />
116
+ <Kbd>esc</Kbd>
117
+ </div>
118
+
119
+ <div className="max-h-80 overflow-y-auto p-1.5">
120
+ {rows.length === 0 ? (
121
+ <p className="px-3 py-6 text-center text-[12px] text-muted-foreground">
122
+ No matches for “{query}”
123
+ </p>
124
+ ) : (
125
+ rows.map((row, at) => (
126
+ <button
127
+ key={row.kind === "action" ? row.action.id : row.item.id}
128
+ type="button"
129
+ // Pointer hover moves selection so mouse and keyboard agree.
130
+ onMouseMove={() => setIndex(at)}
131
+ onClick={() => commit(at)}
132
+ aria-selected={at === index}
133
+ className={cn(
134
+ "flex w-full items-center gap-2.5 rounded-md px-2.5 py-2 text-left text-[13px] transition-colors",
135
+ "[&_svg]:size-3.5 [&_svg]:shrink-0 [&_svg]:text-muted-foreground",
136
+ at === index ? "bg-surface-2" : "hover:bg-surface-2/60",
137
+ )}
138
+ >
139
+ {row.kind === "action" ? (
140
+ <>
141
+ <Plus />
142
+ <span className="flex-1 truncate">{row.action.label}</span>
143
+ </>
144
+ ) : (
145
+ <>
146
+ {TYPE_ICON[row.item.type]}
147
+ <span className="flex-1 truncate">{row.item.title}</span>
148
+ {row.item.subtitle ? (
149
+ <span className="truncate text-[12px] text-muted-foreground">
150
+ {row.item.subtitle}
151
+ </span>
152
+ ) : null}
153
+ </>
154
+ )}
155
+ </button>
156
+ ))
157
+ )}
158
+ </div>
159
+ </div>
160
+ </div>
161
+ );
162
+ }
@@ -0,0 +1,90 @@
1
+ import React from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ export interface ColumnDef<Row> {
5
+ key: string;
6
+ header: string;
7
+ /** Cell content. Return a string and it's rendered as-is. */
8
+ cell: (row: Row) => React.ReactNode;
9
+ /** Right-align and tabular-figure a numeric column. */
10
+ numeric?: boolean;
11
+ className?: string;
12
+ }
13
+
14
+ /**
15
+ * A dense list view. Rows are 36px so a screenful is a screenful of data, and
16
+ * the header sticks while the body scrolls.
17
+ *
18
+ * Generic and presentational — every view here uses the same one, which is why
19
+ * companies and contacts look like the same product.
20
+ */
21
+ export function DataTable<Row extends { id: string }>({
22
+ rows,
23
+ columns,
24
+ onRowClick,
25
+ empty,
26
+ }: {
27
+ rows: Row[];
28
+ columns: ColumnDef<Row>[];
29
+ onRowClick?: (row: Row) => void;
30
+ empty?: React.ReactNode;
31
+ }) {
32
+ if (rows.length === 0 && empty) return <>{empty}</>;
33
+
34
+ return (
35
+ <div className="min-h-0 flex-1 overflow-auto">
36
+ <table className="w-full border-collapse text-[13px]">
37
+ <thead className="sticky top-0 z-10 bg-background">
38
+ <tr className="hairline">
39
+ {columns.map((column) => (
40
+ <th
41
+ key={column.key}
42
+ scope="col"
43
+ className={cn(
44
+ "h-8 px-4 text-left text-[11px] font-medium text-muted-foreground",
45
+ column.numeric && "text-right",
46
+ column.className,
47
+ )}
48
+ >
49
+ {column.header}
50
+ </th>
51
+ ))}
52
+ </tr>
53
+ </thead>
54
+ <tbody>
55
+ {rows.map((row) => (
56
+ <tr
57
+ key={row.id}
58
+ onClick={onRowClick ? () => onRowClick(row) : undefined}
59
+ tabIndex={onRowClick ? 0 : undefined}
60
+ onKeyDown={
61
+ onRowClick
62
+ ? (event) => {
63
+ if (event.key === "Enter") onRowClick(row);
64
+ }
65
+ : undefined
66
+ }
67
+ className={cn(
68
+ "border-b border-border/60 transition-colors",
69
+ onRowClick && "cursor-pointer hover:bg-surface-1",
70
+ )}
71
+ >
72
+ {columns.map((column) => (
73
+ <td
74
+ key={column.key}
75
+ className={cn(
76
+ "h-9 max-w-0 truncate px-4",
77
+ column.numeric && "tabular text-right",
78
+ column.className,
79
+ )}
80
+ >
81
+ {column.cell(row)}
82
+ </td>
83
+ ))}
84
+ </tr>
85
+ ))}
86
+ </tbody>
87
+ </table>
88
+ </div>
89
+ );
90
+ }
@@ -0,0 +1,91 @@
1
+ import React from "react";
2
+ import { cn } from "@/lib/utils";
3
+ import { Avatar } from "@/components/avatar";
4
+ import { daysUntil, money, type Deal } from "@/lib/pipeline";
5
+
6
+ /**
7
+ * One deal on the board. Draggable, but never the source of truth for where it
8
+ * sits — the parent decides that from the synced row, so a teammate's move
9
+ * lands here through the same path as your own.
10
+ */
11
+ export function DealCard({
12
+ deal,
13
+ company,
14
+ owner,
15
+ onOpen,
16
+ onDragStart,
17
+ onDragEnd,
18
+ dragging,
19
+ }: {
20
+ deal: Deal;
21
+ company?: string | null;
22
+ owner?: string | null;
23
+ onOpen: (id: string) => void;
24
+ onDragStart: (id: string) => void;
25
+ onDragEnd: () => void;
26
+ dragging?: boolean;
27
+ }) {
28
+ const days = daysUntil(deal.closeDate);
29
+ // Only surface a date when it's actionable: overdue, or inside a week.
30
+ const overdue = days !== null && days < 0;
31
+ const soon = days !== null && days >= 0 && days <= 7;
32
+
33
+ return (
34
+ <article
35
+ draggable
36
+ onDragStart={(event) => {
37
+ // setData is required for Firefox to start a drag at all.
38
+ event.dataTransfer.setData("text/plain", deal.id);
39
+ event.dataTransfer.effectAllowed = "move";
40
+ onDragStart(deal.id);
41
+ }}
42
+ onDragEnd={onDragEnd}
43
+ onClick={() => onOpen(deal.id)}
44
+ onKeyDown={(event) => {
45
+ if (event.key === "Enter" || event.key === " ") {
46
+ event.preventDefault();
47
+ onOpen(deal.id);
48
+ }
49
+ }}
50
+ role="button"
51
+ tabIndex={0}
52
+ aria-label={`${deal.title}${company ? `, ${company}` : ""}`}
53
+ className={cn(
54
+ "group cursor-pointer rounded-lg border border-border bg-card p-2.5 transition-all",
55
+ "hover:border-ring/40 focus-visible:border-ring",
56
+ dragging && "opacity-40",
57
+ )}
58
+ >
59
+ <div className="flex items-start justify-between gap-2">
60
+ <p className="line-clamp-2 flex-1 text-[13px] font-medium leading-snug">
61
+ {deal.title}
62
+ </p>
63
+ <span className="tabular shrink-0 text-[12px] font-medium text-muted-foreground">
64
+ {money(deal.value)}
65
+ </span>
66
+ </div>
67
+
68
+ {company ? (
69
+ <p className="mt-1 truncate text-[12px] text-muted-foreground">{company}</p>
70
+ ) : null}
71
+
72
+ <div className="mt-2.5 flex items-center justify-between gap-2">
73
+ {owner ? <Avatar name={owner} size="sm" /> : <span />}
74
+ {overdue || soon ? (
75
+ <span
76
+ className={cn(
77
+ "tabular text-[11px]",
78
+ overdue ? "text-destructive" : "text-muted-foreground",
79
+ )}
80
+ >
81
+ {overdue
82
+ ? `${Math.abs(days as number)}d overdue`
83
+ : days === 0
84
+ ? "Today"
85
+ : `${days}d`}
86
+ </span>
87
+ ) : null}
88
+ </div>
89
+ </article>
90
+ );
91
+ }
@@ -0,0 +1,169 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import {
3
+ Dialog,
4
+ DialogContent,
5
+ DialogFooter,
6
+ DialogHeader,
7
+ DialogTitle,
8
+ } from "@/components/ui/dialog";
9
+ import { Button } from "@/components/ui/button";
10
+ import { Input } from "@/components/ui/input";
11
+ import { Label } from "@/components/ui/label";
12
+ import { Select } from "@/components/ui/select";
13
+ import { BOARD_STAGES } from "@/lib/pipeline";
14
+
15
+ export interface DealDraft {
16
+ title: string;
17
+ companyId: string;
18
+ value: number;
19
+ stage: string;
20
+ closeDate: string;
21
+ }
22
+
23
+ /**
24
+ * New deal. A dialog rather than an inline row: creating is deliberate and
25
+ * occasional, and a permanent form above the board would push the pipeline —
26
+ * the thing you came to look at — off the screen.
27
+ */
28
+ export function DealDialog({
29
+ open,
30
+ companies,
31
+ defaultStage = "lead",
32
+ onOpenChange,
33
+ onCreate,
34
+ }: {
35
+ open: boolean;
36
+ companies: Array<{ id: string; name: string }>;
37
+ defaultStage?: string;
38
+ onOpenChange: (open: boolean) => void;
39
+ onCreate: (draft: DealDraft) => void | Promise<void>;
40
+ }) {
41
+ const [title, setTitle] = useState("");
42
+ const [companyId, setCompanyId] = useState("");
43
+ const [value, setValue] = useState("");
44
+ const [stage, setStage] = useState(defaultStage);
45
+ const [closeDate, setCloseDate] = useState("");
46
+ const [busy, setBusy] = useState(false);
47
+
48
+ // Reset on each open so a cancelled draft doesn't reappear next time.
49
+ useEffect(() => {
50
+ if (!open) return;
51
+ setTitle("");
52
+ setCompanyId("");
53
+ setValue("");
54
+ setStage(defaultStage);
55
+ setCloseDate("");
56
+ setBusy(false);
57
+ }, [open, defaultStage]);
58
+
59
+ const canSubmit = title.trim().length > 0 && !busy;
60
+
61
+ async function submit(event: React.FormEvent) {
62
+ event.preventDefault();
63
+ if (!canSubmit) return;
64
+ setBusy(true);
65
+ try {
66
+ await onCreate({
67
+ title: title.trim(),
68
+ companyId,
69
+ value: Number(value) || 0,
70
+ stage,
71
+ closeDate,
72
+ });
73
+ onOpenChange(false);
74
+ } finally {
75
+ setBusy(false);
76
+ }
77
+ }
78
+
79
+ return (
80
+ <Dialog open={open} onOpenChange={onOpenChange}>
81
+ <DialogContent>
82
+ <DialogHeader>
83
+ <DialogTitle>New deal</DialogTitle>
84
+ </DialogHeader>
85
+ <form onSubmit={submit} className="grid gap-4">
86
+ <div className="space-y-1.5">
87
+ <Label htmlFor="deal-title">Title</Label>
88
+ <Input
89
+ id="deal-title"
90
+ autoFocus
91
+ value={title}
92
+ placeholder="Fleet dispatch rollout"
93
+ onChange={(event) => setTitle(event.target.value)}
94
+ />
95
+ </div>
96
+
97
+ <div className="space-y-1.5">
98
+ <Label htmlFor="deal-company">Company</Label>
99
+ <Select
100
+ id="deal-company"
101
+ value={companyId}
102
+ onChange={(event) => setCompanyId(event.target.value)}
103
+ >
104
+ <option value="">—</option>
105
+ {companies.map((company) => (
106
+ <option key={company.id} value={company.id}>
107
+ {company.name}
108
+ </option>
109
+ ))}
110
+ </Select>
111
+ </div>
112
+
113
+ <div className="grid grid-cols-2 gap-3">
114
+ <div className="space-y-1.5">
115
+ <Label htmlFor="deal-value">Value</Label>
116
+ <Input
117
+ id="deal-value"
118
+ type="number"
119
+ min="0"
120
+ step="100"
121
+ inputMode="numeric"
122
+ value={value}
123
+ placeholder="10000"
124
+ onChange={(event) => setValue(event.target.value)}
125
+ />
126
+ </div>
127
+ <div className="space-y-1.5">
128
+ <Label htmlFor="deal-stage">Stage</Label>
129
+ <Select
130
+ id="deal-stage"
131
+ value={stage}
132
+ onChange={(event) => setStage(event.target.value)}
133
+ >
134
+ {BOARD_STAGES.map((s) => (
135
+ <option key={s.id} value={s.id}>
136
+ {s.label}
137
+ </option>
138
+ ))}
139
+ </Select>
140
+ </div>
141
+ </div>
142
+
143
+ <div className="space-y-1.5">
144
+ <Label htmlFor="deal-close">Expected close</Label>
145
+ <Input
146
+ id="deal-close"
147
+ type="date"
148
+ value={closeDate}
149
+ onChange={(event) => setCloseDate(event.target.value)}
150
+ />
151
+ </div>
152
+
153
+ <DialogFooter>
154
+ <Button
155
+ type="button"
156
+ variant="ghost"
157
+ onClick={() => onOpenChange(false)}
158
+ >
159
+ Cancel
160
+ </Button>
161
+ <Button type="submit" disabled={!canSubmit}>
162
+ {busy ? "Creating…" : "Create deal"}
163
+ </Button>
164
+ </DialogFooter>
165
+ </form>
166
+ </DialogContent>
167
+ </Dialog>
168
+ );
169
+ }
@@ -0,0 +1,43 @@
1
+ import React from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ /**
5
+ * An empty state that says what this screen is for and offers the one action
6
+ * that fills it. "No records found" tells a new user nothing and gives them
7
+ * nowhere to go.
8
+ */
9
+ export function EmptyState({
10
+ icon,
11
+ title,
12
+ description,
13
+ action,
14
+ className,
15
+ }: {
16
+ icon?: React.ReactNode;
17
+ title: string;
18
+ description?: string;
19
+ action?: React.ReactNode;
20
+ className?: string;
21
+ }) {
22
+ return (
23
+ <div
24
+ className={cn(
25
+ "flex flex-col items-center justify-center px-6 py-16 text-center",
26
+ className,
27
+ )}
28
+ >
29
+ {icon ? (
30
+ <div className="mb-3 flex size-9 items-center justify-center rounded-lg bg-surface-2 text-muted-foreground [&_svg]:size-4">
31
+ {icon}
32
+ </div>
33
+ ) : null}
34
+ <p className="text-[13px] font-medium">{title}</p>
35
+ {description ? (
36
+ <p className="mt-1 max-w-sm text-[12px] leading-5 text-muted-foreground">
37
+ {description}
38
+ </p>
39
+ ) : null}
40
+ {action ? <div className="mt-4">{action}</div> : null}
41
+ </div>
42
+ );
43
+ }
@@ -0,0 +1,23 @@
1
+ import React from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ /** A keyboard hint. Present in the UI because the shortcuts are real. */
5
+ export function Kbd({
6
+ children,
7
+ className,
8
+ }: {
9
+ children: React.ReactNode;
10
+ className?: string;
11
+ }) {
12
+ return (
13
+ <kbd
14
+ className={cn(
15
+ "inline-flex h-5 min-w-5 items-center justify-center rounded border border-border bg-surface-2 px-1.5",
16
+ "font-sans text-[10px] font-medium text-muted-foreground",
17
+ className,
18
+ )}
19
+ >
20
+ {children}
21
+ </kbd>
22
+ );
23
+ }
@@ -0,0 +1,46 @@
1
+ import React from "react";
2
+ import { metrics, money, percent, type Deal } from "@/lib/pipeline";
3
+
4
+ /**
5
+ * The four numbers a sales lead actually looks at. Derived, never stored — see
6
+ * lib/pipeline.ts, where the arithmetic is unit-tested.
7
+ */
8
+ export function MetricsBar({ deals }: { deals: Deal[] }) {
9
+ const m = metrics(deals);
10
+ return (
11
+ <dl className="grid grid-cols-2 gap-px border-b border-border bg-border sm:grid-cols-4">
12
+ <Metric
13
+ label="Open pipeline"
14
+ value={money(m.open)}
15
+ hint={`${m.openCount} deal${m.openCount === 1 ? "" : "s"}`}
16
+ />
17
+ <Metric
18
+ label="Weighted"
19
+ value={money(m.weighted)}
20
+ hint="by stage probability"
21
+ />
22
+ <Metric label="Won" value={money(m.won)} hint="closed" />
23
+ <Metric label="Win rate" value={percent(m.winRate)} hint="of closed deals" />
24
+ </dl>
25
+ );
26
+ }
27
+
28
+ function Metric({
29
+ label,
30
+ value,
31
+ hint,
32
+ }: {
33
+ label: string;
34
+ value: string;
35
+ hint: string;
36
+ }) {
37
+ return (
38
+ <div className="bg-background px-4 py-3">
39
+ <dt className="text-[11px] text-muted-foreground">{label}</dt>
40
+ <dd className="tabular mt-0.5 text-[18px] font-semibold tracking-tight">
41
+ {value}
42
+ </dd>
43
+ <p className="mt-0.5 text-[11px] text-muted-foreground">{hint}</p>
44
+ </div>
45
+ );
46
+ }
@@ -0,0 +1,36 @@
1
+ import React from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ /**
5
+ * The bar at the top of every view: title on the left, actions on the right.
6
+ * Fixed height so switching pages doesn't shift the content below it.
7
+ */
8
+ export function PageHeader({
9
+ title,
10
+ count,
11
+ children,
12
+ className,
13
+ }: {
14
+ title: string;
15
+ /** Shown beside the title — the number of rows in view. */
16
+ count?: number;
17
+ children?: React.ReactNode;
18
+ className?: string;
19
+ }) {
20
+ return (
21
+ <header
22
+ className={cn(
23
+ "flex h-12 shrink-0 items-center justify-between gap-3 border-b border-border px-4",
24
+ className,
25
+ )}
26
+ >
27
+ <div className="flex min-w-0 items-baseline gap-2">
28
+ <h1 className="truncate text-[13px] font-semibold">{title}</h1>
29
+ {typeof count === "number" ? (
30
+ <span className="tabular text-[12px] text-muted-foreground">{count}</span>
31
+ ) : null}
32
+ </div>
33
+ <div className="flex items-center gap-2">{children}</div>
34
+ </header>
35
+ );
36
+ }