@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,231 @@
1
+ "use client";
2
+
3
+ import React, { useEffect, useMemo, useRef, useState } from "react";
4
+ import { callFn, db, useRouter } from "@pylonsync/react";
5
+ import { useAuth } from "@pylonsync/client";
6
+ import { Sidebar } from "@/components/sidebar";
7
+ import { CommandPalette } from "@/components/command-palette";
8
+ import type { SearchItem } from "@/lib/search";
9
+
10
+ export interface CompanyRow {
11
+ id: string;
12
+ name: string;
13
+ domain?: string | null;
14
+ industry?: string | null;
15
+ size?: string | null;
16
+ createdAt?: string | null;
17
+ }
18
+ export interface ContactRow {
19
+ id: string;
20
+ name: string;
21
+ email?: string | null;
22
+ phone?: string | null;
23
+ title?: string | null;
24
+ companyId?: string | null;
25
+ createdAt?: string | null;
26
+ }
27
+ export interface DealRow {
28
+ id: string;
29
+ title: string;
30
+ value?: number | null;
31
+ stage: string;
32
+ companyId?: string | null;
33
+ contactId?: string | null;
34
+ closeDate?: string | null;
35
+ ownerId?: string | null;
36
+ createdAt?: string | null;
37
+ updatedAt?: string | null;
38
+ }
39
+ export interface ActivityRow {
40
+ id: string;
41
+ kind: string;
42
+ body: string;
43
+ dealId?: string | null;
44
+ contactId?: string | null;
45
+ ownerId?: string | null;
46
+ createdAt?: string | null;
47
+ }
48
+ export interface UserRow {
49
+ id: string;
50
+ email: string;
51
+ displayName?: string | null;
52
+ }
53
+
54
+ export interface WorkspaceData {
55
+ companies: CompanyRow[];
56
+ contacts: ContactRow[];
57
+ deals: DealRow[];
58
+ activities: ActivityRow[];
59
+ companyName: (id: string | null | undefined) => string | null;
60
+ ownerName: (id: string | null | undefined) => string | null;
61
+ loading: boolean;
62
+ }
63
+
64
+ /**
65
+ * The app shell, and the ONLY place that touches `db`.
66
+ *
67
+ * Every screen below receives plain data through the render prop and reports
68
+ * changes through callbacks, which is what keeps the components in
69
+ * `components/` renderable from fixtures in a test with nothing mocked.
70
+ *
71
+ * Each `db.useQuery` is live: when a teammate moves a deal, the row arrives
72
+ * here and the board re-renders. Your own writes take exactly the same path,
73
+ * so there's one behaviour to reason about rather than two.
74
+ */
75
+ export function Workspace({
76
+ email,
77
+ pathname,
78
+ children,
79
+ }: {
80
+ email: string;
81
+ pathname: string;
82
+ children: (data: WorkspaceData) => React.ReactNode;
83
+ }) {
84
+ const router = useRouter();
85
+ const { signOut } = useAuth();
86
+ const { data: companies, loading: loadingCompanies } =
87
+ db.useQuery<CompanyRow>("Company");
88
+ const { data: contacts } = db.useQuery<ContactRow>("Contact");
89
+ const { data: deals } = db.useQuery<DealRow>("Deal");
90
+ const { data: activities } = db.useQuery<ActivityRow>("Activity");
91
+ const { data: users } = db.useQuery<UserRow>("User");
92
+
93
+ const [paletteOpen, setPaletteOpen] = useState(false);
94
+ const seeded = useRef(false);
95
+
96
+ const companyList = companies ?? [];
97
+ const contactList = contacts ?? [];
98
+ const dealList = deals ?? [];
99
+
100
+ // A brand-new workspace gets a realistic pipeline once, so the app never
101
+ // opens on an empty board. `seedWorkspace` no-ops when anything exists, and
102
+ // the ref stops a re-render from firing a second call mid-flight.
103
+ useEffect(() => {
104
+ if (loadingCompanies || seeded.current) return;
105
+ if (companyList.length > 0) {
106
+ seeded.current = true;
107
+ return;
108
+ }
109
+ seeded.current = true;
110
+ callFn("seedWorkspace", {}).catch(() => {
111
+ // A failed seed leaves an empty workspace, which is usable — every view
112
+ // has an empty state that offers to create the first record.
113
+ seeded.current = false;
114
+ });
115
+ }, [loadingCompanies, companyList.length]);
116
+
117
+ // ⌘K / Ctrl+K anywhere, and "/" when you're not already typing.
118
+ useEffect(() => {
119
+ function onKey(event: KeyboardEvent) {
120
+ const target = event.target as HTMLElement | null;
121
+ const typing =
122
+ !!target &&
123
+ (target.tagName === "INPUT" ||
124
+ target.tagName === "TEXTAREA" ||
125
+ target.isContentEditable);
126
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
127
+ event.preventDefault();
128
+ setPaletteOpen((open) => !open);
129
+ } else if (event.key === "/" && !typing) {
130
+ event.preventDefault();
131
+ setPaletteOpen(true);
132
+ }
133
+ }
134
+ window.addEventListener("keydown", onKey);
135
+ return () => window.removeEventListener("keydown", onKey);
136
+ }, []);
137
+
138
+ const byId = useMemo(() => {
139
+ const companyById = new Map(companyList.map((c) => [c.id, c] as const));
140
+ const userById = new Map((users ?? []).map((u) => [u.id, u] as const));
141
+ return { companyById, userById };
142
+ }, [companyList, users]);
143
+
144
+ const searchIndex = useMemo<SearchItem[]>(
145
+ () => [
146
+ ...dealList.map((deal) => ({
147
+ id: `deal:${deal.id}`,
148
+ type: "deal" as const,
149
+ title: deal.title,
150
+ subtitle: byId.companyById.get(deal.companyId ?? "")?.name,
151
+ href: `/deals/${deal.id}`,
152
+ })),
153
+ ...companyList.map((company) => ({
154
+ id: `company:${company.id}`,
155
+ type: "company" as const,
156
+ title: company.name,
157
+ subtitle: company.domain ?? undefined,
158
+ href: "/companies",
159
+ keywords: company.industry ?? undefined,
160
+ })),
161
+ ...contactList.map((contact) => ({
162
+ id: `contact:${contact.id}`,
163
+ type: "contact" as const,
164
+ title: contact.name,
165
+ subtitle: byId.companyById.get(contact.companyId ?? "")?.name,
166
+ href: "/contacts",
167
+ keywords: `${contact.email ?? ""} ${contact.title ?? ""}`,
168
+ })),
169
+ ],
170
+ [dealList, companyList, contactList, byId],
171
+ );
172
+
173
+ const data: WorkspaceData = {
174
+ companies: companyList,
175
+ contacts: contactList,
176
+ deals: dealList,
177
+ activities: activities ?? [],
178
+ companyName: (id) => byId.companyById.get(id ?? "")?.name ?? null,
179
+ ownerName: (id) => {
180
+ const user = byId.userById.get(id ?? "");
181
+ return user?.displayName || user?.email || null;
182
+ },
183
+ loading: loadingCompanies,
184
+ };
185
+
186
+ return (
187
+ <div className="flex h-screen overflow-hidden">
188
+ <Sidebar
189
+ workspace="CRM"
190
+ email={email}
191
+ pathname={pathname}
192
+ counts={{
193
+ "/companies": companyList.length,
194
+ "/contacts": contactList.length,
195
+ }}
196
+ onOpenCommand={() => setPaletteOpen(true)}
197
+ onSignOut={async () => {
198
+ await signOut();
199
+ // Full navigation so the SSR runtime re-resolves auth from the
200
+ // cookie and the login page renders server-side.
201
+ window.location.assign("/login");
202
+ }}
203
+ />
204
+ <main className="flex min-w-0 flex-1 flex-col">{children(data)}</main>
205
+
206
+ <CommandPalette
207
+ open={paletteOpen}
208
+ items={searchIndex}
209
+ actions={[
210
+ {
211
+ id: "new-deal",
212
+ label: "New deal",
213
+ run: () => router.push("/?new=deal"),
214
+ },
215
+ {
216
+ id: "go-companies",
217
+ label: "Go to Companies",
218
+ run: () => router.push("/companies"),
219
+ },
220
+ {
221
+ id: "go-contacts",
222
+ label: "Go to Contacts",
223
+ run: () => router.push("/contacts"),
224
+ },
225
+ ]}
226
+ onClose={() => setPaletteOpen(false)}
227
+ onSelect={(item) => router.push(item.href)}
228
+ />
229
+ </div>
230
+ );
231
+ }
@@ -0,0 +1,171 @@
1
+ import {
2
+ entity,
3
+ field,
4
+ policy,
5
+ auth,
6
+ buildManifest,
7
+ discoverAppRoutes,
8
+ font,
9
+ } from "@pylonsync/sdk";
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // crm — a sales CRM for a small team: companies, contacts, deals moving through
13
+ // a pipeline, and an activity log.
14
+ //
15
+ // The realtime hook: the board subscribes to Deal, so when a teammate drags a
16
+ // deal to "Won" it moves on everyone's board instantly. No refresh, and no
17
+ // standup spent reconciling two people's idea of the forecast.
18
+ //
19
+ // • Company — the account. Contacts and deals hang off it.
20
+ // • Contact — a person at a company. Holds email/phone, so never public.
21
+ // • Deal — the opportunity: value, stage, expected close. The pipeline.
22
+ // • Activity — a note/call/email/meeting logged against a deal or contact.
23
+ // • User — a team member (email/password).
24
+ //
25
+ // TRUST MODEL: an internal tool for ONE team. Every entity is readable and
26
+ // writable by any SIGNED-IN user, and by nobody else — a CRM's whole point is a
27
+ // shared pipeline. `ownerId` records who created a row (for "my deals" and
28
+ // accountability) without partitioning access. For per-rep isolation, tighten
29
+ // allowRead to "auth.userId == data.ownerId" and add a manager role.
30
+ // ---------------------------------------------------------------------------
31
+
32
+ // Signed-in team members only. Deliberately not `"true"`: an anonymous visitor
33
+ // must never read the customer list, and `pylon lint` flags wide-open policies
34
+ // for exactly this reason.
35
+ const TEAM = "auth.userId != null";
36
+
37
+ const Company = entity(
38
+ "Company",
39
+ {
40
+ name: field.string(),
41
+ domain: field.string().optional(),
42
+ industry: field.string().optional(),
43
+ // Free text ("1-10", "50-200") rather than an enum — headcount bands vary
44
+ // by who you ask, and a CRM shouldn't argue with its user about it.
45
+ size: field.string().optional(),
46
+ notes: field.string().optional(),
47
+ ownerId: field.id("User").readonly().optional(),
48
+ createdAt: field.datetime().defaultNow(),
49
+ },
50
+ { indexes: [{ name: "by_name", fields: ["name"], unique: false }] },
51
+ );
52
+
53
+ const Contact = entity(
54
+ "Contact",
55
+ {
56
+ companyId: field.id("Company").optional(),
57
+ name: field.string(),
58
+ email: field.string().optional(),
59
+ phone: field.string().optional(),
60
+ title: field.string().optional(),
61
+ ownerId: field.id("User").readonly().optional(),
62
+ createdAt: field.datetime().defaultNow(),
63
+ },
64
+ { indexes: [{ name: "by_company", fields: ["companyId"], unique: false }] },
65
+ );
66
+
67
+ // `stage` drives the board; PIPELINE in lib/pipeline.ts owns the ordered stages
68
+ // and which of them count as closed.
69
+ const Deal = entity(
70
+ "Deal",
71
+ {
72
+ title: field.string(),
73
+ companyId: field.id("Company").optional(),
74
+ contactId: field.id("Contact").optional(),
75
+ // Whole currency units, not cents — a forecast is read by humans, and this
76
+ // keeps the card, the column total, and the input in one unit.
77
+ value: field.number().default(0),
78
+ stage: field.string().default("lead"),
79
+ closeDate: field.datetime().optional(),
80
+ ownerId: field.id("User").readonly().optional(),
81
+ createdAt: field.datetime().defaultNow(),
82
+ updatedAt: field.datetime().defaultNow(),
83
+ },
84
+ { indexes: [{ name: "by_stage", fields: ["stage"], unique: false }] },
85
+ );
86
+
87
+ // The timeline. Attached to a deal, a contact, or both — logging a call against
88
+ // a contact shouldn't require inventing a deal to hang it on.
89
+ const Activity = entity(
90
+ "Activity",
91
+ {
92
+ // "note" | "call" | "email" | "meeting" — see ACTIVITY_KINDS.
93
+ kind: field.string().default("note"),
94
+ body: field.string(),
95
+ dealId: field.id("Deal").optional(),
96
+ contactId: field.id("Contact").optional(),
97
+ companyId: field.id("Company").optional(),
98
+ ownerId: field.id("User").readonly().optional(),
99
+ createdAt: field.datetime().defaultNow(),
100
+ },
101
+ { indexes: [{ name: "by_deal", fields: ["dealId"], unique: false }] },
102
+ );
103
+
104
+ const User = entity(
105
+ "User",
106
+ {
107
+ email: field.string(),
108
+ displayName: field.string().optional(),
109
+ passwordHash: field.string().serverOnly().optional(),
110
+ avatarColor: field.string().optional(),
111
+ emailVerified: field.datetime().optional(),
112
+ createdAt: field.datetime().defaultNow(),
113
+ },
114
+ { indexes: [{ name: "by_email", fields: ["email"], unique: true }] },
115
+ );
116
+
117
+ const teamPolicy = (name: string, entityName: string) =>
118
+ policy({
119
+ name,
120
+ entity: entityName,
121
+ allowRead: TEAM,
122
+ allowInsert: TEAM,
123
+ allowUpdate: TEAM,
124
+ allowDelete: TEAM,
125
+ });
126
+
127
+ // Team members read each other's rows so a deal can show who owns it. Nobody
128
+ // writes User through the client — auth owns that, and passwordHash is
129
+ // serverOnly regardless.
130
+ const userPolicy = policy({
131
+ name: "user_team_read",
132
+ entity: "User",
133
+ allowRead: TEAM,
134
+ allowInsert: "false",
135
+ allowUpdate: "false",
136
+ allowDelete: "false",
137
+ });
138
+
139
+ const manifest = buildManifest({
140
+ name: "__APP_NAME__",
141
+ version: "0.1.0",
142
+ entities: [Company, Contact, Deal, Activity, User],
143
+ queries: [],
144
+ actions: [],
145
+ policies: [
146
+ teamPolicy("company_team", "Company"),
147
+ teamPolicy("contact_team", "Contact"),
148
+ teamPolicy("deal_team", "Deal"),
149
+ teamPolicy("activity_team", "Activity"),
150
+ userPolicy,
151
+ ],
152
+ auth: auth(),
153
+ // Self-hosted Inter: the build fetches the woff2, serves it same-origin (no
154
+ // third-party request, no FOUT), preloads it, and synthesizes a size-adjusted
155
+ // fallback so there's no layout shift.
156
+ fonts: [
157
+ font({
158
+ family: "Inter",
159
+ variable: "--font-sans",
160
+ weights: ["400", "500", "600", "700"],
161
+ subsets: ["latin"],
162
+ display: "swap",
163
+ preload: true,
164
+ }),
165
+ ],
166
+ routes: await discoverAppRoutes(),
167
+ });
168
+
169
+ console.log(JSON.stringify(manifest, null, 2));
170
+
171
+ export default manifest;
@@ -0,0 +1,4 @@
1
+ [test]
2
+ # Register a DOM (happy-dom) before any test runs so React component tests can
3
+ # render under `pylon test` / `bun test`. Harmless for pure-logic tests.
4
+ preload = ["./tests/setup.ts"]
@@ -0,0 +1,133 @@
1
+ import React, { useState } from "react";
2
+ import { CalendarDays, Mail, PhoneCall, StickyNote } from "lucide-react";
3
+ import { cn } from "@/lib/utils";
4
+ import { Avatar } from "@/components/avatar";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Select } from "@/components/ui/select";
7
+ import { Textarea } from "@/components/ui/textarea";
8
+ import { ACTIVITY_KINDS, relativeTime, type ActivityKind } from "@/lib/pipeline";
9
+
10
+ export interface ActivityRow {
11
+ id: string;
12
+ kind: string;
13
+ body: string;
14
+ ownerId?: string | null;
15
+ createdAt?: string | null;
16
+ }
17
+
18
+ const ICON: Record<string, React.ReactNode> = {
19
+ note: <StickyNote />,
20
+ call: <PhoneCall />,
21
+ email: <Mail />,
22
+ meeting: <CalendarDays />,
23
+ };
24
+
25
+ /**
26
+ * What happened on this deal, newest first, with the composer on top so logging
27
+ * a call takes one click from landing on the page.
28
+ */
29
+ export function ActivityTimeline({
30
+ activities,
31
+ ownerName,
32
+ onLog,
33
+ }: {
34
+ activities: ActivityRow[];
35
+ ownerName: (id: string | null | undefined) => string | null;
36
+ onLog: (kind: ActivityKind, body: string) => void | Promise<void>;
37
+ }) {
38
+ const [kind, setKind] = useState<ActivityKind>("note");
39
+ const [body, setBody] = useState("");
40
+ const [busy, setBusy] = useState(false);
41
+
42
+ const ordered = [...activities].sort((a, b) =>
43
+ (b.createdAt ?? "").localeCompare(a.createdAt ?? ""),
44
+ );
45
+
46
+ async function submit(event: React.FormEvent) {
47
+ event.preventDefault();
48
+ const text = body.trim();
49
+ if (!text || busy) return;
50
+ setBusy(true);
51
+ try {
52
+ await onLog(kind, text);
53
+ setBody("");
54
+ } finally {
55
+ setBusy(false);
56
+ }
57
+ }
58
+
59
+ return (
60
+ <div className="space-y-4">
61
+ <form onSubmit={submit} className="rounded-lg border border-border bg-card p-2.5">
62
+ <Textarea
63
+ value={body}
64
+ rows={2}
65
+ placeholder="Log a call, note, or next step…"
66
+ aria-label="Activity"
67
+ onChange={(event) => setBody(event.target.value)}
68
+ onKeyDown={(event) => {
69
+ // ⌘/Ctrl+Enter submits — the composer is small and always focused
70
+ // when you're typing into it.
71
+ if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
72
+ submit(event);
73
+ }
74
+ }}
75
+ className="min-h-16 resize-none border-0 bg-transparent p-0 shadow-none focus-visible:ring-0"
76
+ />
77
+ <div className="mt-2 flex items-center justify-between gap-2">
78
+ <div className="w-32">
79
+ <Select
80
+ aria-label="Activity type"
81
+ value={kind}
82
+ onChange={(event) => setKind(event.target.value as ActivityKind)}
83
+ className="h-7 text-[12px]"
84
+ >
85
+ {ACTIVITY_KINDS.map((value) => (
86
+ <option key={value} value={value}>
87
+ {value[0].toUpperCase() + value.slice(1)}
88
+ </option>
89
+ ))}
90
+ </Select>
91
+ </div>
92
+ <Button type="submit" size="sm" disabled={!body.trim() || busy}>
93
+ {busy ? "Saving…" : "Log"}
94
+ </Button>
95
+ </div>
96
+ </form>
97
+
98
+ {ordered.length === 0 ? (
99
+ <p className="px-1 py-6 text-center text-[12px] text-muted-foreground">
100
+ No activity yet. Log the first call or note above.
101
+ </p>
102
+ ) : (
103
+ <ol className="space-y-3">
104
+ {ordered.map((activity) => (
105
+ <li key={activity.id} className="flex gap-2.5">
106
+ <div
107
+ className={cn(
108
+ "mt-0.5 flex size-6 shrink-0 items-center justify-center rounded-md",
109
+ "bg-surface-2 text-muted-foreground [&_svg]:size-3",
110
+ )}
111
+ >
112
+ {ICON[activity.kind] ?? ICON.note}
113
+ </div>
114
+ <div className="min-w-0 flex-1">
115
+ <div className="flex items-center gap-1.5 text-[11px] text-muted-foreground">
116
+ <Avatar name={ownerName(activity.ownerId) ?? "?"} size="sm" />
117
+ <span className="capitalize">{activity.kind}</span>
118
+ <span aria-hidden="true">·</span>
119
+ <time dateTime={activity.createdAt ?? undefined}>
120
+ {relativeTime(activity.createdAt)}
121
+ </time>
122
+ </div>
123
+ <p className="mt-1 whitespace-pre-wrap text-[13px] leading-5">
124
+ {activity.body}
125
+ </p>
126
+ </div>
127
+ </li>
128
+ ))}
129
+ </ol>
130
+ )}
131
+ </div>
132
+ );
133
+ }
@@ -0,0 +1,40 @@
1
+ import React from "react";
2
+ import { cn } from "@/lib/utils";
3
+ import { accentIndex, initials } from "@/lib/pipeline";
4
+
5
+ // Deterministic accent per record — the same company keeps its colour across
6
+ // sessions and machines without storing one. Hues are muted so a dense list
7
+ // doesn't turn into confetti.
8
+ const ACCENTS = [
9
+ "bg-indigo-500/15 text-indigo-500 dark:text-indigo-300",
10
+ "bg-emerald-500/15 text-emerald-600 dark:text-emerald-300",
11
+ "bg-amber-500/15 text-amber-600 dark:text-amber-300",
12
+ "bg-rose-500/15 text-rose-500 dark:text-rose-300",
13
+ "bg-sky-500/15 text-sky-600 dark:text-sky-300",
14
+ "bg-violet-500/15 text-violet-500 dark:text-violet-300",
15
+ ];
16
+
17
+ export function Avatar({
18
+ name,
19
+ size = "md",
20
+ className,
21
+ }: {
22
+ name: string | null | undefined;
23
+ size?: "sm" | "md";
24
+ className?: string;
25
+ }) {
26
+ const label = initials(name);
27
+ return (
28
+ <span
29
+ aria-hidden="true"
30
+ className={cn(
31
+ "inline-flex shrink-0 select-none items-center justify-center rounded-full font-medium",
32
+ size === "sm" ? "size-5 text-[9px]" : "size-6 text-[10px]",
33
+ ACCENTS[accentIndex(name ?? "?", ACCENTS.length)],
34
+ className,
35
+ )}
36
+ >
37
+ {label}
38
+ </span>
39
+ );
40
+ }
@@ -0,0 +1,44 @@
1
+ import React from "react";
2
+ import { BOARD_STAGES } from "@/lib/pipeline";
3
+
4
+ /**
5
+ * The board's shape while the replica hydrates.
6
+ *
7
+ * The shell is server-rendered but the deals arrive over sync, so there's a
8
+ * beat before the first row lands. Showing the real column structure with
9
+ * placeholder cards keeps that beat from reading as "you have no deals" — and
10
+ * because the columns are already in place, nothing jumps when the data
11
+ * arrives.
12
+ */
13
+ export function BoardSkeleton() {
14
+ return (
15
+ <div className="flex h-full gap-3 overflow-hidden p-4" aria-hidden="true">
16
+ {BOARD_STAGES.map((stage, column) => (
17
+ <section
18
+ key={stage.id}
19
+ className="flex w-[268px] shrink-0 flex-col rounded-xl border border-border bg-surface-1/50"
20
+ >
21
+ <header className="flex items-center gap-2 px-3 py-2.5">
22
+ <span className="text-[12px] font-medium text-muted-foreground">
23
+ {stage.label}
24
+ </span>
25
+ </header>
26
+ <div className="flex flex-col gap-2 px-2 pb-2">
27
+ {/* Fewer cards further down the funnel, so the placeholder reads
28
+ like a pipeline rather than a grid. */}
29
+ {Array.from({ length: Math.max(1, 3 - column) }).map((_, card) => (
30
+ <div
31
+ key={card}
32
+ className="animate-pulse rounded-lg border border-border bg-card p-2.5"
33
+ >
34
+ <div className="h-3 w-3/4 rounded bg-muted" />
35
+ <div className="mt-2 h-2.5 w-1/2 rounded bg-muted" />
36
+ <div className="mt-3 h-4 w-4 rounded-full bg-muted" />
37
+ </div>
38
+ ))}
39
+ </div>
40
+ </section>
41
+ ))}
42
+ </div>
43
+ );
44
+ }