@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,245 @@
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 { PipelineBoard } from "../components/pipeline-board";
6
+ import { DataTable } from "../components/data-table";
7
+ import { CommandPalette } from "../components/command-palette";
8
+ import { EmptyState } from "../components/empty-state";
9
+ import type { Deal } from "../lib/pipeline";
10
+
11
+ afterEach(cleanup);
12
+
13
+ // Tier 2: components. No mocking needed — they take data as props and report
14
+ // changes through callbacks, because the container owns `db`.
15
+
16
+ const deals: Deal[] = [
17
+ { id: "d1", title: "Fleet dispatch rollout", value: 48_000, stage: "proposal" },
18
+ { id: "d2", title: "Class booking system", value: 7_400, stage: "lead" },
19
+ ];
20
+
21
+ const noName = () => null;
22
+
23
+ describe("PipelineBoard", () => {
24
+ test("renders a column per board stage", () => {
25
+ render(
26
+ <PipelineBoard
27
+ deals={deals}
28
+ companyName={noName}
29
+ ownerName={noName}
30
+ onMove={() => {}}
31
+ onOpen={() => {}}
32
+ />,
33
+ );
34
+ for (const label of ["Lead", "Qualified", "Proposal", "Won"]) {
35
+ expect(screen.getByLabelText(label)).toBeDefined();
36
+ }
37
+ // Lost is real in the model but deliberately not a column.
38
+ expect(screen.queryByLabelText("Lost")).toBeNull();
39
+ });
40
+
41
+ test("shows each column's total in its header", () => {
42
+ render(
43
+ <PipelineBoard
44
+ deals={deals}
45
+ companyName={noName}
46
+ ownerName={noName}
47
+ onMove={() => {}}
48
+ onOpen={() => {}}
49
+ />,
50
+ );
51
+ // Scoped to the column, because the same figure also appears on the card —
52
+ // a bare getByText would match twice and throw.
53
+ const proposal = screen.getByLabelText("Proposal");
54
+ expect(within(proposal).getAllByText("$48K").length).toBeGreaterThan(0);
55
+
56
+ const lead = screen.getByLabelText("Lead");
57
+ expect(within(lead).getAllByText("$7,400").length).toBeGreaterThan(0);
58
+
59
+ // An empty column still totals, rather than showing nothing.
60
+ const won = screen.getByLabelText("Won");
61
+ expect(within(won).getByText("$0")).toBeDefined();
62
+ });
63
+
64
+ test("opening a card reports the deal id", () => {
65
+ const opened: string[] = [];
66
+ render(
67
+ <PipelineBoard
68
+ deals={deals}
69
+ companyName={noName}
70
+ ownerName={noName}
71
+ onMove={() => {}}
72
+ onOpen={(id) => opened.push(id)}
73
+ />,
74
+ );
75
+ fireEvent.click(screen.getByText("Fleet dispatch rollout"));
76
+ expect(opened).toEqual(["d1"]);
77
+ });
78
+
79
+ test("dropping on another column reports the move", () => {
80
+ const moves: Array<[string, string]> = [];
81
+ render(
82
+ <PipelineBoard
83
+ deals={deals}
84
+ companyName={noName}
85
+ ownerName={noName}
86
+ onMove={(id, stage) => moves.push([id, stage])}
87
+ onOpen={() => {}}
88
+ />,
89
+ );
90
+ const data = new Map<string, string>();
91
+ const dataTransfer = {
92
+ setData: (k: string, v: string) => data.set(k, v),
93
+ getData: (k: string) => data.get(k) ?? "",
94
+ effectAllowed: "",
95
+ dropEffect: "",
96
+ };
97
+ fireEvent.dragStart(screen.getByText("Class booking system"), { dataTransfer });
98
+ fireEvent.drop(screen.getByLabelText("Won"), { dataTransfer });
99
+ expect(moves).toEqual([["d2", "won"]]);
100
+ });
101
+
102
+ test("dropping a card back on its own column writes nothing", () => {
103
+ // A jittery drag shouldn't produce a history entry saying it moved.
104
+ const moves: Array<[string, string]> = [];
105
+ render(
106
+ <PipelineBoard
107
+ deals={deals}
108
+ companyName={noName}
109
+ ownerName={noName}
110
+ onMove={(id, stage) => moves.push([id, stage])}
111
+ onOpen={() => {}}
112
+ />,
113
+ );
114
+ const data = new Map<string, string>();
115
+ const dataTransfer = {
116
+ setData: (k: string, v: string) => data.set(k, v),
117
+ getData: (k: string) => data.get(k) ?? "",
118
+ effectAllowed: "",
119
+ dropEffect: "",
120
+ };
121
+ fireEvent.dragStart(screen.getByText("Class booking system"), { dataTransfer });
122
+ fireEvent.drop(screen.getByLabelText("Lead"), { dataTransfer });
123
+ expect(moves).toEqual([]);
124
+ });
125
+ });
126
+
127
+ describe("DataTable", () => {
128
+ const rows = [{ id: "1", name: "Acme" }];
129
+
130
+ test("renders a cell per column", () => {
131
+ render(
132
+ <DataTable
133
+ rows={rows}
134
+ columns={[{ key: "name", header: "Company", cell: (r) => r.name }]}
135
+ />,
136
+ );
137
+ expect(screen.getByRole("table")).toBeDefined();
138
+ expect(screen.getByText("Acme")).toBeDefined();
139
+ });
140
+
141
+ test("shows the empty state instead of a bare header", () => {
142
+ render(
143
+ <DataTable
144
+ rows={[]}
145
+ columns={[{ key: "name", header: "Company", cell: () => null }]}
146
+ empty={<EmptyState title="No companies yet" />}
147
+ />,
148
+ );
149
+ expect(screen.queryByRole("table")).toBeNull();
150
+ expect(screen.getByText("No companies yet")).toBeDefined();
151
+ });
152
+
153
+ test("clicking a row reports it", () => {
154
+ const clicked: string[] = [];
155
+ render(
156
+ <DataTable
157
+ rows={rows}
158
+ columns={[{ key: "name", header: "Company", cell: (r) => r.name }]}
159
+ onRowClick={(row) => clicked.push(row.id)}
160
+ />,
161
+ );
162
+ fireEvent.click(screen.getByText("Acme"));
163
+ expect(clicked).toEqual(["1"]);
164
+ });
165
+ });
166
+
167
+ describe("CommandPalette", () => {
168
+ const items = [
169
+ { id: "1", type: "deal" as const, title: "Fleet dispatch", href: "/d/1" },
170
+ { id: "2", type: "company" as const, title: "Acme Inc", href: "/c/2" },
171
+ ];
172
+
173
+ test("renders nothing when closed", () => {
174
+ const { container } = render(
175
+ <CommandPalette open={false} items={items} onClose={() => {}} onSelect={() => {}} />,
176
+ );
177
+ expect(container.firstChild).toBeNull();
178
+ });
179
+
180
+ test("filters as you type", () => {
181
+ render(
182
+ <CommandPalette open items={items} onClose={() => {}} onSelect={() => {}} />,
183
+ );
184
+ fireEvent.change(screen.getByLabelText(/search deals/i), {
185
+ target: { value: "acme" },
186
+ });
187
+ expect(screen.getByText("Acme Inc")).toBeDefined();
188
+ expect(screen.queryByText("Fleet dispatch")).toBeNull();
189
+ });
190
+
191
+ test("Enter selects the highlighted row and closes", () => {
192
+ const selected: string[] = [];
193
+ let closed = false;
194
+ render(
195
+ <CommandPalette
196
+ open
197
+ items={items}
198
+ onClose={() => {
199
+ closed = true;
200
+ }}
201
+ onSelect={(item) => selected.push(item.id)}
202
+ />,
203
+ );
204
+ const input = screen.getByLabelText(/search deals/i);
205
+ fireEvent.change(input, { target: { value: "fleet" } });
206
+ fireEvent.keyDown(input, { key: "Enter" });
207
+ expect(selected).toEqual(["1"]);
208
+ expect(closed).toBe(true);
209
+ });
210
+
211
+ test("Escape closes without selecting", () => {
212
+ const selected: string[] = [];
213
+ let closed = false;
214
+ render(
215
+ <CommandPalette
216
+ open
217
+ items={items}
218
+ onClose={() => {
219
+ closed = true;
220
+ }}
221
+ onSelect={(item) => selected.push(item.id)}
222
+ />,
223
+ );
224
+ fireEvent.keyDown(screen.getByLabelText(/search deals/i), { key: "Escape" });
225
+ expect(closed).toBe(true);
226
+ expect(selected).toEqual([]);
227
+ });
228
+
229
+ test("actions show only with an empty query", () => {
230
+ render(
231
+ <CommandPalette
232
+ open
233
+ items={items}
234
+ actions={[{ id: "new", label: "New deal", run: () => {} }]}
235
+ onClose={() => {}}
236
+ onSelect={() => {}}
237
+ />,
238
+ );
239
+ expect(screen.getByText("New deal")).toBeDefined();
240
+ fireEvent.change(screen.getByLabelText(/search deals/i), {
241
+ target: { value: "acme" },
242
+ });
243
+ expect(screen.queryByText("New deal")).toBeNull();
244
+ });
245
+ });
@@ -0,0 +1,219 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ BOARD_STAGES,
4
+ PIPELINE,
5
+ accentIndex,
6
+ daysUntil,
7
+ groupByStage,
8
+ initials,
9
+ isOpen,
10
+ isValidStage,
11
+ metrics,
12
+ money,
13
+ nextStage,
14
+ percent,
15
+ previousStage,
16
+ relativeTime,
17
+ sumValue,
18
+ type Deal,
19
+ } from "../lib/pipeline";
20
+
21
+ const deal = (over: Partial<Deal> = {}): Deal => ({
22
+ id: "d1",
23
+ title: "Deal",
24
+ value: 1000,
25
+ stage: "lead",
26
+ ...over,
27
+ });
28
+
29
+ describe("stages", () => {
30
+ test("the board hides Lost but the model keeps it", () => {
31
+ // Lost deals still count against the win rate; they just don't deserve a
32
+ // column competing for attention with live work.
33
+ expect(BOARD_STAGES.map((s) => s.id)).toEqual([
34
+ "lead",
35
+ "qualified",
36
+ "proposal",
37
+ "won",
38
+ ]);
39
+ expect(PIPELINE.some((s) => s.id === "lost")).toBe(true);
40
+ });
41
+
42
+ test("open means not closed", () => {
43
+ expect(isOpen({ stage: "proposal" })).toBe(true);
44
+ expect(isOpen({ stage: "won" })).toBe(false);
45
+ expect(isOpen({ stage: "lost" })).toBe(false);
46
+ });
47
+
48
+ test("advance and retreat stop at the ends", () => {
49
+ expect(nextStage("lead")).toBe("qualified");
50
+ expect(nextStage("proposal")).toBe("won");
51
+ expect(nextStage("won")).toBeNull();
52
+ expect(previousStage("qualified")).toBe("lead");
53
+ expect(previousStage("lead")).toBeNull();
54
+ });
55
+
56
+ test("an unknown stage is rejected, not coerced", () => {
57
+ expect(isValidStage("proposal")).toBe(true);
58
+ expect(isValidStage("Proposal")).toBe(false);
59
+ expect(isValidStage("negotiation")).toBe(false);
60
+ expect(nextStage("nonsense")).toBeNull();
61
+ });
62
+ });
63
+
64
+ describe("groupByStage", () => {
65
+ test("puts each deal in its column, newest first", () => {
66
+ const columns = groupByStage([
67
+ deal({ id: "a", stage: "lead", createdAt: "2026-01-01" }),
68
+ deal({ id: "b", stage: "lead", createdAt: "2026-03-01" }),
69
+ deal({ id: "c", stage: "won" }),
70
+ ]);
71
+ const lead = columns.find((c) => c.stage.id === "lead");
72
+ expect(lead?.deals.map((d) => d.id)).toEqual(["b", "a"]);
73
+ expect(columns.find((c) => c.stage.id === "won")?.deals).toHaveLength(1);
74
+ });
75
+
76
+ test("totals each column", () => {
77
+ const columns = groupByStage([
78
+ deal({ id: "a", stage: "lead", value: 1000 }),
79
+ deal({ id: "b", stage: "lead", value: 250 }),
80
+ ]);
81
+ expect(columns.find((c) => c.stage.id === "lead")?.total).toBe(1250);
82
+ });
83
+
84
+ test("a deal with an unknown stage is dropped, not filed under Lead", () => {
85
+ // Silently reclassifying it would hide a typo behind a plausible board.
86
+ const columns = groupByStage([deal({ stage: "negotiation" })]);
87
+ expect(columns.every((c) => c.deals.length === 0)).toBe(true);
88
+ });
89
+
90
+ test("missing or non-numeric values count as zero", () => {
91
+ expect(sumValue([deal({ value: null }), deal({ value: undefined })])).toBe(0);
92
+ });
93
+ });
94
+
95
+ describe("metrics", () => {
96
+ const book = [
97
+ deal({ id: "1", stage: "lead", value: 10_000 }),
98
+ deal({ id: "2", stage: "proposal", value: 20_000 }),
99
+ deal({ id: "3", stage: "won", value: 30_000 }),
100
+ deal({ id: "4", stage: "lost", value: 40_000 }),
101
+ ];
102
+
103
+ test("open excludes closed deals", () => {
104
+ expect(metrics(book).open).toBe(30_000);
105
+ expect(metrics(book).openCount).toBe(2);
106
+ });
107
+
108
+ test("weighted applies each stage's probability", () => {
109
+ // 10k * 0.1 + 20k * 0.6
110
+ expect(metrics(book).weighted).toBe(13_000);
111
+ });
112
+
113
+ test("won and lost are tracked separately", () => {
114
+ expect(metrics(book).won).toBe(30_000);
115
+ expect(metrics(book).lost).toBe(40_000);
116
+ });
117
+
118
+ test("win rate is over CLOSED deals only", () => {
119
+ expect(metrics(book).winRate).toBe(0.5);
120
+ });
121
+
122
+ test("win rate is null before anything closes, not 0%", () => {
123
+ // 0% and "nothing has closed yet" mean very different things to a sales lead.
124
+ expect(metrics([deal({ stage: "lead" })]).winRate).toBeNull();
125
+ expect(percent(null)).toBe("—");
126
+ });
127
+
128
+ test("an empty book is all zeroes, not NaN", () => {
129
+ expect(metrics([])).toMatchObject({ open: 0, weighted: 0, won: 0, winRate: null });
130
+ });
131
+ });
132
+
133
+ describe("money", () => {
134
+ test("compacts so columns line up in a narrow card", () => {
135
+ expect(money(1_240_000)).toBe("$1.2M");
136
+ expect(money(2_000_000)).toBe("$2M");
137
+ expect(money(48_000)).toBe("$48K");
138
+ expect(money(9_500)).toBe("$9,500");
139
+ expect(money(0)).toBe("$0");
140
+ });
141
+
142
+ test("handles nothing and negatives", () => {
143
+ expect(money(null)).toBe("$0");
144
+ expect(money(undefined)).toBe("$0");
145
+ expect(money(-1500)).toBe("-$1,500");
146
+ });
147
+ });
148
+
149
+ describe("initials", () => {
150
+ test("takes first and last for a name", () => {
151
+ expect(initials("Dana Whitfield")).toBe("DW");
152
+ expect(initials("Acme Corporation Holdings")).toBe("AH");
153
+ });
154
+
155
+ test("uses the local part of an email", () => {
156
+ expect(initials("jordan@example.com")).toBe("JO");
157
+ expect(initials("dana.whitfield@example.com")).toBe("DW");
158
+ });
159
+
160
+ test("degrades rather than throwing", () => {
161
+ expect(initials("")).toBe("?");
162
+ expect(initials(null)).toBe("?");
163
+ expect(initials(" ")).toBe("?");
164
+ });
165
+ });
166
+
167
+ describe("accentIndex", () => {
168
+ test("is stable for the same seed", () => {
169
+ expect(accentIndex("Acme")).toBe(accentIndex("Acme"));
170
+ });
171
+
172
+ test("stays in range", () => {
173
+ for (const seed of ["", "a", "Northwind Logistics", "🙂"]) {
174
+ const index = accentIndex(seed, 6);
175
+ expect(index).toBeGreaterThanOrEqual(0);
176
+ expect(index).toBeLessThan(6);
177
+ }
178
+ });
179
+ });
180
+
181
+ describe("relativeTime", () => {
182
+ const now = Date.parse("2026-07-27T12:00:00Z");
183
+ const ago = (ms: number) => new Date(now - ms).toISOString();
184
+
185
+ test("reads naturally across the ranges", () => {
186
+ expect(relativeTime(ago(10_000), now)).toBe("just now");
187
+ expect(relativeTime(ago(5 * 60_000), now)).toBe("5m ago");
188
+ expect(relativeTime(ago(4 * 3_600_000), now)).toBe("4h ago");
189
+ expect(relativeTime(ago(3 * 86_400_000), now)).toBe("3d ago");
190
+ });
191
+
192
+ test("falls back to a date beyond a week", () => {
193
+ expect(relativeTime(ago(30 * 86_400_000), now)).toMatch(/^[A-Z][a-z]{2} \d+$/);
194
+ });
195
+
196
+ test("empty for missing or unparseable input", () => {
197
+ expect(relativeTime(null, now)).toBe("");
198
+ expect(relativeTime("not-a-date", now)).toBe("");
199
+ });
200
+ });
201
+
202
+ describe("daysUntil", () => {
203
+ const now = Date.parse("2026-07-27T18:00:00Z");
204
+
205
+ test("counts whole days, ignoring the time of day", () => {
206
+ // A deal closing "tomorrow" shouldn't read as 0 just because it's evening.
207
+ expect(daysUntil("2026-07-28T02:00:00Z", now)).toBe(1);
208
+ expect(daysUntil("2026-07-27T23:00:00Z", now)).toBe(0);
209
+ });
210
+
211
+ test("negative when overdue", () => {
212
+ expect(daysUntil("2026-07-20T12:00:00Z", now)).toBe(-7);
213
+ });
214
+
215
+ test("null when there's no date", () => {
216
+ expect(daysUntil(null, now)).toBeNull();
217
+ expect(daysUntil("nope", now)).toBeNull();
218
+ });
219
+ });
@@ -0,0 +1,83 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { moveSelection, searchItems, type SearchItem } from "../lib/search";
3
+
4
+ const items: SearchItem[] = [
5
+ { id: "1", type: "company", title: "Acme Inc", subtitle: "acme.com", href: "/c" },
6
+ {
7
+ id: "2",
8
+ type: "company",
9
+ title: "Acme Corporation Holdings",
10
+ subtitle: "acmeholdings.com",
11
+ href: "/c",
12
+ },
13
+ {
14
+ id: "3",
15
+ type: "contact",
16
+ title: "Dana Whitfield",
17
+ subtitle: "Northwind Logistics",
18
+ href: "/p",
19
+ keywords: "dana@northwind.co VP Operations",
20
+ },
21
+ { id: "4", type: "deal", title: "Fleet dispatch rollout", href: "/d" },
22
+ ];
23
+
24
+ const titles = (q: string) => searchItems(items, q).map((i) => i.title);
25
+
26
+ describe("searchItems", () => {
27
+ test("an empty query lists everything", () => {
28
+ expect(searchItems(items, "").length).toBe(items.length);
29
+ expect(searchItems(items, " ").length).toBe(items.length);
30
+ });
31
+
32
+ test("prefix beats substring", () => {
33
+ expect(titles("acme")[0]).toBe("Acme Inc");
34
+ });
35
+
36
+ test("the shorter title wins a tie", () => {
37
+ // Both start with "Acme"; the more specific record should come first.
38
+ expect(titles("acme")).toEqual(["Acme Inc", "Acme Corporation Holdings"]);
39
+ });
40
+
41
+ test("matches a word inside the title", () => {
42
+ expect(titles("dispatch")).toContain("Fleet dispatch rollout");
43
+ });
44
+
45
+ test("matches the subtitle and hidden keywords", () => {
46
+ expect(titles("northwind")).toContain("Dana Whitfield");
47
+ expect(titles("dana@northwind.co")).toContain("Dana Whitfield");
48
+ expect(titles("VP")).toContain("Dana Whitfield");
49
+ });
50
+
51
+ test("is case-insensitive", () => {
52
+ expect(titles("ACME").length).toBe(2);
53
+ });
54
+
55
+ test("no match is empty, not everything", () => {
56
+ expect(titles("zzzz")).toEqual([]);
57
+ });
58
+
59
+ test("respects the limit", () => {
60
+ expect(searchItems(items, "", 2)).toHaveLength(2);
61
+ });
62
+
63
+ test("a word-boundary hit beats a mid-word one", () => {
64
+ const rows: SearchItem[] = [
65
+ { id: "a", type: "deal", title: "Broadcasting", href: "/" },
66
+ { id: "b", type: "deal", title: "New cast list", href: "/" },
67
+ ];
68
+ expect(searchItems(rows, "cast").map((r) => r.title)[0]).toBe("New cast list");
69
+ });
70
+ });
71
+
72
+ describe("moveSelection", () => {
73
+ test("wraps at both ends", () => {
74
+ expect(moveSelection(0, -1, 3)).toBe(2);
75
+ expect(moveSelection(2, 1, 3)).toBe(0);
76
+ expect(moveSelection(0, 1, 3)).toBe(1);
77
+ });
78
+
79
+ test("an empty list stays at zero rather than going negative", () => {
80
+ expect(moveSelection(0, 1, 0)).toBe(0);
81
+ expect(moveSelection(0, -1, 0)).toBe(0);
82
+ });
83
+ });
@@ -0,0 +1,5 @@
1
+ import { GlobalRegistrator } from "@happy-dom/global-registrator";
2
+
3
+ // Make document/window available so @testing-library/react can render. Loaded
4
+ // via bunfig.toml's [test] preload, before any test file runs.
5
+ GlobalRegistrator.register();
@@ -0,0 +1,33 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "jsx": "react",
7
+ "esModuleInterop": true,
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "lib": [
11
+ "ES2022",
12
+ "DOM"
13
+ ],
14
+ "types": [
15
+ "react",
16
+ "react-dom",
17
+ "node"
18
+ ],
19
+ "baseUrl": ".",
20
+ "paths": {
21
+ "@/*": [
22
+ "./*"
23
+ ]
24
+ }
25
+ },
26
+ "include": [
27
+ "app.ts",
28
+ "app/**/*",
29
+ "components/**/*",
30
+ "lib/**/*",
31
+ "functions/**/*"
32
+ ]
33
+ }
@@ -0,0 +1,37 @@
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+
4
+ import { cn } from "@/lib/utils";
5
+
6
+ const badgeVariants = cva(
7
+ "inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "border-transparent bg-primary text-primary-foreground",
12
+ secondary: "border-transparent bg-secondary text-secondary-foreground",
13
+ destructive: "border-transparent bg-destructive text-white",
14
+ outline: "text-foreground",
15
+ },
16
+ },
17
+ defaultVariants: {
18
+ variant: "default",
19
+ },
20
+ },
21
+ );
22
+
23
+ export interface BadgeProps
24
+ extends React.HTMLAttributes<HTMLSpanElement>,
25
+ VariantProps<typeof badgeVariants> {}
26
+
27
+ function Badge({ className, variant, ...props }: BadgeProps) {
28
+ return (
29
+ <span
30
+ data-slot="badge"
31
+ className={cn(badgeVariants({ variant }), className)}
32
+ {...props}
33
+ />
34
+ );
35
+ }
36
+
37
+ export { Badge, badgeVariants };