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