@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,186 @@
|
|
|
1
|
+
import {
|
|
2
|
+
entity,
|
|
3
|
+
field,
|
|
4
|
+
policy,
|
|
5
|
+
auth,
|
|
6
|
+
buildManifest,
|
|
7
|
+
discoverAppRoutes,
|
|
8
|
+
font,
|
|
9
|
+
} from "@pylonsync/sdk";
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// projects — client project delivery for a small team: projects, a task board,
|
|
13
|
+
// and time logged against the work.
|
|
14
|
+
//
|
|
15
|
+
// TIME IS A LEDGER. A task\'s logged hours are the sum of its TimeEntry rows,
|
|
16
|
+
// never a running total on the task. Two people logging against the same task at
|
|
17
|
+
// once is normal, and a mutable total loses one of them. It also means every
|
|
18
|
+
// hour on an invoice traces to who logged it and when — the question a client
|
|
19
|
+
// asks.
|
|
20
|
+
//
|
|
21
|
+
// • Client — who the work is for.
|
|
22
|
+
// • Project — budget, rate, due date, status.
|
|
23
|
+
// • Task — the board. Status, assignee, estimate.
|
|
24
|
+
// • TimeEntry — minutes against a task. Append-only.
|
|
25
|
+
// • User — a team member (email/password).
|
|
26
|
+
//
|
|
27
|
+
// Minutes are integers. Hours as floats produce 7.999999 on a timesheet.
|
|
28
|
+
//
|
|
29
|
+
// The realtime hook: drag a task and it moves on every teammate\'s board
|
|
30
|
+
// instantly, so a standup doesn\'t start with reconciling two views of the work.
|
|
31
|
+
//
|
|
32
|
+
// TRUST MODEL: an internal tool for ONE team. Every entity is readable and
|
|
33
|
+
// writable by any SIGNED-IN user and by nobody else. There is no client portal —
|
|
34
|
+
// adding one means a separate, narrower policy for that client\'s own projects.
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
const TEAM = "auth.userId != null";
|
|
38
|
+
|
|
39
|
+
const Client = entity(
|
|
40
|
+
"Client",
|
|
41
|
+
{
|
|
42
|
+
name: field.string(),
|
|
43
|
+
email: field.string().optional(),
|
|
44
|
+
createdAt: field.datetime().defaultNow(),
|
|
45
|
+
},
|
|
46
|
+
{ indexes: [{ name: "by_name", fields: ["name"], unique: false }] },
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const Project = entity(
|
|
50
|
+
"Project",
|
|
51
|
+
{
|
|
52
|
+
name: field.string(),
|
|
53
|
+
clientId: field.id("Client").optional(),
|
|
54
|
+
// "active" | "paused" | "complete" — see PROJECT_STATUSES.
|
|
55
|
+
status: field.string().default("active"),
|
|
56
|
+
// Agreed budget in whole MINUTES; 0 means no budget to track against.
|
|
57
|
+
budgetMinutes: field.number().default(0),
|
|
58
|
+
hourlyRateCents: field.number().default(0),
|
|
59
|
+
dueDate: field.datetime().optional(),
|
|
60
|
+
createdAt: field.datetime().defaultNow(),
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
indexes: [
|
|
64
|
+
{ name: "by_status", fields: ["status"], unique: false },
|
|
65
|
+
{ name: "by_client", fields: ["clientId"], unique: false },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const Task = entity(
|
|
71
|
+
"Task",
|
|
72
|
+
{
|
|
73
|
+
projectId: field.id("Project"),
|
|
74
|
+
title: field.string(),
|
|
75
|
+
// "todo" | "doing" | "review" | "done" — see TASK_STATUSES.
|
|
76
|
+
status: field.string().default("todo"),
|
|
77
|
+
assigneeId: field.id("User").optional(),
|
|
78
|
+
estimateMinutes: field.number().default(0),
|
|
79
|
+
// Manual order within a column, so dragging is stable.
|
|
80
|
+
position: field.number().default(0),
|
|
81
|
+
createdAt: field.datetime().defaultNow(),
|
|
82
|
+
updatedAt: field.datetime().defaultNow(),
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
indexes: [
|
|
86
|
+
{ name: "by_project", fields: ["projectId"], unique: false },
|
|
87
|
+
{ name: "by_status", fields: ["status"], unique: false },
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// APPEND-ONLY, like a stock ledger. `projectId` is denormalised so project
|
|
93
|
+
// totals don\'t need a join through Task on every read — the task can\'t change
|
|
94
|
+
// project, so it can\'t go stale.
|
|
95
|
+
const TimeEntry = entity(
|
|
96
|
+
"TimeEntry",
|
|
97
|
+
{
|
|
98
|
+
taskId: field.id("Task"),
|
|
99
|
+
projectId: field.id("Project"),
|
|
100
|
+
minutes: field.number(),
|
|
101
|
+
note: field.string().optional(),
|
|
102
|
+
userId: field.id("User").readonly().optional(),
|
|
103
|
+
spentOn: field.datetime().defaultNow(),
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
indexes: [
|
|
107
|
+
{ name: "by_task", fields: ["taskId"], unique: false },
|
|
108
|
+
{ name: "by_project", fields: ["projectId"], unique: false },
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const User = entity(
|
|
114
|
+
"User",
|
|
115
|
+
{
|
|
116
|
+
email: field.string(),
|
|
117
|
+
displayName: field.string().optional(),
|
|
118
|
+
passwordHash: field.string().serverOnly().optional(),
|
|
119
|
+
avatarColor: field.string().optional(),
|
|
120
|
+
emailVerified: field.datetime().optional(),
|
|
121
|
+
createdAt: field.datetime().defaultNow(),
|
|
122
|
+
},
|
|
123
|
+
{ indexes: [{ name: "by_email", fields: ["email"], unique: true }] },
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const teamPolicy = (name: string, entityName: string) =>
|
|
127
|
+
policy({
|
|
128
|
+
name,
|
|
129
|
+
entity: entityName,
|
|
130
|
+
allowRead: TEAM,
|
|
131
|
+
allowInsert: TEAM,
|
|
132
|
+
allowUpdate: TEAM,
|
|
133
|
+
allowDelete: TEAM,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Logged time is insert-only: editing an entry would quietly change what a
|
|
137
|
+
// client was billed, and deleting one would make a total unexplainable. A
|
|
138
|
+
// correction is a negative entry, which stays visible in the history.
|
|
139
|
+
const timePolicy = policy({
|
|
140
|
+
name: "time_append_only",
|
|
141
|
+
entity: "TimeEntry",
|
|
142
|
+
allowRead: TEAM,
|
|
143
|
+
allowInsert: TEAM,
|
|
144
|
+
allowUpdate: "false",
|
|
145
|
+
allowDelete: "false",
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const userPolicy = policy({
|
|
149
|
+
name: "user_team_read",
|
|
150
|
+
entity: "User",
|
|
151
|
+
allowRead: TEAM,
|
|
152
|
+
allowInsert: "false",
|
|
153
|
+
allowUpdate: "false",
|
|
154
|
+
allowDelete: "false",
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const manifest = buildManifest({
|
|
158
|
+
name: "__APP_NAME__",
|
|
159
|
+
version: "0.1.0",
|
|
160
|
+
entities: [Client, Project, Task, TimeEntry, User],
|
|
161
|
+
queries: [],
|
|
162
|
+
actions: [],
|
|
163
|
+
policies: [
|
|
164
|
+
teamPolicy("client_team", "Client"),
|
|
165
|
+
teamPolicy("project_team", "Project"),
|
|
166
|
+
teamPolicy("task_team", "Task"),
|
|
167
|
+
timePolicy,
|
|
168
|
+
userPolicy,
|
|
169
|
+
],
|
|
170
|
+
auth: auth(),
|
|
171
|
+
fonts: [
|
|
172
|
+
font({
|
|
173
|
+
family: "Inter",
|
|
174
|
+
variable: "--font-sans",
|
|
175
|
+
weights: ["400", "500", "600", "700"],
|
|
176
|
+
subsets: ["latin"],
|
|
177
|
+
display: "swap",
|
|
178
|
+
preload: true,
|
|
179
|
+
}),
|
|
180
|
+
],
|
|
181
|
+
routes: await discoverAppRoutes(),
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
console.log(JSON.stringify(manifest, null, 2));
|
|
185
|
+
|
|
186
|
+
export default manifest;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
import { accentIndex, initials } from "@/lib/format";
|
|
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,37 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { TASK_STATUSES } from "@/lib/work";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The board\'s shape while the replica hydrates. Showing the real columns keeps
|
|
6
|
+
* the beat before the first row lands from reading as "this project has no
|
|
7
|
+
* tasks", and nothing jumps when the data arrives.
|
|
8
|
+
*/
|
|
9
|
+
export function BoardSkeleton() {
|
|
10
|
+
return (
|
|
11
|
+
<div className="flex h-full gap-3 overflow-hidden p-4" aria-hidden="true">
|
|
12
|
+
{TASK_STATUSES.map((status, column) => (
|
|
13
|
+
<section
|
|
14
|
+
key={status.id}
|
|
15
|
+
className="flex w-[268px] shrink-0 flex-col rounded-xl border border-border bg-surface-1/50"
|
|
16
|
+
>
|
|
17
|
+
<header className="flex items-center gap-2 px-3 py-2.5">
|
|
18
|
+
<span className="text-[12px] font-medium text-muted-foreground">
|
|
19
|
+
{status.label}
|
|
20
|
+
</span>
|
|
21
|
+
</header>
|
|
22
|
+
<div className="flex flex-col gap-2 px-2 pb-2">
|
|
23
|
+
{Array.from({ length: Math.max(1, 3 - column) }).map((_, card) => (
|
|
24
|
+
<div
|
|
25
|
+
key={card}
|
|
26
|
+
className="animate-pulse rounded-lg border border-border bg-card p-2.5"
|
|
27
|
+
>
|
|
28
|
+
<div className="h-3 w-3/4 rounded bg-muted" />
|
|
29
|
+
<div className="mt-3 h-4 w-4 rounded-full bg-muted" />
|
|
30
|
+
</div>
|
|
31
|
+
))}
|
|
32
|
+
</div>
|
|
33
|
+
</section>
|
|
34
|
+
))}
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
import { budgetState, duration, money, billableCents } from "@/lib/work";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Budget consumed, as a bar plus the figures.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately separate from task progress: time spent measures effort, not
|
|
9
|
+
* completion. A project can be 90% through its budget and 20% done, and a
|
|
10
|
+
* status report that conflates them lies.
|
|
11
|
+
*/
|
|
12
|
+
export function BudgetBar({
|
|
13
|
+
budgetMinutes,
|
|
14
|
+
loggedMinutes,
|
|
15
|
+
hourlyRateCents,
|
|
16
|
+
}: {
|
|
17
|
+
budgetMinutes?: number | null;
|
|
18
|
+
loggedMinutes: number;
|
|
19
|
+
hourlyRateCents?: number | null;
|
|
20
|
+
}) {
|
|
21
|
+
const budget = Number(budgetMinutes) || 0;
|
|
22
|
+
const state = budgetState(budget, loggedMinutes);
|
|
23
|
+
const pct = budget > 0 ? Math.min(100, (loggedMinutes / budget) * 100) : 0;
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className="space-y-1.5">
|
|
27
|
+
<div className="flex items-baseline justify-between gap-3 text-[12px]">
|
|
28
|
+
<span className="text-muted-foreground">
|
|
29
|
+
{state === "none"
|
|
30
|
+
? "No budget set"
|
|
31
|
+
: `${duration(loggedMinutes)} of ${duration(budget)}`}
|
|
32
|
+
</span>
|
|
33
|
+
{hourlyRateCents ? (
|
|
34
|
+
<span className="tabular text-muted-foreground">
|
|
35
|
+
{money(billableCents(loggedMinutes, hourlyRateCents))} billable
|
|
36
|
+
</span>
|
|
37
|
+
) : null}
|
|
38
|
+
</div>
|
|
39
|
+
{state !== "none" ? (
|
|
40
|
+
<div className="h-1.5 overflow-hidden rounded-full bg-muted">
|
|
41
|
+
<div
|
|
42
|
+
className={cn(
|
|
43
|
+
"h-full rounded-full transition-all",
|
|
44
|
+
state === "over"
|
|
45
|
+
? "bg-destructive"
|
|
46
|
+
: state === "near"
|
|
47
|
+
? "bg-stage-proposal"
|
|
48
|
+
: "bg-stage-won",
|
|
49
|
+
)}
|
|
50
|
+
style={{ width: `${Math.max(2, pct)}%` }}
|
|
51
|
+
/>
|
|
52
|
+
</div>
|
|
53
|
+
) : null}
|
|
54
|
+
{state === "over" ? (
|
|
55
|
+
<p className="text-[11px] text-destructive">
|
|
56
|
+
Over budget by {duration(loggedMinutes - budget)}
|
|
57
|
+
</p>
|
|
58
|
+
) : null}
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { Building2, Plus, Search, User } from "lucide-react";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
import { Kbd } from "@/components/kbd";
|
|
5
|
+
import { moveSelection, searchItems, type SearchItem } from "@/lib/search";
|
|
6
|
+
|
|
7
|
+
const TYPE_ICON: Record<SearchItem["type"], React.ReactNode> = {
|
|
8
|
+
deal: <Search />,
|
|
9
|
+
company: <Building2 />,
|
|
10
|
+
contact: <User />,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export interface CommandAction {
|
|
14
|
+
id: string;
|
|
15
|
+
label: string;
|
|
16
|
+
run: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* ⌘K. Searches the synced replica in memory, so results land as you type with
|
|
21
|
+
* no request per keystroke — see lib/search.ts for the ranking.
|
|
22
|
+
*
|
|
23
|
+
* Presentational: items and actions come in as props, selection is delegated
|
|
24
|
+
* through onSelect. Keyboard handling lives here because it belongs to the
|
|
25
|
+
* widget, not to the page that opened it.
|
|
26
|
+
*/
|
|
27
|
+
export function CommandPalette({
|
|
28
|
+
open,
|
|
29
|
+
items,
|
|
30
|
+
actions = [],
|
|
31
|
+
onClose,
|
|
32
|
+
onSelect,
|
|
33
|
+
}: {
|
|
34
|
+
open: boolean;
|
|
35
|
+
items: SearchItem[];
|
|
36
|
+
actions?: CommandAction[];
|
|
37
|
+
onClose: () => void;
|
|
38
|
+
onSelect: (item: SearchItem) => void;
|
|
39
|
+
}) {
|
|
40
|
+
const [query, setQuery] = useState("");
|
|
41
|
+
const [index, setIndex] = useState(0);
|
|
42
|
+
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
43
|
+
|
|
44
|
+
const results = useMemo(() => searchItems(items, query), [items, query]);
|
|
45
|
+
const shownActions = query.trim() ? [] : actions;
|
|
46
|
+
const rows = useMemo(
|
|
47
|
+
() => [
|
|
48
|
+
...shownActions.map((a) => ({ kind: "action" as const, action: a })),
|
|
49
|
+
...results.map((item) => ({ kind: "item" as const, item })),
|
|
50
|
+
],
|
|
51
|
+
[shownActions, results],
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!open) return;
|
|
56
|
+
setQuery("");
|
|
57
|
+
setIndex(0);
|
|
58
|
+
// Focus after paint so the dialog is mounted and the caret lands.
|
|
59
|
+
const id = requestAnimationFrame(() => inputRef.current?.focus());
|
|
60
|
+
return () => cancelAnimationFrame(id);
|
|
61
|
+
}, [open]);
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
setIndex(0);
|
|
65
|
+
}, [query]);
|
|
66
|
+
|
|
67
|
+
if (!open) return null;
|
|
68
|
+
|
|
69
|
+
function commit(at: number) {
|
|
70
|
+
const row = rows[at];
|
|
71
|
+
if (!row) return;
|
|
72
|
+
if (row.kind === "action") row.action.run();
|
|
73
|
+
else onSelect(row.item);
|
|
74
|
+
onClose();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div
|
|
79
|
+
className="fixed inset-0 z-50 flex items-start justify-center bg-black/50 pt-[12vh]"
|
|
80
|
+
// Clicking the backdrop dismisses; clicks inside the panel must not.
|
|
81
|
+
onMouseDown={(event) => {
|
|
82
|
+
if (event.target === event.currentTarget) onClose();
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
<div
|
|
86
|
+
role="dialog"
|
|
87
|
+
aria-modal="true"
|
|
88
|
+
aria-label="Search"
|
|
89
|
+
className="w-full max-w-lg overflow-hidden rounded-xl border border-border bg-popover shadow-2xl"
|
|
90
|
+
onKeyDown={(event) => {
|
|
91
|
+
if (event.key === "Escape") {
|
|
92
|
+
event.preventDefault();
|
|
93
|
+
onClose();
|
|
94
|
+
} else if (event.key === "ArrowDown") {
|
|
95
|
+
event.preventDefault();
|
|
96
|
+
setIndex((i) => moveSelection(i, 1, rows.length));
|
|
97
|
+
} else if (event.key === "ArrowUp") {
|
|
98
|
+
event.preventDefault();
|
|
99
|
+
setIndex((i) => moveSelection(i, -1, rows.length));
|
|
100
|
+
} else if (event.key === "Enter") {
|
|
101
|
+
event.preventDefault();
|
|
102
|
+
commit(index);
|
|
103
|
+
}
|
|
104
|
+
}}
|
|
105
|
+
>
|
|
106
|
+
<div className="flex items-center gap-2 border-b border-border px-3">
|
|
107
|
+
<Search className="size-4 shrink-0 text-muted-foreground" />
|
|
108
|
+
<input
|
|
109
|
+
ref={inputRef}
|
|
110
|
+
value={query}
|
|
111
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
112
|
+
placeholder="Search deals, companies, contacts…"
|
|
113
|
+
aria-label="Search deals, companies and contacts"
|
|
114
|
+
className="h-11 flex-1 bg-transparent text-[13px] outline-none placeholder:text-muted-foreground"
|
|
115
|
+
/>
|
|
116
|
+
<Kbd>esc</Kbd>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<div className="max-h-80 overflow-y-auto p-1.5">
|
|
120
|
+
{rows.length === 0 ? (
|
|
121
|
+
<p className="px-3 py-6 text-center text-[12px] text-muted-foreground">
|
|
122
|
+
No matches for “{query}”
|
|
123
|
+
</p>
|
|
124
|
+
) : (
|
|
125
|
+
rows.map((row, at) => (
|
|
126
|
+
<button
|
|
127
|
+
key={row.kind === "action" ? row.action.id : row.item.id}
|
|
128
|
+
type="button"
|
|
129
|
+
// Pointer hover moves selection so mouse and keyboard agree.
|
|
130
|
+
onMouseMove={() => setIndex(at)}
|
|
131
|
+
onClick={() => commit(at)}
|
|
132
|
+
aria-selected={at === index}
|
|
133
|
+
className={cn(
|
|
134
|
+
"flex w-full items-center gap-2.5 rounded-md px-2.5 py-2 text-left text-[13px] transition-colors",
|
|
135
|
+
"[&_svg]:size-3.5 [&_svg]:shrink-0 [&_svg]:text-muted-foreground",
|
|
136
|
+
at === index ? "bg-surface-2" : "hover:bg-surface-2/60",
|
|
137
|
+
)}
|
|
138
|
+
>
|
|
139
|
+
{row.kind === "action" ? (
|
|
140
|
+
<>
|
|
141
|
+
<Plus />
|
|
142
|
+
<span className="flex-1 truncate">{row.action.label}</span>
|
|
143
|
+
</>
|
|
144
|
+
) : (
|
|
145
|
+
<>
|
|
146
|
+
{TYPE_ICON[row.item.type]}
|
|
147
|
+
<span className="flex-1 truncate">{row.item.title}</span>
|
|
148
|
+
{row.item.subtitle ? (
|
|
149
|
+
<span className="truncate text-[12px] text-muted-foreground">
|
|
150
|
+
{row.item.subtitle}
|
|
151
|
+
</span>
|
|
152
|
+
) : null}
|
|
153
|
+
</>
|
|
154
|
+
)}
|
|
155
|
+
</button>
|
|
156
|
+
))
|
|
157
|
+
)}
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
|
|
4
|
+
export interface ColumnDef<Row> {
|
|
5
|
+
key: string;
|
|
6
|
+
header: string;
|
|
7
|
+
/** Cell content. Return a string and it's rendered as-is. */
|
|
8
|
+
cell: (row: Row) => React.ReactNode;
|
|
9
|
+
/** Right-align and tabular-figure a numeric column. */
|
|
10
|
+
numeric?: boolean;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A dense list view. Rows are 36px so a screenful is a screenful of data, and
|
|
16
|
+
* the header sticks while the body scrolls.
|
|
17
|
+
*
|
|
18
|
+
* Generic and presentational — every view here uses the same one, which is why
|
|
19
|
+
* companies and contacts look like the same product.
|
|
20
|
+
*/
|
|
21
|
+
export function DataTable<Row extends { id: string }>({
|
|
22
|
+
rows,
|
|
23
|
+
columns,
|
|
24
|
+
onRowClick,
|
|
25
|
+
empty,
|
|
26
|
+
}: {
|
|
27
|
+
rows: Row[];
|
|
28
|
+
columns: ColumnDef<Row>[];
|
|
29
|
+
onRowClick?: (row: Row) => void;
|
|
30
|
+
empty?: React.ReactNode;
|
|
31
|
+
}) {
|
|
32
|
+
if (rows.length === 0 && empty) return <>{empty}</>;
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="min-h-0 flex-1 overflow-auto">
|
|
36
|
+
<table className="w-full border-collapse text-[13px]">
|
|
37
|
+
<thead className="sticky top-0 z-10 bg-background">
|
|
38
|
+
<tr className="hairline">
|
|
39
|
+
{columns.map((column) => (
|
|
40
|
+
<th
|
|
41
|
+
key={column.key}
|
|
42
|
+
scope="col"
|
|
43
|
+
className={cn(
|
|
44
|
+
"h-8 px-4 text-left text-[11px] font-medium text-muted-foreground",
|
|
45
|
+
column.numeric && "text-right",
|
|
46
|
+
column.className,
|
|
47
|
+
)}
|
|
48
|
+
>
|
|
49
|
+
{column.header}
|
|
50
|
+
</th>
|
|
51
|
+
))}
|
|
52
|
+
</tr>
|
|
53
|
+
</thead>
|
|
54
|
+
<tbody>
|
|
55
|
+
{rows.map((row) => (
|
|
56
|
+
<tr
|
|
57
|
+
key={row.id}
|
|
58
|
+
onClick={onRowClick ? () => onRowClick(row) : undefined}
|
|
59
|
+
tabIndex={onRowClick ? 0 : undefined}
|
|
60
|
+
onKeyDown={
|
|
61
|
+
onRowClick
|
|
62
|
+
? (event) => {
|
|
63
|
+
if (event.key === "Enter") onRowClick(row);
|
|
64
|
+
}
|
|
65
|
+
: undefined
|
|
66
|
+
}
|
|
67
|
+
className={cn(
|
|
68
|
+
"border-b border-border/60 transition-colors",
|
|
69
|
+
onRowClick && "cursor-pointer hover:bg-surface-1",
|
|
70
|
+
)}
|
|
71
|
+
>
|
|
72
|
+
{columns.map((column) => (
|
|
73
|
+
<td
|
|
74
|
+
key={column.key}
|
|
75
|
+
className={cn(
|
|
76
|
+
"h-9 max-w-0 truncate px-4",
|
|
77
|
+
column.numeric && "tabular text-right",
|
|
78
|
+
column.className,
|
|
79
|
+
)}
|
|
80
|
+
>
|
|
81
|
+
{column.cell(row)}
|
|
82
|
+
</td>
|
|
83
|
+
))}
|
|
84
|
+
</tr>
|
|
85
|
+
))}
|
|
86
|
+
</tbody>
|
|
87
|
+
</table>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* An empty state that says what this screen is for and offers the one action
|
|
6
|
+
* that fills it. "No records found" tells a new user nothing and gives them
|
|
7
|
+
* nowhere to go.
|
|
8
|
+
*/
|
|
9
|
+
export function EmptyState({
|
|
10
|
+
icon,
|
|
11
|
+
title,
|
|
12
|
+
description,
|
|
13
|
+
action,
|
|
14
|
+
className,
|
|
15
|
+
}: {
|
|
16
|
+
icon?: React.ReactNode;
|
|
17
|
+
title: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
action?: React.ReactNode;
|
|
20
|
+
className?: string;
|
|
21
|
+
}) {
|
|
22
|
+
return (
|
|
23
|
+
<div
|
|
24
|
+
className={cn(
|
|
25
|
+
"flex flex-col items-center justify-center px-6 py-16 text-center",
|
|
26
|
+
className,
|
|
27
|
+
)}
|
|
28
|
+
>
|
|
29
|
+
{icon ? (
|
|
30
|
+
<div className="mb-3 flex size-9 items-center justify-center rounded-lg bg-surface-2 text-muted-foreground [&_svg]:size-4">
|
|
31
|
+
{icon}
|
|
32
|
+
</div>
|
|
33
|
+
) : null}
|
|
34
|
+
<p className="text-[13px] font-medium">{title}</p>
|
|
35
|
+
{description ? (
|
|
36
|
+
<p className="mt-1 max-w-sm text-[12px] leading-5 text-muted-foreground">
|
|
37
|
+
{description}
|
|
38
|
+
</p>
|
|
39
|
+
) : null}
|
|
40
|
+
{action ? <div className="mt-4">{action}</div> : null}
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
|
|
4
|
+
/** A keyboard hint. Present in the UI because the shortcuts are real. */
|
|
5
|
+
export function Kbd({
|
|
6
|
+
children,
|
|
7
|
+
className,
|
|
8
|
+
}: {
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
className?: string;
|
|
11
|
+
}) {
|
|
12
|
+
return (
|
|
13
|
+
<kbd
|
|
14
|
+
className={cn(
|
|
15
|
+
"inline-flex h-5 min-w-5 items-center justify-center rounded border border-border bg-surface-2 px-1.5",
|
|
16
|
+
"font-sans text-[10px] font-medium text-muted-foreground",
|
|
17
|
+
className,
|
|
18
|
+
)}
|
|
19
|
+
>
|
|
20
|
+
{children}
|
|
21
|
+
</kbd>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The bar at the top of every view: title on the left, actions on the right.
|
|
6
|
+
* Fixed height so switching pages doesn't shift the content below it.
|
|
7
|
+
*/
|
|
8
|
+
export function PageHeader({
|
|
9
|
+
title,
|
|
10
|
+
count,
|
|
11
|
+
children,
|
|
12
|
+
className,
|
|
13
|
+
}: {
|
|
14
|
+
title: string;
|
|
15
|
+
/** Shown beside the title — the number of rows in view. */
|
|
16
|
+
count?: number;
|
|
17
|
+
children?: React.ReactNode;
|
|
18
|
+
className?: string;
|
|
19
|
+
}) {
|
|
20
|
+
return (
|
|
21
|
+
<header
|
|
22
|
+
className={cn(
|
|
23
|
+
"flex h-12 shrink-0 items-center justify-between gap-3 border-b border-border px-4",
|
|
24
|
+
className,
|
|
25
|
+
)}
|
|
26
|
+
>
|
|
27
|
+
<div className="flex min-w-0 items-baseline gap-2">
|
|
28
|
+
<h1 className="truncate text-[13px] font-semibold">{title}</h1>
|
|
29
|
+
{typeof count === "number" ? (
|
|
30
|
+
<span className="tabular text-[12px] text-muted-foreground">{count}</span>
|
|
31
|
+
) : null}
|
|
32
|
+
</div>
|
|
33
|
+
<div className="flex items-center gap-2">{children}</div>
|
|
34
|
+
</header>
|
|
35
|
+
);
|
|
36
|
+
}
|