@pylonsync/create-pylon 0.3.344 → 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 (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,43 @@
1
+ import React from "react";
2
+ import { type ErrorBoundaryProps } from "@pylonsync/react";
3
+ import { Button } from "@/components/ui/button";
4
+
5
+ // `app/error.tsx` → the error boundary for this segment. It catches a throw
6
+ // in any page/layout below it and renders at HTTP 500. It's HYDRATED, so
7
+ // this is a real interactive client component: `reset()` re-attempts the
8
+ // route, and useState/onClick work. The thrown error reaches the client as
9
+ // `{ message, digest }` only — the stack stays in the dev overlay
10
+ // (PYLON_DEV_MODE) and the server logs, never in the page.
11
+ export default function Error({ error, reset }: ErrorBoundaryProps) {
12
+ const [tries, setTries] = React.useState(0);
13
+ return (
14
+ <div className="space-y-6">
15
+ <section>
16
+ <h1 className="text-2xl font-semibold tracking-tight">
17
+ Something went wrong
18
+ </h1>
19
+ <p className="mt-2 text-muted-foreground">{error.message}</p>
20
+ {error.digest ? (
21
+ <p className="mt-1 text-xs text-muted-foreground/70">
22
+ Reference: <code>{error.digest}</code>
23
+ </p>
24
+ ) : null}
25
+ </section>
26
+ <div className="flex items-center gap-3">
27
+ <Button
28
+ onClick={() => {
29
+ setTries((n) => n + 1);
30
+ reset();
31
+ }}
32
+ >
33
+ Try again
34
+ </Button>
35
+ {tries > 0 ? (
36
+ <span className="text-sm text-muted-foreground">
37
+ Retried {tries} {tries === 1 ? "time" : "times"}
38
+ </span>
39
+ ) : null}
40
+ </div>
41
+ </div>
42
+ );
43
+ }
@@ -0,0 +1,201 @@
1
+ @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+
4
+ @source "../app/**/*.{tsx,ts,jsx,js}";
5
+ @source "../components/**/*.{tsx,ts,jsx,js}";
6
+
7
+ @custom-variant dark (&:where(.dark, .dark *));
8
+
9
+ /* ---------------------------------------------------------------------------
10
+ Design tokens.
11
+
12
+ Tuned for a dense internal tool rather than a marketing page: a near-neutral
13
+ surface ramp, hairline borders instead of shadows, and one accent used
14
+ sparingly so status colour still means something. Dark is the primary theme —
15
+ `app/layout.tsx` sets `class="dark"` — and light is a real, tested
16
+ alternative, not an afterthought.
17
+
18
+ These are the same variable names shadcn/ui generates against, so
19
+ `npx shadcn@latest add <component>` drops in already themed.
20
+ --------------------------------------------------------------------------- */
21
+ :root {
22
+ --radius: 0.5rem;
23
+
24
+ --background: oklch(1 0 0);
25
+ --foreground: oklch(0.21 0.006 285.9);
26
+ --card: oklch(1 0 0);
27
+ --card-foreground: var(--foreground);
28
+ --popover: oklch(1 0 0);
29
+ --popover-foreground: var(--foreground);
30
+
31
+ --primary: oklch(0.55 0.204 274.5);
32
+ --primary-foreground: oklch(1 0 0);
33
+ --secondary: oklch(0.968 0.002 286.4);
34
+ --secondary-foreground: oklch(0.27 0.006 286);
35
+ --muted: oklch(0.972 0.002 286.4);
36
+ --muted-foreground: oklch(0.53 0.013 286);
37
+ --accent: oklch(0.96 0.003 286.4);
38
+ --accent-foreground: oklch(0.27 0.006 286);
39
+ --destructive: oklch(0.58 0.216 27.3);
40
+
41
+ --border: oklch(0.918 0.004 286.3);
42
+ --input: oklch(0.918 0.004 286.3);
43
+ --ring: oklch(0.55 0.204 274.5);
44
+
45
+ /* Surfaces above the page: sidebar, popovers, hover rows. */
46
+ --surface-1: oklch(0.985 0.001 286.4);
47
+ --surface-2: oklch(0.968 0.002 286.4);
48
+
49
+ /* Pipeline + status. Muted on purpose — a board of saturated chips is
50
+ unreadable, and the eye should land on the one that's actually urgent. */
51
+ --stage-lead: oklch(0.62 0.03 264);
52
+ --stage-qualified: oklch(0.6 0.13 240);
53
+ --stage-proposal: oklch(0.68 0.15 78);
54
+ --stage-won: oklch(0.6 0.15 155);
55
+ --stage-lost: oklch(0.6 0.11 25);
56
+ }
57
+
58
+ .dark {
59
+ --background: oklch(0.165 0.004 285.9);
60
+ --foreground: oklch(0.93 0.002 286.3);
61
+ --card: oklch(0.196 0.005 285.9);
62
+ --card-foreground: var(--foreground);
63
+ --popover: oklch(0.216 0.006 285.9);
64
+ --popover-foreground: var(--foreground);
65
+
66
+ --primary: oklch(0.62 0.19 274.5);
67
+ --primary-foreground: oklch(1 0 0);
68
+ --secondary: oklch(0.25 0.006 286);
69
+ --secondary-foreground: oklch(0.93 0.002 286.3);
70
+ --muted: oklch(0.25 0.006 286);
71
+ --muted-foreground: oklch(0.65 0.012 286);
72
+ --accent: oklch(0.26 0.006 286);
73
+ --accent-foreground: oklch(0.93 0.002 286.3);
74
+ --destructive: oklch(0.65 0.19 25);
75
+
76
+ --border: oklch(1 0 0 / 9%);
77
+ --input: oklch(1 0 0 / 12%);
78
+ --ring: oklch(0.62 0.19 274.5);
79
+
80
+ --surface-1: oklch(0.186 0.004 285.9);
81
+ --surface-2: oklch(0.216 0.006 285.9);
82
+
83
+ --stage-lead: oklch(0.68 0.03 264);
84
+ --stage-qualified: oklch(0.68 0.13 240);
85
+ --stage-proposal: oklch(0.75 0.14 78);
86
+ --stage-won: oklch(0.7 0.15 155);
87
+ --stage-lost: oklch(0.68 0.14 25);
88
+ }
89
+
90
+ @theme inline {
91
+ --radius-sm: calc(var(--radius) - 3px);
92
+ --radius-md: calc(var(--radius) - 1px);
93
+ --radius-lg: var(--radius);
94
+ --radius-xl: calc(var(--radius) + 4px);
95
+
96
+ --color-background: var(--background);
97
+ --color-foreground: var(--foreground);
98
+ --color-card: var(--card);
99
+ --color-card-foreground: var(--card-foreground);
100
+ --color-popover: var(--popover);
101
+ --color-popover-foreground: var(--popover-foreground);
102
+ --color-primary: var(--primary);
103
+ --color-primary-foreground: var(--primary-foreground);
104
+ --color-secondary: var(--secondary);
105
+ --color-secondary-foreground: var(--secondary-foreground);
106
+ --color-muted: var(--muted);
107
+ --color-muted-foreground: var(--muted-foreground);
108
+ --color-accent: var(--accent);
109
+ --color-accent-foreground: var(--accent-foreground);
110
+ --color-destructive: var(--destructive);
111
+ --color-border: var(--border);
112
+ --color-input: var(--input);
113
+ --color-ring: var(--ring);
114
+
115
+ --color-surface-1: var(--surface-1);
116
+ --color-surface-2: var(--surface-2);
117
+
118
+ --color-stage-lead: var(--stage-lead);
119
+ --color-stage-qualified: var(--stage-qualified);
120
+ --color-stage-proposal: var(--stage-proposal);
121
+ --color-stage-won: var(--stage-won);
122
+ --color-stage-lost: var(--stage-lost);
123
+ }
124
+
125
+ @layer base {
126
+ *,
127
+ ::after,
128
+ ::before,
129
+ ::backdrop,
130
+ ::file-selector-button {
131
+ border-color: var(--color-border, currentColor);
132
+ outline-color: var(--color-ring);
133
+ }
134
+
135
+ html {
136
+ /* An internal tool is a fixed shell with scrolling panes, not a scrolling
137
+ document — this keeps the sidebar and header from drifting. */
138
+ height: 100%;
139
+ -webkit-tap-highlight-color: transparent;
140
+ }
141
+
142
+ body {
143
+ height: 100%;
144
+ background-color: var(--color-background);
145
+ color: var(--color-foreground);
146
+ /* 13px is the density these screens are designed at; the slight negative
147
+ tracking is what keeps Inter from looking loose at that size. */
148
+ font-size: 13px;
149
+ line-height: 1.5;
150
+ letter-spacing: -0.006em;
151
+ -webkit-font-smoothing: antialiased;
152
+ text-rendering: optimizeLegibility;
153
+ }
154
+
155
+ button {
156
+ cursor: pointer;
157
+ }
158
+
159
+ /* Numbers that sit in columns must not jitter as they change. */
160
+ .tabular,
161
+ table {
162
+ font-variant-numeric: tabular-nums;
163
+ }
164
+
165
+ /* Thin, self-effacing scrollbars — the board has several at once. */
166
+ * {
167
+ scrollbar-width: thin;
168
+ scrollbar-color: color-mix(in oklab, var(--color-foreground) 18%, transparent)
169
+ transparent;
170
+ }
171
+ ::-webkit-scrollbar {
172
+ width: 10px;
173
+ height: 10px;
174
+ }
175
+ ::-webkit-scrollbar-thumb {
176
+ background-color: color-mix(in oklab, var(--color-foreground) 16%, transparent);
177
+ border: 3px solid transparent;
178
+ background-clip: content-box;
179
+ border-radius: 999px;
180
+ }
181
+ ::-webkit-scrollbar-thumb:hover {
182
+ background-color: color-mix(in oklab, var(--color-foreground) 28%, transparent);
183
+ }
184
+ ::-webkit-scrollbar-corner {
185
+ background: transparent;
186
+ }
187
+
188
+ /* Only show focus rings for keyboard users; a mouse click shouldn't ring. */
189
+ :focus-visible {
190
+ outline: 2px solid var(--color-ring);
191
+ outline-offset: 1px;
192
+ }
193
+ }
194
+
195
+ @layer components {
196
+ /* A hairline that reads at 1px on any DPR without the doubling you get from
197
+ a plain border on retina. */
198
+ .hairline {
199
+ box-shadow: inset 0 -1px 0 0 var(--color-border);
200
+ }
201
+ }
@@ -0,0 +1,253 @@
1
+ "use client";
2
+
3
+ import React, { useState } from "react";
4
+ import { Link, callFn, db, useRouter } from "@pylonsync/react";
5
+ import { ArrowLeft, Trash2 } from "lucide-react";
6
+ import { Button } from "@/components/ui/button";
7
+ import { Select } from "@/components/ui/select";
8
+ import { PageHeader } from "@/components/page-header";
9
+ import { EmptyState } from "@/components/empty-state";
10
+ import { StatusBadge } from "@/components/status-badge";
11
+ import { LineItems } from "@/components/line-items";
12
+ import { TotalsPanel } from "@/components/totals-panel";
13
+ import { PaymentDialog } from "@/components/payment-dialog";
14
+ import { relativeTime } from "@/lib/format";
15
+ import { STATUSES, displayStatus, money, totals } from "@/lib/billing";
16
+ import { Workspace } from "../../workspace";
17
+
18
+ export function InvoiceView({
19
+ email,
20
+ invoiceId,
21
+ }: {
22
+ email: string;
23
+ invoiceId: string;
24
+ }) {
25
+ const router = useRouter();
26
+ const [payOpen, setPayOpen] = useState(false);
27
+
28
+ return (
29
+ <Workspace email={email} pathname="/">
30
+ {(data) => {
31
+ const invoice = data.invoices.find((i) => i.id === invoiceId);
32
+
33
+ if (!invoice) {
34
+ return (
35
+ <>
36
+ <PageHeader title="Invoice" />
37
+ <EmptyState
38
+ title={data.loading ? "Loading…" : "Invoice not found"}
39
+ description={
40
+ data.loading ? undefined : "It may have been deleted, or the link is wrong."
41
+ }
42
+ action={
43
+ data.loading ? undefined : (
44
+ <Button size="sm" variant="secondary" asChild>
45
+ <Link href="/">Back to invoices</Link>
46
+ </Button>
47
+ )
48
+ }
49
+ />
50
+ </>
51
+ );
52
+ }
53
+
54
+ const t = totals(invoice, data.items, data.payments);
55
+ const shown = displayStatus(invoice, t.balanceCents);
56
+ const client = data.clients.find((c) => c.id === invoice.clientId);
57
+ const lines = data.items.filter((item) => item.invoiceId === invoice.id);
58
+ const paid = data.payments
59
+ .filter((payment) => payment.invoiceId === invoice.id)
60
+ .sort((a, b) => (b.paidAt ?? "").localeCompare(a.paidAt ?? ""));
61
+ // A sent invoice is a document someone is paying against; editing its
62
+ // lines after the fact desyncs it from the copy in their inbox.
63
+ const editable = invoice.status === "draft";
64
+
65
+ return (
66
+ <>
67
+ <PageHeader title={invoice.number}>
68
+ <StatusBadge status={shown} />
69
+ <div className="w-28">
70
+ <Select
71
+ aria-label="Status"
72
+ value={invoice.status}
73
+ className="h-7 text-[12px]"
74
+ onChange={(event) =>
75
+ void callFn("setInvoiceStatus", {
76
+ invoiceId: invoice.id,
77
+ status: event.target.value,
78
+ })
79
+ }
80
+ >
81
+ {STATUSES.map((s) => (
82
+ <option key={s.id} value={s.id}>
83
+ {s.label}
84
+ </option>
85
+ ))}
86
+ </Select>
87
+ </div>
88
+ {t.balanceCents > 0 && invoice.status !== "void" ? (
89
+ <Button size="sm" onClick={() => setPayOpen(true)}>
90
+ Record payment
91
+ </Button>
92
+ ) : null}
93
+ {editable ? (
94
+ <Button
95
+ size="sm"
96
+ variant="ghost"
97
+ onClick={async () => {
98
+ await db.delete("Invoice", invoice.id);
99
+ router.push("/");
100
+ }}
101
+ className="text-muted-foreground hover:text-destructive"
102
+ >
103
+ <Trash2 />
104
+ </Button>
105
+ ) : null}
106
+ </PageHeader>
107
+
108
+ <div className="min-h-0 flex-1 overflow-y-auto">
109
+ <div className="mx-auto flex max-w-3xl flex-col gap-6 px-6 py-6">
110
+ <Link
111
+ href="/"
112
+ className="inline-flex w-fit items-center gap-1.5 text-[12px] text-muted-foreground transition-colors hover:text-foreground"
113
+ >
114
+ <ArrowLeft className="size-3.5" />
115
+ Invoices
116
+ </Link>
117
+
118
+ <dl className="grid grid-cols-2 gap-x-6 gap-y-4 rounded-lg border border-border bg-card p-4 sm:grid-cols-4">
119
+ <Field label="Client">
120
+ <Select
121
+ aria-label="Client"
122
+ value={invoice.clientId ?? ""}
123
+ className="h-7 text-[12px]"
124
+ onChange={(event) =>
125
+ void db.update("Invoice", invoice.id, {
126
+ clientId: event.target.value || null,
127
+ })
128
+ }
129
+ >
130
+ <option value="">—</option>
131
+ {data.clients.map((c) => (
132
+ <option key={c.id} value={c.id}>
133
+ {c.name}
134
+ </option>
135
+ ))}
136
+ </Select>
137
+ </Field>
138
+ <Field label="Issued">
139
+ {invoice.issueDate
140
+ ? new Date(invoice.issueDate).toLocaleDateString("en-US", {
141
+ month: "short",
142
+ day: "numeric",
143
+ year: "numeric",
144
+ })
145
+ : "—"}
146
+ </Field>
147
+ <Field label="Due">
148
+ {invoice.dueDate
149
+ ? new Date(invoice.dueDate).toLocaleDateString("en-US", {
150
+ month: "short",
151
+ day: "numeric",
152
+ year: "numeric",
153
+ })
154
+ : "—"}
155
+ </Field>
156
+ <Field label="Balance">
157
+ <span
158
+ className={
159
+ "tabular font-medium" +
160
+ (t.balanceCents > 0 ? " text-destructive" : "")
161
+ }
162
+ >
163
+ {money(t.balanceCents)}
164
+ </span>
165
+ </Field>
166
+ </dl>
167
+
168
+ {client?.address ? (
169
+ <div className="text-[12px] text-muted-foreground">
170
+ <p className="mb-1 font-medium text-foreground">Billed to</p>
171
+ <p className="whitespace-pre-wrap">{client.address}</p>
172
+ </div>
173
+ ) : null}
174
+
175
+ <LineItems
176
+ items={lines}
177
+ editable={editable}
178
+ onAdd={async (draft) => {
179
+ await db.insert("LineItem", {
180
+ ...draft,
181
+ invoiceId: invoice.id,
182
+ position: lines.length,
183
+ });
184
+ }}
185
+ onRemove={async (id) => {
186
+ await db.delete("LineItem", id);
187
+ }}
188
+ />
189
+
190
+ <TotalsPanel totals={t} taxRateBps={invoice.taxRateBps} />
191
+
192
+ {paid.length > 0 ? (
193
+ <section>
194
+ <h2 className="mb-2 text-[13px] font-semibold">Payments</h2>
195
+ <ul className="space-y-1.5">
196
+ {paid.map((payment) => (
197
+ <li
198
+ key={payment.id}
199
+ className="flex items-center gap-3 rounded-md border border-border bg-card px-3 py-2 text-[12px]"
200
+ >
201
+ <span className="tabular font-medium">
202
+ {money(payment.amountCents)}
203
+ </span>
204
+ <span className="text-muted-foreground capitalize">
205
+ {payment.method ?? "other"}
206
+ </span>
207
+ {payment.reference ? (
208
+ <span className="truncate text-muted-foreground">
209
+ {payment.reference}
210
+ </span>
211
+ ) : null}
212
+ <time
213
+ className="ml-auto text-muted-foreground"
214
+ dateTime={payment.paidAt ?? undefined}
215
+ >
216
+ {relativeTime(payment.paidAt)}
217
+ </time>
218
+ </li>
219
+ ))}
220
+ </ul>
221
+ </section>
222
+ ) : null}
223
+ </div>
224
+ </div>
225
+
226
+ <PaymentDialog
227
+ open={payOpen}
228
+ balanceCents={t.balanceCents}
229
+ onOpenChange={setPayOpen}
230
+ onRecord={(amountCents, method, reference) =>
231
+ callFn("recordPayment", {
232
+ invoiceId: invoice.id,
233
+ amountCents,
234
+ method,
235
+ ...(reference ? { reference } : {}),
236
+ })
237
+ }
238
+ />
239
+ </>
240
+ );
241
+ }}
242
+ </Workspace>
243
+ );
244
+ }
245
+
246
+ function Field({ label, children }: { label: string; children: React.ReactNode }) {
247
+ return (
248
+ <div className="min-w-0">
249
+ <dt className="text-[11px] text-muted-foreground">{label}</dt>
250
+ <dd className="mt-1 truncate text-[13px]">{children}</dd>
251
+ </div>
252
+ );
253
+ }
@@ -0,0 +1,23 @@
1
+ import React, { use } from "react";
2
+ import type { Metadata, PageProps } from "@pylonsync/react";
3
+ import { InvoiceView } from "./invoice-view";
4
+
5
+ export const metadata: Metadata = {
6
+ title: "Invoice",
7
+ robots: "noindex",
8
+ };
9
+
10
+ /** `app/invoices/[id]/page.tsx` -> `/invoices/:id`. */
11
+ export default function InvoicePage({
12
+ auth,
13
+ response,
14
+ params,
15
+ serverData,
16
+ }: PageProps<{ id: string }>) {
17
+ if (!auth.user_id || auth.user_id.startsWith("guest_")) {
18
+ response.redirect("/login");
19
+ return null;
20
+ }
21
+ const me = use(serverData.get<{ email?: string }>("User", auth.user_id));
22
+ return <InvoiceView email={me?.email ?? ""} invoiceId={params.id} />;
23
+ }