@pylonsync/create-pylon 0.3.344 → 0.3.346

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 (295) hide show
  1. package/bin/create-pylon.js +35 -0
  2. package/package.json +1 -1
  3. package/templates/crm/AGENTS.md +116 -0
  4. package/templates/crm/CLAUDE.md +7 -0
  5. package/templates/crm/README.md +96 -0
  6. package/templates/crm/app/companies/companies-view.tsx +129 -0
  7. package/templates/crm/app/companies/page.tsx +17 -0
  8. package/templates/crm/app/contacts/contacts-view.tsx +139 -0
  9. package/templates/crm/app/contacts/page.tsx +17 -0
  10. package/templates/crm/app/deals/[id]/deal-view.tsx +189 -0
  11. package/templates/crm/app/deals/[id]/page.tsx +23 -0
  12. package/templates/crm/app/error.tsx +43 -0
  13. package/templates/crm/app/globals.css +201 -0
  14. package/templates/crm/app/layout.tsx +31 -0
  15. package/templates/crm/app/login/auth-form.tsx +122 -0
  16. package/templates/crm/app/login/page.tsx +38 -0
  17. package/templates/crm/app/not-found.tsx +29 -0
  18. package/templates/crm/app/page.tsx +38 -0
  19. package/templates/crm/app/pipeline-view.tsx +99 -0
  20. package/templates/crm/app/robots.ts +12 -0
  21. package/templates/crm/app/sitemap.ts +27 -0
  22. package/templates/crm/app/workspace.tsx +231 -0
  23. package/templates/crm/app.ts +171 -0
  24. package/templates/crm/bunfig +4 -0
  25. package/templates/crm/components/activity-timeline.tsx +133 -0
  26. package/templates/crm/components/avatar.tsx +40 -0
  27. package/templates/crm/components/board-skeleton.tsx +44 -0
  28. package/templates/crm/components/command-palette.tsx +162 -0
  29. package/templates/crm/components/data-table.tsx +90 -0
  30. package/templates/crm/components/deal-card.tsx +91 -0
  31. package/templates/crm/components/deal-dialog.tsx +169 -0
  32. package/templates/crm/components/empty-state.tsx +43 -0
  33. package/templates/crm/components/kbd.tsx +23 -0
  34. package/templates/crm/components/metrics-bar.tsx +46 -0
  35. package/templates/crm/components/page-header.tsx +36 -0
  36. package/templates/crm/components/pipeline-board.tsx +105 -0
  37. package/templates/crm/components/record-dialog.tsx +126 -0
  38. package/templates/crm/components/sidebar.tsx +112 -0
  39. package/templates/crm/components/stage-badge.tsx +33 -0
  40. package/templates/crm/components/ui/badge.tsx +37 -0
  41. package/templates/crm/components/ui/button.tsx +56 -0
  42. package/templates/crm/components/ui/card.tsx +90 -0
  43. package/templates/crm/components/ui/dialog.tsx +110 -0
  44. package/templates/crm/components/ui/input.tsx +30 -0
  45. package/templates/crm/components/ui/label.tsx +21 -0
  46. package/templates/crm/components/ui/select.tsx +39 -0
  47. package/templates/crm/components/ui/table.tsx +124 -0
  48. package/templates/crm/components/ui/textarea.tsx +24 -0
  49. package/templates/crm/components.json +20 -0
  50. package/templates/crm/functions/createDeal.ts +59 -0
  51. package/templates/crm/functions/logActivity.ts +49 -0
  52. package/templates/crm/functions/moveDeal.ts +53 -0
  53. package/templates/crm/functions/seedWorkspace.ts +63 -0
  54. package/templates/crm/gitignore +10 -0
  55. package/templates/crm/lib/pipeline.ts +221 -0
  56. package/templates/crm/lib/search.ts +78 -0
  57. package/templates/crm/lib/seed.ts +130 -0
  58. package/templates/crm/lib/utils.ts +10 -0
  59. package/templates/crm/package.json +39 -0
  60. package/templates/crm/tests/components.test.tsx +245 -0
  61. package/templates/crm/tests/pipeline.test.ts +219 -0
  62. package/templates/crm/tests/search.test.ts +83 -0
  63. package/templates/crm/tests/setup.ts +5 -0
  64. package/templates/crm/tsconfig.json +33 -0
  65. package/templates/helpdesk/AGENTS.md +116 -0
  66. package/templates/helpdesk/CLAUDE.md +7 -0
  67. package/templates/helpdesk/README.md +90 -0
  68. package/templates/helpdesk/app/customers/customers-view.tsx +140 -0
  69. package/templates/helpdesk/app/customers/page.tsx +17 -0
  70. package/templates/helpdesk/app/error.tsx +43 -0
  71. package/templates/helpdesk/app/globals.css +201 -0
  72. package/templates/helpdesk/app/inbox-view.tsx +147 -0
  73. package/templates/helpdesk/app/layout.tsx +31 -0
  74. package/templates/helpdesk/app/login/auth-form.tsx +122 -0
  75. package/templates/helpdesk/app/login/page.tsx +38 -0
  76. package/templates/helpdesk/app/not-found.tsx +29 -0
  77. package/templates/helpdesk/app/page.tsx +38 -0
  78. package/templates/helpdesk/app/robots.ts +12 -0
  79. package/templates/helpdesk/app/sitemap.ts +27 -0
  80. package/templates/helpdesk/app/tickets/[id]/page.tsx +23 -0
  81. package/templates/helpdesk/app/tickets/[id]/ticket-view.tsx +164 -0
  82. package/templates/helpdesk/app/workspace.tsx +210 -0
  83. package/templates/helpdesk/app.ts +142 -0
  84. package/templates/helpdesk/bunfig +4 -0
  85. package/templates/helpdesk/components/avatar.tsx +40 -0
  86. package/templates/helpdesk/components/command-palette.tsx +162 -0
  87. package/templates/helpdesk/components/data-table.tsx +90 -0
  88. package/templates/helpdesk/components/empty-state.tsx +43 -0
  89. package/templates/helpdesk/components/kbd.tsx +23 -0
  90. package/templates/helpdesk/components/new-ticket-dialog.tsx +147 -0
  91. package/templates/helpdesk/components/page-header.tsx +36 -0
  92. package/templates/helpdesk/components/priority-badge.tsx +61 -0
  93. package/templates/helpdesk/components/record-dialog.tsx +126 -0
  94. package/templates/helpdesk/components/sidebar.tsx +111 -0
  95. package/templates/helpdesk/components/sla-indicator.tsx +47 -0
  96. package/templates/helpdesk/components/thread.tsx +143 -0
  97. package/templates/helpdesk/components/ticket-list.tsx +78 -0
  98. package/templates/helpdesk/components/ui/badge.tsx +37 -0
  99. package/templates/helpdesk/components/ui/button.tsx +56 -0
  100. package/templates/helpdesk/components/ui/card.tsx +90 -0
  101. package/templates/helpdesk/components/ui/dialog.tsx +110 -0
  102. package/templates/helpdesk/components/ui/input.tsx +30 -0
  103. package/templates/helpdesk/components/ui/label.tsx +21 -0
  104. package/templates/helpdesk/components/ui/select.tsx +39 -0
  105. package/templates/helpdesk/components/ui/table.tsx +124 -0
  106. package/templates/helpdesk/components/ui/textarea.tsx +24 -0
  107. package/templates/helpdesk/components.json +20 -0
  108. package/templates/helpdesk/functions/createTicket.ts +62 -0
  109. package/templates/helpdesk/functions/replyToTicket.ts +55 -0
  110. package/templates/helpdesk/functions/seedWorkspace.ts +52 -0
  111. package/templates/helpdesk/functions/setTicketState.ts +58 -0
  112. package/templates/helpdesk/gitignore +10 -0
  113. package/templates/helpdesk/lib/format.ts +53 -0
  114. package/templates/helpdesk/lib/search.ts +78 -0
  115. package/templates/helpdesk/lib/seed.ts +185 -0
  116. package/templates/helpdesk/lib/tickets.ts +184 -0
  117. package/templates/helpdesk/lib/utils.ts +10 -0
  118. package/templates/helpdesk/package.json +39 -0
  119. package/templates/helpdesk/tests/components.test.tsx +202 -0
  120. package/templates/helpdesk/tests/search.test.ts +83 -0
  121. package/templates/helpdesk/tests/setup.ts +5 -0
  122. package/templates/helpdesk/tests/tickets.test.ts +215 -0
  123. package/templates/helpdesk/tsconfig.json +33 -0
  124. package/templates/inventory/AGENTS.md +116 -0
  125. package/templates/inventory/CLAUDE.md +7 -0
  126. package/templates/inventory/README.md +103 -0
  127. package/templates/inventory/app/error.tsx +43 -0
  128. package/templates/inventory/app/globals.css +201 -0
  129. package/templates/inventory/app/layout.tsx +31 -0
  130. package/templates/inventory/app/login/auth-form.tsx +122 -0
  131. package/templates/inventory/app/login/page.tsx +38 -0
  132. package/templates/inventory/app/movements/movements-view.tsx +103 -0
  133. package/templates/inventory/app/movements/page.tsx +17 -0
  134. package/templates/inventory/app/not-found.tsx +29 -0
  135. package/templates/inventory/app/page.tsx +34 -0
  136. package/templates/inventory/app/products/[id]/page.tsx +23 -0
  137. package/templates/inventory/app/products/[id]/product-view.tsx +178 -0
  138. package/templates/inventory/app/products-view.tsx +213 -0
  139. package/templates/inventory/app/robots.ts +12 -0
  140. package/templates/inventory/app/sitemap.ts +27 -0
  141. package/templates/inventory/app/workspace.tsx +172 -0
  142. package/templates/inventory/app.ts +154 -0
  143. package/templates/inventory/bunfig +4 -0
  144. package/templates/inventory/components/avatar.tsx +40 -0
  145. package/templates/inventory/components/command-palette.tsx +162 -0
  146. package/templates/inventory/components/data-table.tsx +90 -0
  147. package/templates/inventory/components/empty-state.tsx +43 -0
  148. package/templates/inventory/components/kbd.tsx +23 -0
  149. package/templates/inventory/components/movement-dialog.tsx +182 -0
  150. package/templates/inventory/components/page-header.tsx +36 -0
  151. package/templates/inventory/components/record-dialog.tsx +126 -0
  152. package/templates/inventory/components/sidebar.tsx +111 -0
  153. package/templates/inventory/components/stock-level.tsx +47 -0
  154. package/templates/inventory/components/summary-bar.tsx +56 -0
  155. package/templates/inventory/components/ui/badge.tsx +37 -0
  156. package/templates/inventory/components/ui/button.tsx +56 -0
  157. package/templates/inventory/components/ui/card.tsx +90 -0
  158. package/templates/inventory/components/ui/dialog.tsx +110 -0
  159. package/templates/inventory/components/ui/input.tsx +30 -0
  160. package/templates/inventory/components/ui/label.tsx +21 -0
  161. package/templates/inventory/components/ui/select.tsx +39 -0
  162. package/templates/inventory/components/ui/table.tsx +124 -0
  163. package/templates/inventory/components/ui/textarea.tsx +24 -0
  164. package/templates/inventory/components.json +20 -0
  165. package/templates/inventory/functions/recordMovement.ts +72 -0
  166. package/templates/inventory/functions/seedWorkspace.ts +40 -0
  167. package/templates/inventory/gitignore +10 -0
  168. package/templates/inventory/lib/format.ts +53 -0
  169. package/templates/inventory/lib/search.ts +78 -0
  170. package/templates/inventory/lib/seed.ts +102 -0
  171. package/templates/inventory/lib/stock.ts +207 -0
  172. package/templates/inventory/lib/utils.ts +10 -0
  173. package/templates/inventory/package.json +39 -0
  174. package/templates/inventory/tests/components.test.tsx +155 -0
  175. package/templates/inventory/tests/search.test.ts +83 -0
  176. package/templates/inventory/tests/setup.ts +5 -0
  177. package/templates/inventory/tests/stock.test.ts +255 -0
  178. package/templates/inventory/tsconfig.json +33 -0
  179. package/templates/invoices/AGENTS.md +116 -0
  180. package/templates/invoices/CLAUDE.md +7 -0
  181. package/templates/invoices/README.md +99 -0
  182. package/templates/invoices/app/clients/clients-view.tsx +130 -0
  183. package/templates/invoices/app/clients/page.tsx +17 -0
  184. package/templates/invoices/app/error.tsx +43 -0
  185. package/templates/invoices/app/globals.css +201 -0
  186. package/templates/invoices/app/invoices/[id]/invoice-view.tsx +253 -0
  187. package/templates/invoices/app/invoices/[id]/page.tsx +23 -0
  188. package/templates/invoices/app/invoices-view.tsx +183 -0
  189. package/templates/invoices/app/layout.tsx +31 -0
  190. package/templates/invoices/app/login/auth-form.tsx +122 -0
  191. package/templates/invoices/app/login/page.tsx +38 -0
  192. package/templates/invoices/app/not-found.tsx +29 -0
  193. package/templates/invoices/app/page.tsx +36 -0
  194. package/templates/invoices/app/robots.ts +12 -0
  195. package/templates/invoices/app/sitemap.ts +27 -0
  196. package/templates/invoices/app/workspace.tsx +174 -0
  197. package/templates/invoices/app.ts +169 -0
  198. package/templates/invoices/bunfig +4 -0
  199. package/templates/invoices/components/avatar.tsx +40 -0
  200. package/templates/invoices/components/command-palette.tsx +162 -0
  201. package/templates/invoices/components/data-table.tsx +90 -0
  202. package/templates/invoices/components/empty-state.tsx +43 -0
  203. package/templates/invoices/components/kbd.tsx +23 -0
  204. package/templates/invoices/components/line-items.tsx +172 -0
  205. package/templates/invoices/components/page-header.tsx +36 -0
  206. package/templates/invoices/components/payment-dialog.tsx +132 -0
  207. package/templates/invoices/components/record-dialog.tsx +126 -0
  208. package/templates/invoices/components/sidebar.tsx +111 -0
  209. package/templates/invoices/components/status-badge.tsx +35 -0
  210. package/templates/invoices/components/summary-bar.tsx +65 -0
  211. package/templates/invoices/components/totals-panel.tsx +70 -0
  212. package/templates/invoices/components/ui/badge.tsx +37 -0
  213. package/templates/invoices/components/ui/button.tsx +56 -0
  214. package/templates/invoices/components/ui/card.tsx +90 -0
  215. package/templates/invoices/components/ui/dialog.tsx +110 -0
  216. package/templates/invoices/components/ui/input.tsx +30 -0
  217. package/templates/invoices/components/ui/label.tsx +21 -0
  218. package/templates/invoices/components/ui/select.tsx +39 -0
  219. package/templates/invoices/components/ui/table.tsx +124 -0
  220. package/templates/invoices/components/ui/textarea.tsx +24 -0
  221. package/templates/invoices/components.json +20 -0
  222. package/templates/invoices/functions/createInvoice.ts +49 -0
  223. package/templates/invoices/functions/recordPayment.ts +72 -0
  224. package/templates/invoices/functions/seedWorkspace.ts +57 -0
  225. package/templates/invoices/functions/setInvoiceStatus.ts +35 -0
  226. package/templates/invoices/gitignore +10 -0
  227. package/templates/invoices/lib/billing.ts +259 -0
  228. package/templates/invoices/lib/format.ts +53 -0
  229. package/templates/invoices/lib/search.ts +78 -0
  230. package/templates/invoices/lib/seed.ts +190 -0
  231. package/templates/invoices/lib/utils.ts +10 -0
  232. package/templates/invoices/package.json +39 -0
  233. package/templates/invoices/tests/billing.test.ts +283 -0
  234. package/templates/invoices/tests/components.test.tsx +172 -0
  235. package/templates/invoices/tests/search.test.ts +83 -0
  236. package/templates/invoices/tests/setup.ts +5 -0
  237. package/templates/invoices/tsconfig.json +33 -0
  238. package/templates/projects/AGENTS.md +116 -0
  239. package/templates/projects/CLAUDE.md +7 -0
  240. package/templates/projects/README.md +96 -0
  241. package/templates/projects/app/clients/clients-view.tsx +126 -0
  242. package/templates/projects/app/clients/page.tsx +17 -0
  243. package/templates/projects/app/error.tsx +43 -0
  244. package/templates/projects/app/globals.css +201 -0
  245. package/templates/projects/app/layout.tsx +31 -0
  246. package/templates/projects/app/login/auth-form.tsx +122 -0
  247. package/templates/projects/app/login/page.tsx +38 -0
  248. package/templates/projects/app/not-found.tsx +29 -0
  249. package/templates/projects/app/page.tsx +30 -0
  250. package/templates/projects/app/projects/[id]/page.tsx +23 -0
  251. package/templates/projects/app/projects/[id]/project-view.tsx +179 -0
  252. package/templates/projects/app/projects-view.tsx +181 -0
  253. package/templates/projects/app/robots.ts +12 -0
  254. package/templates/projects/app/sitemap.ts +27 -0
  255. package/templates/projects/app/workspace.tsx +190 -0
  256. package/templates/projects/app.ts +186 -0
  257. package/templates/projects/bunfig +4 -0
  258. package/templates/projects/components/avatar.tsx +40 -0
  259. package/templates/projects/components/board-skeleton.tsx +37 -0
  260. package/templates/projects/components/budget-bar.tsx +61 -0
  261. package/templates/projects/components/command-palette.tsx +162 -0
  262. package/templates/projects/components/data-table.tsx +90 -0
  263. package/templates/projects/components/empty-state.tsx +43 -0
  264. package/templates/projects/components/kbd.tsx +23 -0
  265. package/templates/projects/components/page-header.tsx +36 -0
  266. package/templates/projects/components/record-dialog.tsx +126 -0
  267. package/templates/projects/components/sidebar.tsx +111 -0
  268. package/templates/projects/components/task-board.tsx +109 -0
  269. package/templates/projects/components/task-card.tsx +77 -0
  270. package/templates/projects/components/time-dialog.tsx +114 -0
  271. package/templates/projects/components/ui/badge.tsx +37 -0
  272. package/templates/projects/components/ui/button.tsx +56 -0
  273. package/templates/projects/components/ui/card.tsx +90 -0
  274. package/templates/projects/components/ui/dialog.tsx +110 -0
  275. package/templates/projects/components/ui/input.tsx +30 -0
  276. package/templates/projects/components/ui/label.tsx +21 -0
  277. package/templates/projects/components/ui/select.tsx +39 -0
  278. package/templates/projects/components/ui/table.tsx +124 -0
  279. package/templates/projects/components/ui/textarea.tsx +24 -0
  280. package/templates/projects/components.json +20 -0
  281. package/templates/projects/functions/logTime.ts +49 -0
  282. package/templates/projects/functions/moveTask.ts +40 -0
  283. package/templates/projects/functions/seedWorkspace.ts +59 -0
  284. package/templates/projects/gitignore +10 -0
  285. package/templates/projects/lib/format.ts +53 -0
  286. package/templates/projects/lib/search.ts +78 -0
  287. package/templates/projects/lib/seed.ts +140 -0
  288. package/templates/projects/lib/utils.ts +10 -0
  289. package/templates/projects/lib/work.ts +233 -0
  290. package/templates/projects/package.json +39 -0
  291. package/templates/projects/tests/components.test.tsx +200 -0
  292. package/templates/projects/tests/search.test.ts +83 -0
  293. package/templates/projects/tests/setup.ts +5 -0
  294. package/templates/projects/tests/work.test.ts +238 -0
  295. package/templates/projects/tsconfig.json +33 -0
