@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,78 @@
1
+ // Client-side search powering the ⌘K palette.
2
+ //
3
+ // A small team's CRM fits in the synced replica, so search runs locally against
4
+ // data already in memory: results appear as you type, with no request per
5
+ // keystroke. Swap in `db.useSearch` (Pylon's FTS) when a workspace outgrows
6
+ // that — the component boundary doesn't change, only this file.
7
+
8
+ export interface SearchItem {
9
+ id: string;
10
+ type: "deal" | "company" | "contact";
11
+ title: string;
12
+ /** Second line: the company for a contact, the domain for a company. */
13
+ subtitle?: string;
14
+ href: string;
15
+ /** Extra text matched but not displayed — an email, say. */
16
+ keywords?: string;
17
+ }
18
+
19
+ export interface Scored extends SearchItem {
20
+ score: number;
21
+ }
22
+
23
+ /**
24
+ * Rank items against a query. Prefix matches beat word-boundary matches, which
25
+ * beat substring matches; ties break on the shorter title, so "Acme" ranks
26
+ * above "Acme Corporation Holdings" for the query "acme".
27
+ */
28
+ export function searchItems(
29
+ items: SearchItem[],
30
+ query: string,
31
+ limit = 8,
32
+ ): SearchItem[] {
33
+ const q = query.trim().toLowerCase();
34
+ if (!q) return items.slice(0, limit);
35
+
36
+ const scored: Scored[] = [];
37
+ for (const item of items) {
38
+ const score = scoreItem(item, q);
39
+ if (score > 0) scored.push({ ...item, score });
40
+ }
41
+ scored.sort(
42
+ (a, b) => b.score - a.score || a.title.length - b.title.length ||
43
+ a.title.localeCompare(b.title),
44
+ );
45
+ return scored.slice(0, limit);
46
+ }
47
+
48
+ function scoreItem(item: SearchItem, q: string): number {
49
+ const title = item.title.toLowerCase();
50
+ if (title.startsWith(q)) return 100;
51
+ if (wordStart(title, q)) return 80;
52
+ if (title.includes(q)) return 60;
53
+
54
+ const haystack = `${item.subtitle ?? ""} ${item.keywords ?? ""}`.toLowerCase();
55
+ if (wordStart(haystack, q)) return 40;
56
+ if (haystack.includes(q)) return 20;
57
+ return 0;
58
+ }
59
+
60
+ function wordStart(haystack: string, needle: string): boolean {
61
+ let from = 0;
62
+ for (;;) {
63
+ const at = haystack.indexOf(needle, from);
64
+ if (at === -1) return false;
65
+ if (at === 0 || /[\s._@/-]/.test(haystack[at - 1])) return true;
66
+ from = at + 1;
67
+ }
68
+ }
69
+
70
+ /** Move through a list with arrow keys, wrapping at both ends. */
71
+ export function moveSelection(
72
+ current: number,
73
+ delta: number,
74
+ length: number,
75
+ ): number {
76
+ if (length === 0) return 0;
77
+ return (current + delta + length) % length;
78
+ }
@@ -0,0 +1,140 @@
1
+ // Demo data for a brand-new delivery workspace.
2
+ //
3
+ // An empty project list shows nothing about boards, budgets, or time. The first
4
+ // sign-in seeds two live projects — one comfortably inside its budget, one over
5
+ // it — so the budget states are visible rather than theoretical.
6
+
7
+ export interface SeedClient {
8
+ key: string;
9
+ name: string;
10
+ email: string;
11
+ }
12
+
13
+ export interface SeedProject {
14
+ key: string;
15
+ client: string;
16
+ name: string;
17
+ status: string;
18
+ /** Agreed budget in hours. */
19
+ budgetHours: number;
20
+ hourlyRate: number;
21
+ dueInDays: number;
22
+ }
23
+
24
+ export interface SeedTask {
25
+ key: string;
26
+ project: string;
27
+ title: string;
28
+ status: string;
29
+ estimateHours: number;
30
+ /** Hours logged against it; each becomes one or more entries. */
31
+ loggedHours: number[];
32
+ }
33
+
34
+ export const SEED_CLIENTS: SeedClient[] = [
35
+ { key: "northwind", name: "Northwind Logistics", email: "dana@northwind.co" },
36
+ { key: "hallmark", name: "Hallmark Dental", email: "priya@hallmarkdental.com" },
37
+ ];
38
+
39
+ export const SEED_PROJECTS: SeedProject[] = [
40
+ {
41
+ key: "dispatch",
42
+ client: "northwind",
43
+ name: "Dispatch portal",
44
+ status: "active",
45
+ budgetHours: 80,
46
+ hourlyRate: 165,
47
+ dueInDays: 24,
48
+ },
49
+ // Over budget: 40h agreed, ~46h logged.
50
+ {
51
+ key: "intake",
52
+ client: "hallmark",
53
+ name: "Patient intake rebuild",
54
+ status: "active",
55
+ budgetHours: 40,
56
+ hourlyRate: 150,
57
+ dueInDays: 9,
58
+ },
59
+ ];
60
+
61
+ export const SEED_TASKS: SeedTask[] = [
62
+ { key: "d-auth", project: "dispatch", title: "SSO with their identity provider", status: "done", estimateHours: 8, loggedHours: [3, 4.5] },
63
+ { key: "d-model", project: "dispatch", title: "Route + stop data model", status: "done", estimateHours: 6, loggedHours: [5] },
64
+ { key: "d-board", project: "dispatch", title: "Dispatcher board UI", status: "doing", estimateHours: 20, loggedHours: [6, 4] },
65
+ { key: "d-mobile", project: "dispatch", title: "Driver mobile view", status: "review", estimateHours: 12, loggedHours: [9] },
66
+ { key: "d-report", project: "dispatch", title: "Weekly utilisation report", status: "todo", estimateHours: 6, loggedHours: [] },
67
+ { key: "d-import", project: "dispatch", title: "Import legacy stop history", status: "todo", estimateHours: 10, loggedHours: [] },
68
+
69
+ { key: "i-forms", project: "intake", title: "Form builder", status: "done", estimateHours: 16, loggedHours: [8, 7, 4] },
70
+ { key: "i-sig", project: "intake", title: "Consent signatures", status: "done", estimateHours: 8, loggedHours: [6, 5] },
71
+ { key: "i-sync", project: "intake", title: "Sync to practice software", status: "doing", estimateHours: 12, loggedHours: [9, 7] },
72
+ { key: "i-train", project: "intake", title: "Staff training session", status: "todo", estimateHours: 4, loggedHours: [] },
73
+ ];
74
+
75
+ export interface ShapedSeed {
76
+ clients: Array<{ key: string; row: Record<string, unknown> }>;
77
+ projects: Array<{ key: string; client: string; row: Record<string, unknown> }>;
78
+ tasks: Array<{ key: string; project: string; row: Record<string, unknown> }>;
79
+ entries: Array<{ task: string; project: string; row: Record<string, unknown> }>;
80
+ }
81
+
82
+ export function shapeSeed(now: number = Date.now()): ShapedSeed {
83
+ const at = (days: number) => new Date(now + days * 86_400_000).toISOString();
84
+
85
+ const tasks: ShapedSeed["tasks"] = [];
86
+ const entries: ShapedSeed["entries"] = [];
87
+ const positions = new Map<string, number>();
88
+
89
+ SEED_TASKS.forEach((task, index) => {
90
+ const columnKey = `${task.project}:${task.status}`;
91
+ const position = positions.get(columnKey) ?? 0;
92
+ positions.set(columnKey, position + 1);
93
+
94
+ tasks.push({
95
+ key: task.key,
96
+ project: task.project,
97
+ row: {
98
+ title: task.title,
99
+ status: task.status,
100
+ estimateMinutes: Math.round(task.estimateHours * 60),
101
+ position,
102
+ createdAt: at(-30 + index),
103
+ updatedAt: at(-2),
104
+ },
105
+ });
106
+
107
+ task.loggedHours.forEach((hours, entryIndex) => {
108
+ entries.push({
109
+ task: task.key,
110
+ project: task.project,
111
+ row: {
112
+ minutes: Math.round(hours * 60),
113
+ note: null,
114
+ spentOn: at(-14 + entryIndex * 2),
115
+ },
116
+ });
117
+ });
118
+ });
119
+
120
+ return {
121
+ clients: SEED_CLIENTS.map((c) => ({
122
+ key: c.key,
123
+ row: { name: c.name, email: c.email, createdAt: at(-90) },
124
+ })),
125
+ projects: SEED_PROJECTS.map((p) => ({
126
+ key: p.key,
127
+ client: p.client,
128
+ row: {
129
+ name: p.name,
130
+ status: p.status,
131
+ budgetMinutes: Math.round(p.budgetHours * 60),
132
+ hourlyRateCents: Math.round(p.hourlyRate * 100),
133
+ dueDate: at(p.dueInDays),
134
+ createdAt: at(-45),
135
+ },
136
+ })),
137
+ tasks,
138
+ entries,
139
+ };
140
+ }
@@ -0,0 +1,10 @@
1
+ import { type ClassValue, clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ // `cn` — the shadcn class merger. clsx resolves conditional/array class
5
+ // inputs; tailwind-merge then dedupes conflicting Tailwind utilities so
6
+ // the last one wins (e.g. `cn("px-2", "px-4")` → "px-4"). Every shadcn
7
+ // component routes its className through this.
8
+ export function cn(...inputs: ClassValue[]) {
9
+ return twMerge(clsx(inputs));
10
+ }
@@ -0,0 +1,233 @@
1
+ // Projects, tasks, and time.
2
+ //
3
+ // TIME IS A LEDGER, like stock in the inventory template: a task's logged hours
4
+ // are the sum of its time entries, never a running total on the task. Two people
5
+ // logging against the same task at once is normal, and a mutable total loses one
6
+ // of them. It also means every hour on an invoice traces to who logged it and
7
+ // when — which is the question a client asks.
8
+ //
9
+ // Minutes are integers. Hours as floats produce 7.999999 on a timesheet, and
10
+ // nobody wants to explain that.
11
+ //
12
+ // Pure: no React, no `db`.
13
+
14
+ export interface Status {
15
+ id: string;
16
+ label: string;
17
+ /** Done and cancelled leave the active board. */
18
+ terminal?: boolean;
19
+ }
20
+
21
+ export const TASK_STATUSES: Status[] = [
22
+ { id: "todo", label: "To do" },
23
+ { id: "doing", label: "In progress" },
24
+ { id: "review", label: "Review" },
25
+ { id: "done", label: "Done", terminal: true },
26
+ ];
27
+
28
+ export const PROJECT_STATUSES: Status[] = [
29
+ { id: "active", label: "Active" },
30
+ { id: "paused", label: "Paused" },
31
+ { id: "complete", label: "Complete", terminal: true },
32
+ ];
33
+
34
+ export interface Project {
35
+ id: string;
36
+ name: string;
37
+ clientId?: string | null;
38
+ status: string;
39
+ /** Agreed budget in whole minutes, 0 for none. */
40
+ budgetMinutes?: number | null;
41
+ hourlyRateCents?: number | null;
42
+ dueDate?: string | null;
43
+ createdAt?: string | null;
44
+ }
45
+
46
+ export interface Task {
47
+ id: string;
48
+ projectId: string;
49
+ title: string;
50
+ status: string;
51
+ assigneeId?: string | null;
52
+ /** What we thought it would take, in minutes. */
53
+ estimateMinutes?: number | null;
54
+ position?: number | null;
55
+ createdAt?: string | null;
56
+ }
57
+
58
+ export interface TimeEntry {
59
+ id: string;
60
+ taskId: string;
61
+ projectId: string;
62
+ minutes: number;
63
+ note?: string | null;
64
+ userId?: string | null;
65
+ spentOn?: string | null;
66
+ }
67
+
68
+ export function taskStatusById(id: string): Status | undefined {
69
+ return TASK_STATUSES.find((s) => s.id === id);
70
+ }
71
+
72
+ export function isValidTaskStatus(id: string): boolean {
73
+ return TASK_STATUSES.some((s) => s.id === id);
74
+ }
75
+
76
+ export function isValidProjectStatus(id: string): boolean {
77
+ return PROJECT_STATUSES.some((s) => s.id === id);
78
+ }
79
+
80
+ export function isOpen(task: { status: string }): boolean {
81
+ return !taskStatusById(task.status)?.terminal;
82
+ }
83
+
84
+ export interface Column {
85
+ status: Status;
86
+ tasks: Task[];
87
+ }
88
+
89
+ /** Group a project's tasks into board columns, in their manual order. */
90
+ export function groupByStatus(tasks: Task[]): Column[] {
91
+ return TASK_STATUSES.map((status) => ({
92
+ status,
93
+ tasks: tasks
94
+ .filter((task) => task.status === status.id)
95
+ .sort(
96
+ (a, b) =>
97
+ (Number(a.position) || 0) - (Number(b.position) || 0) ||
98
+ (a.createdAt ?? "").localeCompare(b.createdAt ?? ""),
99
+ ),
100
+ }));
101
+ }
102
+
103
+ /** Minutes logged against one task. */
104
+ export function minutesForTask(taskId: string, entries: TimeEntry[]): number {
105
+ let total = 0;
106
+ for (const entry of entries) {
107
+ if (entry.taskId !== taskId) continue;
108
+ const minutes = Number(entry.minutes);
109
+ if (Number.isFinite(minutes)) total += Math.trunc(minutes);
110
+ }
111
+ return total;
112
+ }
113
+
114
+ /**
115
+ * Minutes per task in one pass — a board calling minutesForTask per card would
116
+ * be O(tasks × entries).
117
+ */
118
+ export function minutesByTask(entries: TimeEntry[]): Map<string, number> {
119
+ const totals = new Map<string, number>();
120
+ for (const entry of entries) {
121
+ const minutes = Number(entry.minutes);
122
+ if (!Number.isFinite(minutes)) continue;
123
+ totals.set(entry.taskId, (totals.get(entry.taskId) ?? 0) + Math.trunc(minutes));
124
+ }
125
+ return totals;
126
+ }
127
+
128
+ export function minutesForProject(projectId: string, entries: TimeEntry[]): number {
129
+ let total = 0;
130
+ for (const entry of entries) {
131
+ if (entry.projectId !== projectId) continue;
132
+ const minutes = Number(entry.minutes);
133
+ if (Number.isFinite(minutes)) total += Math.trunc(minutes);
134
+ }
135
+ return total;
136
+ }
137
+
138
+ export interface Progress {
139
+ total: number;
140
+ done: number;
141
+ /** 0–1, or null when there's nothing to measure. */
142
+ ratio: number | null;
143
+ }
144
+
145
+ /**
146
+ * Progress by task COUNT, not by logged time.
147
+ *
148
+ * Time spent measures effort, not completion — a project can be 90% through its
149
+ * budget and 20% done, and conflating them is how a status report lies.
150
+ * `budgetState` reports the other half separately.
151
+ */
152
+ export function progress(tasks: Task[]): Progress {
153
+ const total = tasks.length;
154
+ const done = tasks.filter((task) => !isOpen(task)).length;
155
+ return { total, done, ratio: total === 0 ? null : done / total };
156
+ }
157
+
158
+ export type BudgetState = "none" | "ok" | "near" | "over";
159
+
160
+ /** Where a project sits against its agreed budget. */
161
+ export function budgetState(
162
+ budgetMinutes: number | null | undefined,
163
+ loggedMinutes: number,
164
+ ): BudgetState {
165
+ const budget = Number(budgetMinutes);
166
+ if (!Number.isFinite(budget) || budget <= 0) return "none";
167
+ if (loggedMinutes > budget) return "over";
168
+ // 85% is where you can still do something about it.
169
+ if (loggedMinutes >= budget * 0.85) return "near";
170
+ return "ok";
171
+ }
172
+
173
+ /** What the logged time is worth at the project's rate. */
174
+ export function billableCents(
175
+ minutes: number,
176
+ hourlyRateCents: number | null | undefined,
177
+ ): number {
178
+ const rate = Number(hourlyRateCents) || 0;
179
+ return Math.round((minutes * rate) / 60);
180
+ }
181
+
182
+ /** "7h 30m" / "45m" / "0m" — the format a timesheet is read in. */
183
+ export function duration(minutes: number | null | undefined): string {
184
+ const value = Math.trunc(Number(minutes) || 0);
185
+ const sign = value < 0 ? "-" : "";
186
+ const abs = Math.abs(value);
187
+ const hours = Math.floor(abs / 60);
188
+ const rest = abs % 60;
189
+ if (hours === 0) return `${sign}${rest}m`;
190
+ if (rest === 0) return `${sign}${hours}h`;
191
+ return `${sign}${hours}h ${rest}m`;
192
+ }
193
+
194
+ export function money(cents: number | null | undefined): string {
195
+ const value = Number(cents) || 0;
196
+ const sign = value < 0 ? "-" : "";
197
+ const abs = Math.abs(value);
198
+ const dollars = Math.floor(abs / 100).toLocaleString("en-US");
199
+ const rest = String(abs % 100).padStart(2, "0");
200
+ return `${sign}$${dollars}.${rest}`;
201
+ }
202
+
203
+ /**
204
+ * Parse typed time into minutes. Accepts "90", "1.5h", "1h30", "1h 30m", "45m".
205
+ *
206
+ * People write time however they think of it, and rejecting "1h30" because it
207
+ * isn't "90" is the kind of friction that stops time being logged at all —
208
+ * which costs far more than a lenient parser.
209
+ */
210
+ export function parseDuration(input: string): number | null {
211
+ const text = input.trim().toLowerCase().replace(/\s+/g, "");
212
+ if (!text) return null;
213
+
214
+ // "1h30" / "1h30m" / "2h" / "45m"
215
+ const hm = text.match(/^(\d+(?:\.\d+)?)h(?:(\d+)m?)?$/);
216
+ if (hm) {
217
+ const hours = Number(hm[1]);
218
+ const mins = hm[2] ? Number(hm[2]) : 0;
219
+ if (!Number.isFinite(hours) || !Number.isFinite(mins) || mins >= 60) return null;
220
+ return Math.round(hours * 60 + mins);
221
+ }
222
+
223
+ const m = text.match(/^(\d+)m$/);
224
+ if (m) return Number(m[1]);
225
+
226
+ // A bare number is minutes — the unit a timesheet field is usually in.
227
+ if (/^\d+(\.\d+)?$/.test(text)) {
228
+ const value = Number(text);
229
+ return Number.isFinite(value) ? Math.round(value) : null;
230
+ }
231
+
232
+ return null;
233
+ }
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "__APP_NAME_KEBAB__",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "pylon dev",
8
+ "deploy": "pylon deploy",
9
+ "check": "tsc --noEmit",
10
+ "test": "pylon test"
11
+ },
12
+ "dependencies": {
13
+ "@pylonsync/react": "^__PYLON_VERSION__",
14
+ "@pylonsync/sdk": "^__PYLON_VERSION__",
15
+ "@pylonsync/functions": "^__PYLON_VERSION__",
16
+ "@pylonsync/client": "^__PYLON_VERSION__",
17
+ "react": "^19.0.0",
18
+ "react-dom": "^19.0.0",
19
+ "tailwindcss": "^4.3.0",
20
+ "@tailwindcss/cli": "^4.3.0",
21
+ "tw-animate-css": "^1.2.0",
22
+ "class-variance-authority": "^0.7.1",
23
+ "clsx": "^2.1.1",
24
+ "tailwind-merge": "^2.5.0",
25
+ "lucide-react": "^0.460.0",
26
+ "@radix-ui/react-dialog": "^1.1.4",
27
+ "@radix-ui/react-slot": "^1.1.0"
28
+ },
29
+ "devDependencies": {
30
+ "@happy-dom/global-registrator": "^20.10.0",
31
+ "@pylonsync/cli": "^__PYLON_VERSION__",
32
+ "@testing-library/dom": "^10.4.0",
33
+ "@testing-library/react": "^16.3.0",
34
+ "@types/node": "^22.0.0",
35
+ "@types/react": "^19.0.0",
36
+ "@types/react-dom": "^19.0.0",
37
+ "typescript": "^5.6.0"
38
+ }
39
+ }
@@ -0,0 +1,200 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ // This project uses the classic JSX transform, so .tsx tests import React.
3
+ import React from "react";
4
+ import { cleanup, fireEvent, render, screen, within } from "@testing-library/react";
5
+ import { TaskBoard } from "../components/task-board";
6
+ import { TimeDialog } from "../components/time-dialog";
7
+ import { BudgetBar } from "../components/budget-bar";
8
+ import type { Task } from "../lib/work";
9
+
10
+ afterEach(cleanup);
11
+
12
+ // Tier 2: components. No mocking needed — they take data as props and report
13
+ // changes through callbacks, because the container owns `db`.
14
+
15
+ const tasks: Task[] = [
16
+ { id: "t1", projectId: "p1", title: "Dispatcher board UI", status: "doing" },
17
+ { id: "t2", projectId: "p1", title: "Weekly report", status: "todo" },
18
+ ];
19
+
20
+ const noName = () => null;
21
+ const noMinutes = () => 0;
22
+
23
+ describe("TaskBoard", () => {
24
+ test("renders every column", () => {
25
+ render(
26
+ <TaskBoard
27
+ tasks={tasks}
28
+ assigneeName={noName}
29
+ minutesFor={noMinutes}
30
+ onMove={() => {}}
31
+ onOpen={() => {}}
32
+ />,
33
+ );
34
+ for (const label of ["To do", "In progress", "Review", "Done"]) {
35
+ expect(screen.getByLabelText(label)).toBeDefined();
36
+ }
37
+ });
38
+
39
+ test("dropping on another column reports the move", () => {
40
+ const moves: Array<[string, string]> = [];
41
+ render(
42
+ <TaskBoard
43
+ tasks={tasks}
44
+ assigneeName={noName}
45
+ minutesFor={noMinutes}
46
+ onMove={(id, status) => moves.push([id, status])}
47
+ onOpen={() => {}}
48
+ />,
49
+ );
50
+ const data = new Map<string, string>();
51
+ const dataTransfer = {
52
+ setData: (k: string, v: string) => data.set(k, v),
53
+ getData: (k: string) => data.get(k) ?? "",
54
+ effectAllowed: "",
55
+ dropEffect: "",
56
+ };
57
+ fireEvent.dragStart(screen.getByText("Weekly report"), { dataTransfer });
58
+ fireEvent.drop(screen.getByLabelText("Done"), { dataTransfer });
59
+ expect(moves).toEqual([["t2", "done"]]);
60
+ });
61
+
62
+ test("dropping back on the same column writes nothing", () => {
63
+ const moves: unknown[] = [];
64
+ render(
65
+ <TaskBoard
66
+ tasks={tasks}
67
+ assigneeName={noName}
68
+ minutesFor={noMinutes}
69
+ onMove={() => moves.push(1)}
70
+ onOpen={() => {}}
71
+ />,
72
+ );
73
+ const data = new Map<string, string>();
74
+ const dataTransfer = {
75
+ setData: (k: string, v: string) => data.set(k, v),
76
+ getData: (k: string) => data.get(k) ?? "",
77
+ effectAllowed: "",
78
+ dropEffect: "",
79
+ };
80
+ fireEvent.dragStart(screen.getByText("Weekly report"), { dataTransfer });
81
+ fireEvent.drop(screen.getByLabelText("To do"), { dataTransfer });
82
+ expect(moves).toEqual([]);
83
+ });
84
+
85
+ test("shows a column's logged total", () => {
86
+ render(
87
+ <TaskBoard
88
+ tasks={tasks}
89
+ assigneeName={noName}
90
+ minutesFor={(id) => (id === "t1" ? 600 : 0)}
91
+ onMove={() => {}}
92
+ onOpen={() => {}}
93
+ />,
94
+ );
95
+ expect(within(screen.getByLabelText("In progress")).getAllByText("10h").length)
96
+ .toBeGreaterThan(0);
97
+ });
98
+
99
+ test("flags a card that has blown its estimate", () => {
100
+ // Showing "3h / 8h" on every card is noise; an overrun is worth seeing.
101
+ render(
102
+ <TaskBoard
103
+ tasks={[{ ...tasks[0], estimateMinutes: 120 }]}
104
+ assigneeName={noName}
105
+ minutesFor={() => 300}
106
+ onMove={() => {}}
107
+ onOpen={() => {}}
108
+ />,
109
+ );
110
+ expect(screen.getByText("5h / 2h")).toBeDefined();
111
+ });
112
+
113
+ test("opening a card reports the task id", () => {
114
+ const opened: string[] = [];
115
+ render(
116
+ <TaskBoard
117
+ tasks={tasks}
118
+ assigneeName={noName}
119
+ minutesFor={noMinutes}
120
+ onMove={() => {}}
121
+ onOpen={(id) => opened.push(id)}
122
+ />,
123
+ );
124
+ fireEvent.click(screen.getByText("Dispatcher board UI"));
125
+ expect(opened).toEqual(["t1"]);
126
+ });
127
+ });
128
+
129
+ describe("TimeDialog", () => {
130
+ test("accepts the formats people actually type", () => {
131
+ const logged: number[] = [];
132
+ render(
133
+ <TimeDialog
134
+ open
135
+ taskTitle="Dispatcher board UI"
136
+ onOpenChange={() => {}}
137
+ onLog={(minutes) => logged.push(minutes)}
138
+ />,
139
+ );
140
+ fireEvent.change(screen.getByLabelText("Time"), { target: { value: "1h30" } });
141
+ fireEvent.click(screen.getByRole("button", { name: /log time/i }));
142
+ expect(logged).toEqual([90]);
143
+ });
144
+
145
+ test("previews what it parsed, so a typo is visible before submitting", () => {
146
+ render(
147
+ <TimeDialog open taskTitle="X" onOpenChange={() => {}} onLog={() => {}} />,
148
+ );
149
+ fireEvent.change(screen.getByLabelText("Time"), { target: { value: "90" } });
150
+ expect(screen.getByText("1h 30m")).toBeDefined();
151
+ });
152
+
153
+ test("refuses an unparseable value", () => {
154
+ const logged: number[] = [];
155
+ render(
156
+ <TimeDialog open taskTitle="X" onOpenChange={() => {}} onLog={(m) => logged.push(m)} />,
157
+ );
158
+ fireEvent.change(screen.getByLabelText("Time"), { target: { value: "soon" } });
159
+ fireEvent.click(screen.getByRole("button", { name: /log time/i }));
160
+ expect(logged).toEqual([]);
161
+ expect(screen.getByText(/try 90/i)).toBeDefined();
162
+ });
163
+
164
+ test("refuses more than a day", () => {
165
+ // A typo on a timesheet becomes a typo on an invoice.
166
+ const logged: number[] = [];
167
+ render(
168
+ <TimeDialog open taskTitle="X" onOpenChange={() => {}} onLog={(m) => logged.push(m)} />,
169
+ );
170
+ fireEvent.change(screen.getByLabelText("Time"), { target: { value: "30h" } });
171
+ fireEvent.click(screen.getByRole("button", { name: /log time/i }));
172
+ expect(logged).toEqual([]);
173
+ });
174
+
175
+ test("renders nothing when closed", () => {
176
+ const { container } = render(
177
+ <TimeDialog open={false} taskTitle="X" onOpenChange={() => {}} onLog={() => {}} />,
178
+ );
179
+ expect(container.firstChild).toBeNull();
180
+ });
181
+ });
182
+
183
+ describe("BudgetBar", () => {
184
+ test("calls out an overrun with the amount", () => {
185
+ render(<BudgetBar budgetMinutes={2400} loggedMinutes={2760} />);
186
+ expect(screen.getByText(/over budget by 6h/i)).toBeDefined();
187
+ });
188
+
189
+ test("shows billable value when there's a rate", () => {
190
+ render(
191
+ <BudgetBar budgetMinutes={2400} loggedMinutes={600} hourlyRateCents={16_500} />,
192
+ );
193
+ expect(screen.getByText(/\$1,650\.00 billable/)).toBeDefined();
194
+ });
195
+
196
+ test("says so when there's no budget rather than drawing an empty bar", () => {
197
+ render(<BudgetBar budgetMinutes={0} loggedMinutes={600} />);
198
+ expect(screen.getByText("No budget set")).toBeDefined();
199
+ });
200
+ });