@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,96 @@
|
|
|
1
|
+
# __APP_NAME__
|
|
2
|
+
|
|
3
|
+
Client project delivery for a small team — projects, a task board, and time
|
|
4
|
+
logged against the work. One [Pylon](https://pylonsync.com) app: SSR frontend,
|
|
5
|
+
API, auth, and realtime sync from one process on one port.
|
|
6
|
+
|
|
7
|
+
## Develop
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
__RUN_DEV__
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Open http://localhost:4321 and create an account. The first sign-in seeds two
|
|
14
|
+
live projects — one comfortably inside its budget, one over it — so both states
|
|
15
|
+
are visible rather than theoretical. Delete `functions/seedWorkspace.ts` and
|
|
16
|
+
`lib/seed.ts` once you have real work.
|
|
17
|
+
|
|
18
|
+
## What's interesting here
|
|
19
|
+
|
|
20
|
+
**Time is a ledger, not a total.** A task\'s logged hours are the sum of its
|
|
21
|
+
`TimeEntry` rows. Two people logging against the same task at once is normal,
|
|
22
|
+
and a mutable total loses one of them. It also means every hour on an invoice
|
|
23
|
+
traces to who logged it and when — which is the question a client asks.
|
|
24
|
+
|
|
25
|
+
Entries are **insert-only at the policy level**:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
allowUpdate: "false",
|
|
29
|
+
allowDelete: "false",
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Editing an entry would quietly change what someone was billed. A correction is a
|
|
33
|
+
negative entry, which stays visible in the history.
|
|
34
|
+
|
|
35
|
+
**Progress and budget are measured separately.** Progress counts tasks done;
|
|
36
|
+
budget tracks time spent. A project can be 90% through its budget and 20% done,
|
|
37
|
+
and a status report that conflates the two lies.
|
|
38
|
+
|
|
39
|
+
**The board is live.** Drag a task and it moves on every teammate\'s board
|
|
40
|
+
through the same write → sync → re-render path your own move takes, so a standup
|
|
41
|
+
doesn\'t start with reconciling two views of the work. The new position is
|
|
42
|
+
computed server-side, so two simultaneous drags don\'t claim the same index.
|
|
43
|
+
|
|
44
|
+
**Time input is lenient on purpose.** "90", "1.5h", "1h30", "45m" all work.
|
|
45
|
+
Rejecting "1h30" because it isn\'t "90" is the kind of friction that stops time
|
|
46
|
+
being logged at all, which costs far more than a forgiving parser. Values over a
|
|
47
|
+
day are refused — a typo on a timesheet becomes a typo on an invoice.
|
|
48
|
+
|
|
49
|
+
## Layout
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
app.ts entities + policies (note the append-only ledger)
|
|
53
|
+
app/
|
|
54
|
+
page.tsx "/" — the project list
|
|
55
|
+
projects-view.tsx list container
|
|
56
|
+
workspace.tsx app shell — the ONLY place that touches `db`
|
|
57
|
+
projects/[id]/ task board, budget, time logging
|
|
58
|
+
clients/ client list + billable rollups
|
|
59
|
+
login/ email/password auth
|
|
60
|
+
components/ presentational — props in, callbacks out
|
|
61
|
+
ui/ shadcn primitives
|
|
62
|
+
lib/work.ts statuses, time, budget, progress — pure
|
|
63
|
+
lib/format.ts time + initials — pure
|
|
64
|
+
functions/ server functions
|
|
65
|
+
tests/ pure logic + component tests
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Minutes are integers. Hours as floats produce 7.999999 on a timesheet, and
|
|
69
|
+
nobody wants to explain that.
|
|
70
|
+
|
|
71
|
+
## Access
|
|
72
|
+
|
|
73
|
+
Every entity is readable and writable by any **signed-in** user, and by nobody
|
|
74
|
+
else. Time entries are additionally insert-only for everyone.
|
|
75
|
+
|
|
76
|
+
There is no client portal. Adding one means a separate, narrower policy scoped
|
|
77
|
+
to that client\'s own projects, not loosening these.
|
|
78
|
+
|
|
79
|
+
## Grow it
|
|
80
|
+
|
|
81
|
+
- **Add a column:** `TASK_STATUSES` in `lib/work.ts`. The board, the validator,
|
|
82
|
+
and the skeleton all read from it.
|
|
83
|
+
- **Invoice from time:** the billable rollup is already there
|
|
84
|
+
(`billableCents`) — pair it with the `invoices` template.
|
|
85
|
+
- **Per-person timesheets:** group `TimeEntry` by `userId`; the rows already
|
|
86
|
+
carry it.
|
|
87
|
+
|
|
88
|
+
## Deploy
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
pylon lint --strict
|
|
92
|
+
pylon test
|
|
93
|
+
pylon deploy
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Docs: https://docs.pylonsync.com
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState } from "react";
|
|
4
|
+
import { db } from "@pylonsync/react";
|
|
5
|
+
import { Plus, Users } from "lucide-react";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import { PageHeader } from "@/components/page-header";
|
|
8
|
+
import { DataTable, type ColumnDef } from "@/components/data-table";
|
|
9
|
+
import { EmptyState } from "@/components/empty-state";
|
|
10
|
+
import { RecordDialog } from "@/components/record-dialog";
|
|
11
|
+
import { Avatar } from "@/components/avatar";
|
|
12
|
+
import { billableCents, duration, minutesForProject, money } from "@/lib/work";
|
|
13
|
+
import { Workspace, type ClientRow } from "../workspace";
|
|
14
|
+
|
|
15
|
+
export function ClientsView({ email }: { email: string }) {
|
|
16
|
+
const [open, setOpen] = useState(false);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<Workspace email={email} pathname="/clients">
|
|
20
|
+
{(data) => {
|
|
21
|
+
const forClient = (clientId: string) =>
|
|
22
|
+
data.projects.filter((p) => p.clientId === clientId);
|
|
23
|
+
|
|
24
|
+
const columns: ColumnDef<ClientRow>[] = [
|
|
25
|
+
{
|
|
26
|
+
key: "name",
|
|
27
|
+
header: "Client",
|
|
28
|
+
cell: (row) => (
|
|
29
|
+
<span className="flex items-center gap-2">
|
|
30
|
+
<Avatar name={row.name} size="sm" />
|
|
31
|
+
<span className="truncate font-medium">{row.name}</span>
|
|
32
|
+
</span>
|
|
33
|
+
),
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
key: "email",
|
|
37
|
+
header: "Email",
|
|
38
|
+
cell: (row) => (
|
|
39
|
+
<span className="text-muted-foreground">{row.email ?? "—"}</span>
|
|
40
|
+
),
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
key: "projects",
|
|
44
|
+
header: "Projects",
|
|
45
|
+
numeric: true,
|
|
46
|
+
cell: (row) => forClient(row.id).length,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
key: "logged",
|
|
50
|
+
header: "Logged",
|
|
51
|
+
numeric: true,
|
|
52
|
+
cell: (row) =>
|
|
53
|
+
duration(
|
|
54
|
+
forClient(row.id).reduce(
|
|
55
|
+
(sum, p) => sum + minutesForProject(p.id, data.entries),
|
|
56
|
+
0,
|
|
57
|
+
),
|
|
58
|
+
),
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
key: "billable",
|
|
62
|
+
header: "Billable",
|
|
63
|
+
numeric: true,
|
|
64
|
+
cell: (row) =>
|
|
65
|
+
money(
|
|
66
|
+
forClient(row.id).reduce(
|
|
67
|
+
(sum, p) =>
|
|
68
|
+
sum +
|
|
69
|
+
billableCents(
|
|
70
|
+
minutesForProject(p.id, data.entries),
|
|
71
|
+
p.hourlyRateCents,
|
|
72
|
+
),
|
|
73
|
+
0,
|
|
74
|
+
),
|
|
75
|
+
),
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const rows = [...data.clients].sort((a, b) => a.name.localeCompare(b.name));
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<>
|
|
83
|
+
<PageHeader title="Clients" count={rows.length}>
|
|
84
|
+
<Button size="sm" onClick={() => setOpen(true)}>
|
|
85
|
+
<Plus />
|
|
86
|
+
New client
|
|
87
|
+
</Button>
|
|
88
|
+
</PageHeader>
|
|
89
|
+
|
|
90
|
+
<DataTable
|
|
91
|
+
rows={rows}
|
|
92
|
+
columns={columns}
|
|
93
|
+
empty={
|
|
94
|
+
<EmptyState
|
|
95
|
+
icon={<Users />}
|
|
96
|
+
title="No clients yet"
|
|
97
|
+
description="Who the work is for. Projects hang off a client, and billable totals roll up here."
|
|
98
|
+
action={
|
|
99
|
+
<Button size="sm" onClick={() => setOpen(true)}>
|
|
100
|
+
<Plus />
|
|
101
|
+
New client
|
|
102
|
+
</Button>
|
|
103
|
+
}
|
|
104
|
+
/>
|
|
105
|
+
}
|
|
106
|
+
/>
|
|
107
|
+
|
|
108
|
+
<RecordDialog
|
|
109
|
+
open={open}
|
|
110
|
+
title="New client"
|
|
111
|
+
submitLabel="Create client"
|
|
112
|
+
onOpenChange={setOpen}
|
|
113
|
+
fields={[
|
|
114
|
+
{ name: "name", label: "Name", required: true, placeholder: "Northwind Logistics" },
|
|
115
|
+
{ name: "email", label: "Email", type: "email", placeholder: "dana@northwind.co" },
|
|
116
|
+
]}
|
|
117
|
+
onCreate={async (values) => {
|
|
118
|
+
await db.insert("Client", values);
|
|
119
|
+
}}
|
|
120
|
+
/>
|
|
121
|
+
</>
|
|
122
|
+
);
|
|
123
|
+
}}
|
|
124
|
+
</Workspace>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React, { use } from "react";
|
|
2
|
+
import type { Metadata, PageProps } from "@pylonsync/react";
|
|
3
|
+
import { ClientsView } from "./clients-view";
|
|
4
|
+
|
|
5
|
+
export const metadata: Metadata = {
|
|
6
|
+
title: "Clients",
|
|
7
|
+
robots: "noindex",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default function ClientsPage({ auth, response, serverData }: PageProps) {
|
|
11
|
+
if (!auth.user_id || auth.user_id.startsWith("guest_")) {
|
|
12
|
+
response.redirect("/login");
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const me = use(serverData.get<{ email?: string }>("User", auth.user_id));
|
|
16
|
+
return <ClientsView email={me?.email ?? ""} />;
|
|
17
|
+
}
|
|
@@ -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,31 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
interface LayoutProps {
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The document shell. The app chrome — sidebar, command palette, shortcuts —
|
|
9
|
+
* lives in `app/workspace.tsx` instead, because it needs the synced data; this
|
|
10
|
+
* layer only sets up the page.
|
|
11
|
+
*
|
|
12
|
+
* `class="dark"` makes dark the default theme. The light tokens in globals.css
|
|
13
|
+
* are complete and tested, so removing the class flips the whole app; swap it
|
|
14
|
+
* for a stored preference when you want a toggle.
|
|
15
|
+
*/
|
|
16
|
+
export default function RootLayout({ children }: LayoutProps) {
|
|
17
|
+
return (
|
|
18
|
+
<html lang="en" className="dark">
|
|
19
|
+
<head>
|
|
20
|
+
<meta charSet="utf-8" />
|
|
21
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
22
|
+
<title>__APP_NAME__</title>
|
|
23
|
+
{/* Tailwind is compiled by Pylon from app/globals.css and the
|
|
24
|
+
stylesheet link is injected here automatically. Inter is declared in
|
|
25
|
+
app.ts (fonts: [...]) and self-hosted by the build — @font-face, the
|
|
26
|
+
preload link, and a size-adjusted fallback are injected too. */}
|
|
27
|
+
</head>
|
|
28
|
+
<body className="h-full">{children}</body>
|
|
29
|
+
</html>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
ApiError,
|
|
6
|
+
passwordLogin,
|
|
7
|
+
passwordRegister,
|
|
8
|
+
persistSession,
|
|
9
|
+
} from "@pylonsync/client";
|
|
10
|
+
import { Button } from "@/components/ui/button";
|
|
11
|
+
import { Input } from "@/components/ui/input";
|
|
12
|
+
import { Label } from "@/components/ui/label";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* One form, two modes. Calls the built-in auth API (`/api/auth/password/*`),
|
|
16
|
+
* then `persistSession` writes the token so the sync engine and `callFn`
|
|
17
|
+
* authenticate as this user on the next load.
|
|
18
|
+
*
|
|
19
|
+
* The navigation is a full `assign`, not a client route change, so the SSR
|
|
20
|
+
* runtime re-resolves auth from the HttpOnly cookie and the first paint of the
|
|
21
|
+
* workspace is already signed in.
|
|
22
|
+
*/
|
|
23
|
+
export function AuthForm() {
|
|
24
|
+
const [mode, setMode] = useState<"login" | "signup">("login");
|
|
25
|
+
const [email, setEmail] = useState("");
|
|
26
|
+
const [password, setPassword] = useState("");
|
|
27
|
+
const [error, setError] = useState<string | null>(null);
|
|
28
|
+
const [pending, setPending] = useState(false);
|
|
29
|
+
|
|
30
|
+
async function onSubmit(event: React.FormEvent) {
|
|
31
|
+
event.preventDefault();
|
|
32
|
+
setError(null);
|
|
33
|
+
setPending(true);
|
|
34
|
+
try {
|
|
35
|
+
const session =
|
|
36
|
+
mode === "login"
|
|
37
|
+
? await passwordLogin({ email, password })
|
|
38
|
+
: await passwordRegister({ email, password });
|
|
39
|
+
persistSession(session);
|
|
40
|
+
window.location.assign("/");
|
|
41
|
+
} catch (err) {
|
|
42
|
+
setError(messageFor(err));
|
|
43
|
+
setPending(false);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="space-y-5">
|
|
49
|
+
<form onSubmit={onSubmit} className="space-y-3.5">
|
|
50
|
+
<div className="space-y-1.5">
|
|
51
|
+
<Label htmlFor="email">Email</Label>
|
|
52
|
+
<Input
|
|
53
|
+
id="email"
|
|
54
|
+
type="email"
|
|
55
|
+
value={email}
|
|
56
|
+
onChange={(event) => setEmail(event.target.value)}
|
|
57
|
+
required
|
|
58
|
+
autoComplete="email"
|
|
59
|
+
placeholder="you@company.com"
|
|
60
|
+
/>
|
|
61
|
+
</div>
|
|
62
|
+
<div className="space-y-1.5">
|
|
63
|
+
<Label htmlFor="password">Password</Label>
|
|
64
|
+
<Input
|
|
65
|
+
id="password"
|
|
66
|
+
type="password"
|
|
67
|
+
value={password}
|
|
68
|
+
onChange={(event) => setPassword(event.target.value)}
|
|
69
|
+
required
|
|
70
|
+
autoComplete={mode === "login" ? "current-password" : "new-password"}
|
|
71
|
+
placeholder={mode === "login" ? "Your password" : "At least 10 characters"}
|
|
72
|
+
/>
|
|
73
|
+
</div>
|
|
74
|
+
{error ? (
|
|
75
|
+
<p className="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-[12px] leading-snug text-destructive">
|
|
76
|
+
{error}
|
|
77
|
+
</p>
|
|
78
|
+
) : null}
|
|
79
|
+
<Button type="submit" disabled={pending} className="w-full">
|
|
80
|
+
{pending ? "…" : mode === "login" ? "Sign in" : "Create account"}
|
|
81
|
+
</Button>
|
|
82
|
+
</form>
|
|
83
|
+
|
|
84
|
+
<p className="text-center text-[12px] text-muted-foreground">
|
|
85
|
+
{mode === "login" ? "First time here?" : "Already have an account?"}{" "}
|
|
86
|
+
<button
|
|
87
|
+
type="button"
|
|
88
|
+
onClick={() => {
|
|
89
|
+
setMode(mode === "login" ? "signup" : "login");
|
|
90
|
+
setError(null);
|
|
91
|
+
}}
|
|
92
|
+
className="font-medium text-foreground underline underline-offset-2"
|
|
93
|
+
>
|
|
94
|
+
{mode === "login" ? "Create an account" : "Sign in"}
|
|
95
|
+
</button>
|
|
96
|
+
</p>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// `ApiError` carries a stable `.code`, so branch on the code rather than the
|
|
102
|
+
// message — the copy here is ours, the message is the framework's.
|
|
103
|
+
function messageFor(err: unknown): string {
|
|
104
|
+
if (err instanceof ApiError) {
|
|
105
|
+
switch (err.code) {
|
|
106
|
+
case "INVALID_CREDENTIALS":
|
|
107
|
+
return "Wrong email or password.";
|
|
108
|
+
case "USER_EXISTS":
|
|
109
|
+
return "That email is already registered — sign in instead.";
|
|
110
|
+
case "WEAK_PASSWORD":
|
|
111
|
+
return "Pick a longer password — at least 10 characters.";
|
|
112
|
+
case "PWNED_PASSWORD":
|
|
113
|
+
return "That password has appeared in a known breach. Choose another.";
|
|
114
|
+
case "RATE_LIMITED":
|
|
115
|
+
return "Too many attempts — try again in a minute.";
|
|
116
|
+
default:
|
|
117
|
+
return err.message;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (err instanceof Error) return err.message;
|
|
121
|
+
return "Something went wrong. Try again.";
|
|
122
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Metadata, PageProps } from "@pylonsync/react";
|
|
3
|
+
import { AuthForm } from "./auth-form";
|
|
4
|
+
|
|
5
|
+
export const metadata: Metadata = {
|
|
6
|
+
title: "Sign in",
|
|
7
|
+
robots: "noindex",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default function LoginPage({ auth, response }: PageProps) {
|
|
11
|
+
// Already signed in? Nothing to do here.
|
|
12
|
+
if (auth.user_id && !auth.user_id.startsWith("guest_")) {
|
|
13
|
+
response.redirect("/");
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<main className="flex min-h-screen items-center justify-center px-6">
|
|
19
|
+
<div className="w-full max-w-[320px]">
|
|
20
|
+
<div className="mb-7 flex items-center gap-2">
|
|
21
|
+
<span className="flex size-7 items-center justify-center rounded-md bg-primary text-[12px] font-bold text-primary-foreground">
|
|
22
|
+
C
|
|
23
|
+
</span>
|
|
24
|
+
<span className="text-[15px] font-semibold tracking-tight">CRM</span>
|
|
25
|
+
</div>
|
|
26
|
+
<h1 className="text-[19px] font-semibold tracking-tight">
|
|
27
|
+
Sign in to your workspace
|
|
28
|
+
</h1>
|
|
29
|
+
<p className="mt-1 text-[13px] text-muted-foreground">
|
|
30
|
+
Your team shares one pipeline. Anyone with an account sees it.
|
|
31
|
+
</p>
|
|
32
|
+
<div className="mt-6">
|
|
33
|
+
<AuthForm />
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</main>
|
|
37
|
+
);
|
|
38
|
+
}
|