@@ -0,0 +1,124 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ // The wrapper owns the horizontal scroll so a wide table never widens the page.
6
+ function Table({ className, ...props }: React.HTMLAttributes<HTMLTableElement>) {
7
+ return (
8
+ <div className="relative w-full overflow-x-auto">
9
+ <table
10
+ data-slot="table"
11
+ className={cn("w-full caption-bottom text-sm", className)}
12
+ {...props}
13
+ />
14
+ </div>
15
+ );
16
+ }
17
+
18
+ function TableHeader({
19
+ className,
20
+ ...props
21
+ }: React.HTMLAttributes<HTMLTableSectionElement>) {
22
+ return (
23
+ <thead
24
+ data-slot="table-header"
25
+ className={cn("[&_tr]:border-b", className)}
26
+ {...props}
27
+ />
28
+ );
29
+ }
30
+
31
+ function TableBody({
32
+ className,
33
+ ...props
34
+ }: React.HTMLAttributes<HTMLTableSectionElement>) {
35
+ return (
36
+ <tbody
37
+ data-slot="table-body"
38
+ className={cn("[&_tr:last-child]:border-0", className)}
39
+ {...props}
40
+ />
41
+ );
42
+ }
43
+
44
+ function TableFooter({
45
+ className,
46
+ ...props
47
+ }: React.HTMLAttributes<HTMLTableSectionElement>) {
48
+ return (
49
+ <tfoot
50
+ data-slot="table-footer"
51
+ className={cn("border-t bg-muted/50 font-medium", className)}
52
+ {...props}
53
+ />
54
+ );
55
+ }
56
+
57
+ function TableRow({
58
+ className,
59
+ ...props
60
+ }: React.HTMLAttributes<HTMLTableRowElement>) {
61
+ return (
62
+ <tr
63
+ data-slot="table-row"
64
+ className={cn(
65
+ "border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
66
+ className,
67
+ )}
68
+ {...props}
69
+ />
70
+ );
71
+ }
72
+
73
+ function TableHead({
74
+ className,
75
+ ...props
76
+ }: React.ThHTMLAttributes<HTMLTableCellElement>) {
77
+ return (
78
+ <th
79
+ data-slot="table-head"
80
+ className={cn(
81
+ "h-10 px-3 text-left align-middle text-xs font-medium text-muted-foreground",
82
+ className,
83
+ )}
84
+ {...props}
85
+ />
86
+ );
87
+ }
88
+
89
+ function TableCell({
90
+ className,
91
+ ...props
92
+ }: React.TdHTMLAttributes<HTMLTableCellElement>) {
93
+ return (
94
+ <td
95
+ data-slot="table-cell"
96
+ className={cn("p-3 align-middle", className)}
97
+ {...props}
98
+ />
99
+ );
100
+ }
101
+
102
+ function TableCaption({
103
+ className,
104
+ ...props
105
+ }: React.HTMLAttributes<HTMLTableCaptionElement>) {
106
+ return (
107
+ <caption
108
+ data-slot="table-caption"
109
+ className={cn("mt-4 text-sm text-muted-foreground", className)}
110
+ {...props}
111
+ />
112
+ );
113
+ }
114
+
115
+ export {
116
+ Table,
117
+ TableHeader,
118
+ TableBody,
119
+ TableFooter,
120
+ TableHead,
121
+ TableRow,
122
+ TableCell,
123
+ TableCaption,
124
+ };
@@ -0,0 +1,24 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ function Textarea({
6
+ className,
7
+ ...props
8
+ }: React.TextareaHTMLAttributes<HTMLTextAreaElement>) {
9
+ return (
10
+ <textarea
11
+ data-slot="textarea"
12
+ className={cn(
13
+ "flex min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm transition-colors",
14
+ "placeholder:text-muted-foreground",
15
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
16
+ "disabled:cursor-not-allowed disabled:opacity-50",
17
+ className,
18
+ )}
19
+ {...props}
20
+ />
21
+ );
22
+ }
23
+
24
+ export { Textarea };
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": false,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "app/globals.css",
9
+ "baseColor": "zinc",
10
+ "cssVariables": true
11
+ },
12
+ "aliases": {
13
+ "components": "@/components",
14
+ "utils": "@/lib/utils",
15
+ "ui": "@/components/ui",
16
+ "lib": "@/lib",
17
+ "hooks": "@/hooks"
18
+ },
19
+ "iconLibrary": "lucide"
20
+ }
@@ -0,0 +1,49 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+
3
+ /**
4
+ * Log time against a task.
5
+ *
6
+ * A mutation rather than a bare insert so `projectId` is taken from the TASK
7
+ * rather than trusted from the client — a mislabelled entry would land on
8
+ * another client\'s invoice, which is the worst failure this app has.
9
+ *
10
+ * Entries are append-only (see the policy). A correction is a NEGATIVE entry,
11
+ * which stays visible in the history rather than quietly changing what someone
12
+ * was billed.
13
+ */
14
+ export default mutation<
15
+ { taskId: string; minutes: number; note?: string },
16
+ { ok: true; id: string }
17
+ >({
18
+ auth: "user",
19
+ args: {
20
+ taskId: v.id("Task"),
21
+ minutes: v.number(),
22
+ note: v.optional(v.string()),
23
+ },
24
+ async handler(ctx, args) {
25
+ const minutes = Math.trunc(Number(args.minutes) || 0);
26
+ if (minutes === 0) {
27
+ throw ctx.error("INVALID_ARGS", "Log a non-zero number of minutes.");
28
+ }
29
+ // A day has 1440 minutes; anything past that is a typo, and a typo on a
30
+ // timesheet becomes a typo on an invoice.
31
+ if (Math.abs(minutes) > 1440) {
32
+ throw ctx.error("INVALID_ARGS", "That's more than a day — check the value.");
33
+ }
34
+
35
+ const task = await ctx.db.get("Task", args.taskId);
36
+ if (!task) throw ctx.error("NOT_FOUND", "Task not found.");
37
+
38
+ const id = await ctx.db.insert("TimeEntry", {
39
+ taskId: args.taskId,
40
+ projectId: task.projectId,
41
+ minutes,
42
+ note: args.note?.trim() || null,
43
+ userId: ctx.auth.userId,
44
+ spentOn: new Date().toISOString(),
45
+ });
46
+
47
+ return { ok: true, id: id as string } as const;
48
+ },
49
+ });
@@ -0,0 +1,40 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+ import { isValidTaskStatus } from "../lib/work";
3
+
4
+ /**
5
+ * Move a task to another column.
6
+ *
7
+ * Validated server-side: the board only sends known statuses, but the endpoint
8
+ * is reachable directly and an unknown status would strand the task in a column
9
+ * that never renders.
10
+ *
11
+ * The new position is the end of the target column, computed here rather than
12
+ * sent by the client — two people dragging into the same column simultaneously
13
+ * would otherwise both claim the same index.
14
+ */
15
+ export default mutation<{ taskId: string; status: string }, { ok: true }>({
16
+ auth: "user",
17
+ args: { taskId: v.id("Task"), status: v.string() },
18
+ async handler(ctx, args) {
19
+ if (!isValidTaskStatus(args.status)) {
20
+ throw ctx.error("INVALID_ARGS", `Unknown status "${args.status}".`);
21
+ }
22
+
23
+ const task = await ctx.db.get("Task", args.taskId);
24
+ if (!task) throw ctx.error("NOT_FOUND", "Task not found.");
25
+ if (task.status === args.status) return { ok: true } as const;
26
+
27
+ const siblings = (await ctx.db.query("Task", {
28
+ projectId: task.projectId,
29
+ })) as Array<{ status?: string; position?: number }>;
30
+ const end = siblings.filter((row) => row.status === args.status).length;
31
+
32
+ await ctx.db.update("Task", args.taskId, {
33
+ status: args.status,
34
+ position: end,
35
+ updatedAt: new Date().toISOString(),
36
+ });
37
+
38
+ return { ok: true } as const;
39
+ },
40
+ });
@@ -0,0 +1,59 @@
1
+ import { mutation } from "@pylonsync/functions";
2
+ import { shapeSeed } from "../lib/seed";
3
+
4
+ /**
5
+ * Fill a brand-new delivery workspace once.
6
+ *
7
+ * Seeds time as ENTRIES rather than totals, because logged time here is the sum
8
+ * of the ledger — a seed that wrote totals would contradict the app\'s own model.
9
+ * One project lands over its budget so that state is visible on first load.
10
+ *
11
+ * Returns immediately if any project exists, so it is safe on every load.
12
+ */
13
+ export default mutation<Record<string, never>, { seeded: boolean }>({
14
+ auth: "user",
15
+ args: {},
16
+ async handler(ctx) {
17
+ const existing = await ctx.db.query("Project", { $limit: 1 });
18
+ if (existing.length > 0) return { seeded: false };
19
+
20
+ const seed = shapeSeed();
21
+ const me = ctx.auth.userId;
22
+
23
+ const clientIds = new Map<string, string>();
24
+ for (const client of seed.clients) {
25
+ const id = await ctx.db.insert("Client", client.row);
26
+ clientIds.set(client.key, id as string);
27
+ }
28
+
29
+ const projectIds = new Map<string, string>();
30
+ for (const project of seed.projects) {
31
+ const id = await ctx.db.insert("Project", {
32
+ ...project.row,
33
+ clientId: clientIds.get(project.client) ?? null,
34
+ });
35
+ projectIds.set(project.key, id as string);
36
+ }
37
+
38
+ const taskIds = new Map<string, string>();
39
+ for (const task of seed.tasks) {
40
+ const id = await ctx.db.insert("Task", {
41
+ ...task.row,
42
+ projectId: projectIds.get(task.project) ?? null,
43
+ assigneeId: me,
44
+ });
45
+ taskIds.set(task.key, id as string);
46
+ }
47
+
48
+ for (const entry of seed.entries) {
49
+ await ctx.db.insert("TimeEntry", {
50
+ ...entry.row,
51
+ taskId: taskIds.get(entry.task) ?? null,
52
+ projectId: projectIds.get(entry.project) ?? null,
53
+ userId: me,
54
+ });
55
+ }
56
+
57
+ return { seeded: true };
58
+ },
59
+ });
@@ -0,0 +1,10 @@
1
+ node_modules/
2
+ .pylon/
3
+ pylon.manifest.json
4
+ pylon.client.ts
5
+ web/dist/
6
+ *.db
7
+ *.db-*
8
+ .env
9
+ .env.local
10
+ .DS_Store
@@ -0,0 +1,53 @@
1
+ // Small display helpers shared across views. Pure, so the timeline and the
2
+ // queue can't drift on how they render the same instant.
3
+
4
+ /** Initials for an avatar: "Dana Whitfield" → "DW", "jordan@x.com" → "JO". */
5
+ export function initials(nameOrEmail: string | null | undefined): string {
6
+ const raw = (nameOrEmail ?? "").trim();
7
+ if (!raw) return "?";
8
+ const name = raw.includes("@") ? raw.split("@")[0] : raw;
9
+ const parts = name.split(/[\s._-]+/).filter(Boolean);
10
+ if (parts.length === 0) return "?";
11
+ if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
12
+ return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
13
+ }
14
+
15
+ /** A stable accent per record, so the same person keeps their colour. */
16
+ export function accentIndex(seed: string, buckets = 6): number {
17
+ let hash = 0;
18
+ for (let i = 0; i < seed.length; i += 1) {
19
+ hash = (hash * 31 + seed.charCodeAt(i)) | 0;
20
+ }
21
+ return Math.abs(hash) % buckets;
22
+ }
23
+
24
+ /** "just now" / "4h ago" / "Mar 3". */
25
+ export function relativeTime(
26
+ iso: string | null | undefined,
27
+ now: number = Date.now(),
28
+ ): string {
29
+ if (!iso) return "";
30
+ const then = Date.parse(iso);
31
+ if (!Number.isFinite(then)) return "";
32
+ const seconds = Math.round((now - then) / 1000);
33
+ if (seconds < 45) return "just now";
34
+ const minutes = Math.round(seconds / 60);
35
+ if (minutes < 60) return `${minutes}m ago`;
36
+ const hours = Math.round(minutes / 60);
37
+ if (hours < 24) return `${hours}h ago`;
38
+ const days = Math.round(hours / 24);
39
+ if (days < 7) return `${days}d ago`;
40
+ return new Date(then).toLocaleDateString("en-US", { month: "short", day: "numeric" });
41
+ }
42
+
43
+ /** "3h 20m" / "45m" / "2d" — a duration an agent reads at a glance. */
44
+ export function duration(minutes: number): string {
45
+ const abs = Math.abs(Math.round(minutes));
46
+ if (abs < 60) return `${abs}m`;
47
+ if (abs < 1440) {
48
+ const hours = Math.floor(abs / 60);
49
+ const rest = abs % 60;
50
+ return rest === 0 ? `${hours}h` : `${hours}h ${rest}m`;
51
+ }
52
+ return `${Math.floor(abs / 1440)}d`;
53
+ }
@@ -0,0 +1,78 @@
1
+ // Client-side search powering the ⌘K palette.
2
+ //
3
+ // A small team's CRM fits in the synced replica, so search runs locally against
4
+ // data already in memory: results appear as you type, with no request per
5
+ // keystroke. Swap in `db.useSearch` (Pylon's FTS) when a workspace outgrows
6
+ // that — the component boundary doesn't change, only this file.
7
+
8
+ export interface SearchItem {
9
+ id: string;
10
+ type: "deal" | "company" | "contact";
11
+ title: string;
12
+ /** Second line: the company for a contact, the domain for a company. */
13
+ subtitle?: string;
14
+ href: string;
15
+ /** Extra text matched but not displayed — an email, say. */
16
+ keywords?: string;
17
+ }
18
+
19
+ export interface Scored extends SearchItem {
20
+ score: number;
21
+ }
22
+
23
+ /**
24
+ * Rank items against a query. Prefix matches beat word-boundary matches, which
25
+ * beat substring matches; ties break on the shorter title, so "Acme" ranks
26
+ * above "Acme Corporation Holdings" for the query "acme".
27
+ */
28
+ export function searchItems(
29
+ items: SearchItem[],
30
+ query: string,
31
+ limit = 8,
32
+ ): SearchItem[] {
33
+ const q = query.trim().toLowerCase();
34
+ if (!q) return items.slice(0, limit);
35
+
36
+ const scored: Scored[] = [];
37
+ for (const item of items) {
38
+ const score = scoreItem(item, q);
39
+ if (score > 0) scored.push({ ...item, score });
40
+ }
41
+ scored.sort(
42
+ (a, b) => b.score - a.score || a.title.length - b.title.length ||
43
+ a.title.localeCompare(b.title),
44
+ );
45
+ return scored.slice(0, limit);
46
+ }
47
+
48
+ function scoreItem(item: SearchItem, q: string): number {
49
+ const title = item.title.toLowerCase();
50
+ if (title.startsWith(q)) return 100;
51
+ if (wordStart(title, q)) return 80;
52
+ if (title.includes(q)) return 60;
53
+
54
+ const haystack = `${item.subtitle ?? ""} ${item.keywords ?? ""}`.toLowerCase();
55
+ if (wordStart(haystack, q)) return 40;
56
+ if (haystack.includes(q)) return 20;
57
+ return 0;
58
+ }
59
+
60
+ function wordStart(haystack: string, needle: string): boolean {
61
+ let from = 0;
62
+ for (;;) {
63
+ const at = haystack.indexOf(needle, from);
64
+ if (at === -1) return false;
65
+ if (at === 0 || /[\s._@/-]/.test(haystack[at - 1])) return true;
66
+ from = at + 1;
67
+ }
68
+ }
69
+
70
+ /** Move through a list with arrow keys, wrapping at both ends. */
71
+ export function moveSelection(
72
+ current: number,
73
+ delta: number,
74
+ length: number,
75
+ ): number {
76
+ if (length === 0) return 0;
77
+ return (current + delta + length) % length;
78
+ }
@@ -0,0 +1,140 @@
1
+ // Demo data for a brand-new delivery workspace.
2
+ //
3
+ // An empty project list shows nothing about boards, budgets, or time. The first
4
+ // sign-in seeds two live projects — one comfortably inside its budget, one over
5
+ // it — so the budget states are visible rather than theoretical.
6
+
7
+ export interface SeedClient {
8
+ key: string;
9
+ name: string;
10
+ email: string;
11
+ }
12
+
13
+ export interface SeedProject {
14
+ key: string;
15
+ client: string;
16
+ name: string;
17
+ status: string;
18
+ /** Agreed budget in hours. */
19
+ budgetHours: number;
20
+ hourlyRate: number;
21
+ dueInDays: number;
22
+ }
23
+
24
+ export interface SeedTask {
25
+ key: string;
26
+ project: string;
27
+ title: string;
28
+ status: string;
29
+ estimateHours: number;
30
+ /** Hours logged against it; each becomes one or more entries. */
31
+ loggedHours: number[];
32
+ }
33
+
34
+ export const SEED_CLIENTS: SeedClient[] = [
35
+ { key: "northwind", name: "Northwind Logistics", email: "dana@northwind.co" },
36
+ { key: "hallmark", name: "Hallmark Dental", email: "priya@hallmarkdental.com" },
37
+ ];
38
+
39
+ export const SEED_PROJECTS: SeedProject[] = [
40
+ {
41
+ key: "dispatch",
42
+ client: "northwind",
43
+ name: "Dispatch portal",
44
+ status: "active",
45
+ budgetHours: 80,
46
+ hourlyRate: 165,
47
+ dueInDays: 24,
48
+ },
49
+ // Over budget: 40h agreed, ~46h logged.
50
+ {
51
+ key: "intake",
52
+ client: "hallmark",
53
+ name: "Patient intake rebuild",
54
+ status: "active",
55
+ budgetHours: 40,
56
+ hourlyRate: 150,
57
+ dueInDays: 9,
58
+ },
59
+ ];
60
+
61
+ export const SEED_TASKS: SeedTask[] = [
62
+ { key: "d-auth", project: "dispatch", title: "SSO with their identity provider", status: "done", estimateHours: 8, loggedHours: [3, 4.5] },
63
+ { key: "d-model", project: "dispatch", title: "Route + stop data model", status: "done", estimateHours: 6, loggedHours: [5] },
64
+ { key: "d-board", project: "dispatch", title: "Dispatcher board UI", status: "doing", estimateHours: 20, loggedHours: [6, 4] },
65
+ { key: "d-mobile", project: "dispatch", title: "Driver mobile view", status: "review", estimateHours: 12, loggedHours: [9] },
66
+ { key: "d-report", project: "dispatch", title: "Weekly utilisation report", status: "todo", estimateHours: 6, loggedHours: [] },
67
+ { key: "d-import", project: "dispatch", title: "Import legacy stop history", status: "todo", estimateHours: 10, loggedHours: [] },
68
+
69
+ { key: "i-forms", project: "intake", title: "Form builder", status: "done", estimateHours: 16, loggedHours: [8, 7, 4] },
70
+ { key: "i-sig", project: "intake", title: "Consent signatures", status: "done", estimateHours: 8, loggedHours: [6, 5] },
71
+ { key: "i-sync", project: "intake", title: "Sync to practice software", status: "doing", estimateHours: 12, loggedHours: [9, 7] },
72
+ { key: "i-train", project: "intake", title: "Staff training session", status: "todo", estimateHours: 4, loggedHours: [] },
73
+ ];
74
+
75
+ export interface ShapedSeed {
76
+ clients: Array<{ key: string; row: Record<string, unknown> }>;
77
+ projects: Array<{ key: string; client: string; row: Record<string, unknown> }>;
78
+ tasks: Array<{ key: string; project: string; row: Record<string, unknown> }>;
79
+ entries: Array<{ task: string; project: string; row: Record<string, unknown> }>;
80
+ }
81
+
82
+ export function shapeSeed(now: number = Date.now()): ShapedSeed {
83
+ const at = (days: number) => new Date(now + days * 86_400_000).toISOString();
84
+
85
+ const tasks: ShapedSeed["tasks"] = [];
86
+ const entries: ShapedSeed["entries"] = [];
87
+ const positions = new Map<string, number>();
88
+
89
+ SEED_TASKS.forEach((task, index) => {
90
+ const columnKey = `${task.project}:${task.status}`;
91
+ const position = positions.get(columnKey) ?? 0;
92
+ positions.set(columnKey, position + 1);
93
+
94
+ tasks.push({
95
+ key: task.key,
96
+ project: task.project,
97
+ row: {
98
+ title: task.title,
99
+ status: task.status,
100
+ estimateMinutes: Math.round(task.estimateHours * 60),
101
+ position,
102
+ createdAt: at(-30 + index),
103
+ updatedAt: at(-2),
104
+ },
105
+ });
106
+
107
+ task.loggedHours.forEach((hours, entryIndex) => {
108
+ entries.push({
109
+ task: task.key,
110
+ project: task.project,
111
+ row: {
112
+ minutes: Math.round(hours * 60),
113
+ note: null,
114
+ spentOn: at(-14 + entryIndex * 2),
115
+ },
116
+ });
117
+ });
118
+ });
119
+
120
+ return {
121
+ clients: SEED_CLIENTS.map((c) => ({
122
+ key: c.key,
123
+ row: { name: c.name, email: c.email, createdAt: at(-90) },
124
+ })),
125
+ projects: SEED_PROJECTS.map((p) => ({
126
+ key: p.key,
127
+ client: p.client,
128
+ row: {
129
+ name: p.name,
130
+ status: p.status,
131
+ budgetMinutes: Math.round(p.budgetHours * 60),
132
+ hourlyRateCents: Math.round(p.hourlyRate * 100),
133
+ dueDate: at(p.dueInDays),
134
+ createdAt: at(-45),
135
+ },
136
+ })),
137
+ tasks,
138
+ entries,
139
+ };
140
+ }
@@ -0,0 +1,10 @@
1
+ import { type ClassValue, clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ // `cn` — the shadcn class merger. clsx resolves conditional/array class
5
+ // inputs; tailwind-merge then dedupes conflicting Tailwind utilities so
6
+ // the last one wins (e.g. `cn("px-2", "px-4")` → "px-4"). Every shadcn
7
+ // component routes its className through this.
8
+ export function cn(...inputs: ClassValue[]) {
9
+ return twMerge(clsx(inputs));
10
+ }