@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,30 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
function Input({
|
|
6
|
+
className,
|
|
7
|
+
type,
|
|
8
|
+
...props
|
|
9
|
+
}: React.InputHTMLAttributes<HTMLInputElement>) {
|
|
10
|
+
return (
|
|
11
|
+
<input
|
|
12
|
+
type={type}
|
|
13
|
+
data-slot="input"
|
|
14
|
+
className={cn(
|
|
15
|
+
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors",
|
|
16
|
+
"placeholder:text-muted-foreground",
|
|
17
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
18
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
19
|
+
// Date/time inputs render a native picker indicator that ignores the
|
|
20
|
+
// text color; nudge it so it stays visible in dark mode.
|
|
21
|
+
"[&::-webkit-calendar-picker-indicator]:opacity-60 dark:[&::-webkit-calendar-picker-indicator]:invert",
|
|
22
|
+
"file:border-0 file:bg-transparent file:text-sm file:font-medium",
|
|
23
|
+
className,
|
|
24
|
+
)}
|
|
25
|
+
{...props}
|
|
26
|
+
/>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export { Input };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
function Label({
|
|
6
|
+
className,
|
|
7
|
+
...props
|
|
8
|
+
}: React.LabelHTMLAttributes<HTMLLabelElement>) {
|
|
9
|
+
return (
|
|
10
|
+
<label
|
|
11
|
+
data-slot="label"
|
|
12
|
+
className={cn(
|
|
13
|
+
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
|
14
|
+
className,
|
|
15
|
+
)}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { Label };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { ChevronDown } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A styled native `<select>`.
|
|
8
|
+
*
|
|
9
|
+
* shadcn's Select is a Radix listbox; this deliberately isn't. A native select
|
|
10
|
+
* needs no extra dependency, is keyboard- and screen-reader-correct for free,
|
|
11
|
+
* and opens as the platform picker on mobile — which is what you want for the
|
|
12
|
+
* plain "pick one value" case that scaffolded forms are full of. Reach for
|
|
13
|
+
* `npx shadcn add select` when you need custom option rendering or search.
|
|
14
|
+
*/
|
|
15
|
+
function Select({
|
|
16
|
+
className,
|
|
17
|
+
children,
|
|
18
|
+
...props
|
|
19
|
+
}: React.SelectHTMLAttributes<HTMLSelectElement>) {
|
|
20
|
+
return (
|
|
21
|
+
<div className="relative">
|
|
22
|
+
<select
|
|
23
|
+
data-slot="select"
|
|
24
|
+
className={cn(
|
|
25
|
+
"flex h-9 w-full appearance-none rounded-md border border-input bg-transparent py-1 pl-3 pr-8 text-sm shadow-sm transition-colors",
|
|
26
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
27
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
28
|
+
className,
|
|
29
|
+
)}
|
|
30
|
+
{...props}
|
|
31
|
+
>
|
|
32
|
+
{children}
|
|
33
|
+
</select>
|
|
34
|
+
<ChevronDown className="pointer-events-none absolute right-2.5 top-1/2 size-4 -translate-y-1/2 opacity-50" />
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export { Select };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
// The wrapper owns the horizontal scroll so a wide table never widens the page.
|
|
6
|
+
function Table({ className, ...props }: React.HTMLAttributes<HTMLTableElement>) {
|
|
7
|
+
return (
|
|
8
|
+
<div className="relative w-full overflow-x-auto">
|
|
9
|
+
<table
|
|
10
|
+
data-slot="table"
|
|
11
|
+
className={cn("w-full caption-bottom text-sm", className)}
|
|
12
|
+
{...props}
|
|
13
|
+
/>
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function TableHeader({
|
|
19
|
+
className,
|
|
20
|
+
...props
|
|
21
|
+
}: React.HTMLAttributes<HTMLTableSectionElement>) {
|
|
22
|
+
return (
|
|
23
|
+
<thead
|
|
24
|
+
data-slot="table-header"
|
|
25
|
+
className={cn("[&_tr]:border-b", className)}
|
|
26
|
+
{...props}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function TableBody({
|
|
32
|
+
className,
|
|
33
|
+
...props
|
|
34
|
+
}: React.HTMLAttributes<HTMLTableSectionElement>) {
|
|
35
|
+
return (
|
|
36
|
+
<tbody
|
|
37
|
+
data-slot="table-body"
|
|
38
|
+
className={cn("[&_tr:last-child]:border-0", className)}
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function TableFooter({
|
|
45
|
+
className,
|
|
46
|
+
...props
|
|
47
|
+
}: React.HTMLAttributes<HTMLTableSectionElement>) {
|
|
48
|
+
return (
|
|
49
|
+
<tfoot
|
|
50
|
+
data-slot="table-footer"
|
|
51
|
+
className={cn("border-t bg-muted/50 font-medium", className)}
|
|
52
|
+
{...props}
|
|
53
|
+
/>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function TableRow({
|
|
58
|
+
className,
|
|
59
|
+
...props
|
|
60
|
+
}: React.HTMLAttributes<HTMLTableRowElement>) {
|
|
61
|
+
return (
|
|
62
|
+
<tr
|
|
63
|
+
data-slot="table-row"
|
|
64
|
+
className={cn(
|
|
65
|
+
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
|
66
|
+
className,
|
|
67
|
+
)}
|
|
68
|
+
{...props}
|
|
69
|
+
/>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function TableHead({
|
|
74
|
+
className,
|
|
75
|
+
...props
|
|
76
|
+
}: React.ThHTMLAttributes<HTMLTableCellElement>) {
|
|
77
|
+
return (
|
|
78
|
+
<th
|
|
79
|
+
data-slot="table-head"
|
|
80
|
+
className={cn(
|
|
81
|
+
"h-10 px-3 text-left align-middle text-xs font-medium text-muted-foreground",
|
|
82
|
+
className,
|
|
83
|
+
)}
|
|
84
|
+
{...props}
|
|
85
|
+
/>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function TableCell({
|
|
90
|
+
className,
|
|
91
|
+
...props
|
|
92
|
+
}: React.TdHTMLAttributes<HTMLTableCellElement>) {
|
|
93
|
+
return (
|
|
94
|
+
<td
|
|
95
|
+
data-slot="table-cell"
|
|
96
|
+
className={cn("p-3 align-middle", className)}
|
|
97
|
+
{...props}
|
|
98
|
+
/>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function TableCaption({
|
|
103
|
+
className,
|
|
104
|
+
...props
|
|
105
|
+
}: React.HTMLAttributes<HTMLTableCaptionElement>) {
|
|
106
|
+
return (
|
|
107
|
+
<caption
|
|
108
|
+
data-slot="table-caption"
|
|
109
|
+
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
|
110
|
+
{...props}
|
|
111
|
+
/>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export {
|
|
116
|
+
Table,
|
|
117
|
+
TableHeader,
|
|
118
|
+
TableBody,
|
|
119
|
+
TableFooter,
|
|
120
|
+
TableHead,
|
|
121
|
+
TableRow,
|
|
122
|
+
TableCell,
|
|
123
|
+
TableCaption,
|
|
124
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
function Textarea({
|
|
6
|
+
className,
|
|
7
|
+
...props
|
|
8
|
+
}: React.TextareaHTMLAttributes<HTMLTextAreaElement>) {
|
|
9
|
+
return (
|
|
10
|
+
<textarea
|
|
11
|
+
data-slot="textarea"
|
|
12
|
+
className={cn(
|
|
13
|
+
"flex min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm transition-colors",
|
|
14
|
+
"placeholder:text-muted-foreground",
|
|
15
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
16
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
17
|
+
className,
|
|
18
|
+
)}
|
|
19
|
+
{...props}
|
|
20
|
+
/>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export { Textarea };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "new-york",
|
|
4
|
+
"rsc": false,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "",
|
|
8
|
+
"css": "app/globals.css",
|
|
9
|
+
"baseColor": "zinc",
|
|
10
|
+
"cssVariables": true
|
|
11
|
+
},
|
|
12
|
+
"aliases": {
|
|
13
|
+
"components": "@/components",
|
|
14
|
+
"utils": "@/lib/utils",
|
|
15
|
+
"ui": "@/components/ui",
|
|
16
|
+
"lib": "@/lib",
|
|
17
|
+
"hooks": "@/hooks"
|
|
18
|
+
},
|
|
19
|
+
"iconLibrary": "lucide"
|
|
20
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { mutation, v } from "@pylonsync/functions";
|
|
2
|
+
import { isValidPriority } from "../lib/tickets";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Open a ticket on a customer's behalf — the "log a phone call" path.
|
|
6
|
+
*
|
|
7
|
+
* Writes the ticket and its first message together so the thread is never
|
|
8
|
+
* empty, and marks that first message as coming FROM the customer: it's their
|
|
9
|
+
* report, typed in by an agent, and the SLA clock should start unanswered.
|
|
10
|
+
*/
|
|
11
|
+
export default mutation<
|
|
12
|
+
{
|
|
13
|
+
subject: string;
|
|
14
|
+
body: string;
|
|
15
|
+
customerId?: string;
|
|
16
|
+
priority?: string;
|
|
17
|
+
},
|
|
18
|
+
{ ok: true; id: string }
|
|
19
|
+
>({
|
|
20
|
+
auth: "user",
|
|
21
|
+
args: {
|
|
22
|
+
subject: v.string(),
|
|
23
|
+
body: v.string(),
|
|
24
|
+
customerId: v.optional(v.id("Customer")),
|
|
25
|
+
priority: v.optional(v.string()),
|
|
26
|
+
},
|
|
27
|
+
async handler(ctx, args) {
|
|
28
|
+
const subject = args.subject.trim();
|
|
29
|
+
if (!subject) throw ctx.error("INVALID_ARGS", "A ticket needs a subject.");
|
|
30
|
+
|
|
31
|
+
const priority = args.priority ?? "normal";
|
|
32
|
+
if (!isValidPriority(priority)) {
|
|
33
|
+
throw ctx.error("INVALID_ARGS", `Unknown priority "${priority}".`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const now = new Date().toISOString();
|
|
37
|
+
const ticketId = await ctx.db.insert("Ticket", {
|
|
38
|
+
subject,
|
|
39
|
+
status: "open",
|
|
40
|
+
priority,
|
|
41
|
+
customerId: args.customerId ?? null,
|
|
42
|
+
assigneeId: null,
|
|
43
|
+
firstRespondedAt: null,
|
|
44
|
+
createdAt: now,
|
|
45
|
+
updatedAt: now,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const body = args.body.trim();
|
|
49
|
+
if (body) {
|
|
50
|
+
await ctx.db.insert("Message", {
|
|
51
|
+
ticketId,
|
|
52
|
+
body,
|
|
53
|
+
fromCustomer: true,
|
|
54
|
+
internal: false,
|
|
55
|
+
authorId: null,
|
|
56
|
+
createdAt: now,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { ok: true, id: ticketId as string } as const;
|
|
61
|
+
},
|
|
62
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { mutation, v } from "@pylonsync/functions";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Post an agent message on a ticket.
|
|
5
|
+
*
|
|
6
|
+
* A mutation rather than a bare insert because a reply changes the ticket too:
|
|
7
|
+
* the first public reply stamps `firstRespondedAt`, which is what the SLA state
|
|
8
|
+
* is computed from. Doing that client-side would let a slow or offline tab
|
|
9
|
+
* decide when the clock stopped.
|
|
10
|
+
*
|
|
11
|
+
* An INTERNAL note deliberately does NOT stop the clock — the customer is still
|
|
12
|
+
* waiting, and a team that could clear its SLA by talking to itself would have
|
|
13
|
+
* an SLA worth nothing.
|
|
14
|
+
*/
|
|
15
|
+
export default mutation<
|
|
16
|
+
{ ticketId: string; body: string; internal?: boolean },
|
|
17
|
+
{ ok: true; id: string }
|
|
18
|
+
>({
|
|
19
|
+
auth: "user",
|
|
20
|
+
args: {
|
|
21
|
+
ticketId: v.id("Ticket"),
|
|
22
|
+
body: v.string(),
|
|
23
|
+
internal: v.optional(v.boolean()),
|
|
24
|
+
},
|
|
25
|
+
async handler(ctx, args) {
|
|
26
|
+
const body = args.body.trim();
|
|
27
|
+
if (!body) throw ctx.error("INVALID_ARGS", "A reply needs a body.");
|
|
28
|
+
|
|
29
|
+
const ticket = await ctx.db.get("Ticket", args.ticketId);
|
|
30
|
+
if (!ticket) throw ctx.error("NOT_FOUND", "Ticket not found.");
|
|
31
|
+
|
|
32
|
+
const internal = args.internal ?? false;
|
|
33
|
+
const now = new Date().toISOString();
|
|
34
|
+
|
|
35
|
+
const id = await ctx.db.insert("Message", {
|
|
36
|
+
ticketId: args.ticketId,
|
|
37
|
+
body,
|
|
38
|
+
fromCustomer: false,
|
|
39
|
+
internal,
|
|
40
|
+
authorId: ctx.auth.userId,
|
|
41
|
+
createdAt: now,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const patch: Record<string, unknown> = { updatedAt: now };
|
|
45
|
+
if (!internal && !ticket.firstRespondedAt) {
|
|
46
|
+
patch.firstRespondedAt = now;
|
|
47
|
+
}
|
|
48
|
+
// Replying to something nobody owns takes ownership — the common case, and
|
|
49
|
+
// it stops two agents answering the same ticket.
|
|
50
|
+
if (!ticket.assigneeId) patch.assigneeId = ctx.auth.userId;
|
|
51
|
+
await ctx.db.update("Ticket", args.ticketId, patch);
|
|
52
|
+
|
|
53
|
+
return { ok: true, id: id as string } as const;
|
|
54
|
+
},
|
|
55
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { mutation } from "@pylonsync/functions";
|
|
2
|
+
import { shapeSeed } from "../lib/seed";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Fill a brand-new helpdesk with a realistic queue, once.
|
|
6
|
+
*
|
|
7
|
+
* An empty inbox demonstrates nothing about triage, SLA, or the thread view.
|
|
8
|
+
* The client calls this after sign-in; it returns immediately if any ticket
|
|
9
|
+
* already exists, so it is safe on every load and can never duplicate the
|
|
10
|
+
* fixtures or touch real data.
|
|
11
|
+
*
|
|
12
|
+
* Delete this function and lib/seed.ts once real tickets arrive.
|
|
13
|
+
*/
|
|
14
|
+
export default mutation<Record<string, never>, { seeded: boolean }>({
|
|
15
|
+
auth: "user",
|
|
16
|
+
args: {},
|
|
17
|
+
async handler(ctx) {
|
|
18
|
+
const existing = await ctx.db.query("Ticket", { $limit: 1 });
|
|
19
|
+
if (existing.length > 0) return { seeded: false };
|
|
20
|
+
|
|
21
|
+
const seed = shapeSeed();
|
|
22
|
+
const me = ctx.auth.userId;
|
|
23
|
+
|
|
24
|
+
const customerIds = new Map<string, string>();
|
|
25
|
+
for (const customer of seed.customers) {
|
|
26
|
+
const id = await ctx.db.insert("Customer", customer.row);
|
|
27
|
+
customerIds.set(customer.key, id as string);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const ticketIds = new Map<string, string>();
|
|
31
|
+
for (const ticket of seed.tickets) {
|
|
32
|
+
const id = await ctx.db.insert("Ticket", {
|
|
33
|
+
...ticket.row,
|
|
34
|
+
customerId: customerIds.get(ticket.customer) ?? null,
|
|
35
|
+
// Answered tickets are assigned to whoever is seeding; the unanswered
|
|
36
|
+
// ones stay unassigned so the queue has something to pick up.
|
|
37
|
+
assigneeId: ticket.row.firstRespondedAt ? me : null,
|
|
38
|
+
});
|
|
39
|
+
ticketIds.set(ticket.key, id as string);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
for (const message of seed.messages) {
|
|
43
|
+
await ctx.db.insert("Message", {
|
|
44
|
+
...message.row,
|
|
45
|
+
ticketId: ticketIds.get(message.ticket) ?? null,
|
|
46
|
+
authorId: message.row.fromCustomer ? null : me,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { seeded: true };
|
|
51
|
+
},
|
|
52
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { mutation, v } from "@pylonsync/functions";
|
|
2
|
+
import { isValidPriority, isValidStatus } from "../lib/tickets";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Change a ticket's status, priority, or assignee.
|
|
6
|
+
*
|
|
7
|
+
* One mutation for all three because they share the same validation and the
|
|
8
|
+
* same `updatedAt` bump, and because a queue that accepts an unknown status
|
|
9
|
+
* strands the ticket in a filter nothing renders. Every value is checked
|
|
10
|
+
* server-side — the UI only ever offers known ones, but the endpoint is
|
|
11
|
+
* reachable directly.
|
|
12
|
+
*
|
|
13
|
+
* `assigneeId: null` explicitly unassigns; omitting it leaves the owner alone.
|
|
14
|
+
*/
|
|
15
|
+
export default mutation<
|
|
16
|
+
{
|
|
17
|
+
ticketId: string;
|
|
18
|
+
status?: string;
|
|
19
|
+
priority?: string;
|
|
20
|
+
assigneeId?: string | null;
|
|
21
|
+
},
|
|
22
|
+
{ ok: true }
|
|
23
|
+
>({
|
|
24
|
+
auth: "user",
|
|
25
|
+
args: {
|
|
26
|
+
ticketId: v.id("Ticket"),
|
|
27
|
+
status: v.optional(v.string()),
|
|
28
|
+
priority: v.optional(v.string()),
|
|
29
|
+
assigneeId: v.optional(v.id("User")),
|
|
30
|
+
},
|
|
31
|
+
async handler(ctx, args) {
|
|
32
|
+
const ticket = await ctx.db.get("Ticket", args.ticketId);
|
|
33
|
+
if (!ticket) throw ctx.error("NOT_FOUND", "Ticket not found.");
|
|
34
|
+
|
|
35
|
+
const patch: Record<string, unknown> = { updatedAt: new Date().toISOString() };
|
|
36
|
+
|
|
37
|
+
if (args.status !== undefined) {
|
|
38
|
+
if (!isValidStatus(args.status)) {
|
|
39
|
+
throw ctx.error("INVALID_ARGS", `Unknown status "${args.status}".`);
|
|
40
|
+
}
|
|
41
|
+
patch.status = args.status;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (args.priority !== undefined) {
|
|
45
|
+
if (!isValidPriority(args.priority)) {
|
|
46
|
+
throw ctx.error("INVALID_ARGS", `Unknown priority "${args.priority}".`);
|
|
47
|
+
}
|
|
48
|
+
patch.priority = args.priority;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (args.assigneeId !== undefined) {
|
|
52
|
+
patch.assigneeId = args.assigneeId || null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
await ctx.db.update("Ticket", args.ticketId, patch);
|
|
56
|
+
return { ok: true } as const;
|
|
57
|
+
},
|
|
58
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Small display helpers shared across views. Pure, so the timeline and the
|
|
2
|
+
// queue can't drift on how they render the same instant.
|
|
3
|
+
|
|
4
|
+
/** Initials for an avatar: "Dana Whitfield" → "DW", "jordan@x.com" → "JO". */
|
|
5
|
+
export function initials(nameOrEmail: string | null | undefined): string {
|
|
6
|
+
const raw = (nameOrEmail ?? "").trim();
|
|
7
|
+
if (!raw) return "?";
|
|
8
|
+
const name = raw.includes("@") ? raw.split("@")[0] : raw;
|
|
9
|
+
const parts = name.split(/[\s._-]+/).filter(Boolean);
|
|
10
|
+
if (parts.length === 0) return "?";
|
|
11
|
+
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
|
|
12
|
+
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** A stable accent per record, so the same person keeps their colour. */
|
|
16
|
+
export function accentIndex(seed: string, buckets = 6): number {
|
|
17
|
+
let hash = 0;
|
|
18
|
+
for (let i = 0; i < seed.length; i += 1) {
|
|
19
|
+
hash = (hash * 31 + seed.charCodeAt(i)) | 0;
|
|
20
|
+
}
|
|
21
|
+
return Math.abs(hash) % buckets;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** "just now" / "4h ago" / "Mar 3". */
|
|
25
|
+
export function relativeTime(
|
|
26
|
+
iso: string | null | undefined,
|
|
27
|
+
now: number = Date.now(),
|
|
28
|
+
): string {
|
|
29
|
+
if (!iso) return "";
|
|
30
|
+
const then = Date.parse(iso);
|
|
31
|
+
if (!Number.isFinite(then)) return "";
|
|
32
|
+
const seconds = Math.round((now - then) / 1000);
|
|
33
|
+
if (seconds < 45) return "just now";
|
|
34
|
+
const minutes = Math.round(seconds / 60);
|
|
35
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
36
|
+
const hours = Math.round(minutes / 60);
|
|
37
|
+
if (hours < 24) return `${hours}h ago`;
|
|
38
|
+
const days = Math.round(hours / 24);
|
|
39
|
+
if (days < 7) return `${days}d ago`;
|
|
40
|
+
return new Date(then).toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** "3h 20m" / "45m" / "2d" — a duration an agent reads at a glance. */
|
|
44
|
+
export function duration(minutes: number): string {
|
|
45
|
+
const abs = Math.abs(Math.round(minutes));
|
|
46
|
+
if (abs < 60) return `${abs}m`;
|
|
47
|
+
if (abs < 1440) {
|
|
48
|
+
const hours = Math.floor(abs / 60);
|
|
49
|
+
const rest = abs % 60;
|
|
50
|
+
return rest === 0 ? `${hours}h` : `${hours}h ${rest}m`;
|
|
51
|
+
}
|
|
52
|
+
return `${Math.floor(abs / 1440)}d`;
|
|
53
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Client-side search powering the ⌘K palette.
|
|
2
|
+
//
|
|
3
|
+
// A small team's CRM fits in the synced replica, so search runs locally against
|
|
4
|
+
// data already in memory: results appear as you type, with no request per
|
|
5
|
+
// keystroke. Swap in `db.useSearch` (Pylon's FTS) when a workspace outgrows
|
|
6
|
+
// that — the component boundary doesn't change, only this file.
|
|
7
|
+
|
|
8
|
+
export interface SearchItem {
|
|
9
|
+
id: string;
|
|
10
|
+
type: "deal" | "company" | "contact";
|
|
11
|
+
title: string;
|
|
12
|
+
/** Second line: the company for a contact, the domain for a company. */
|
|
13
|
+
subtitle?: string;
|
|
14
|
+
href: string;
|
|
15
|
+
/** Extra text matched but not displayed — an email, say. */
|
|
16
|
+
keywords?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface Scored extends SearchItem {
|
|
20
|
+
score: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Rank items against a query. Prefix matches beat word-boundary matches, which
|
|
25
|
+
* beat substring matches; ties break on the shorter title, so "Acme" ranks
|
|
26
|
+
* above "Acme Corporation Holdings" for the query "acme".
|
|
27
|
+
*/
|
|
28
|
+
export function searchItems(
|
|
29
|
+
items: SearchItem[],
|
|
30
|
+
query: string,
|
|
31
|
+
limit = 8,
|
|
32
|
+
): SearchItem[] {
|
|
33
|
+
const q = query.trim().toLowerCase();
|
|
34
|
+
if (!q) return items.slice(0, limit);
|
|
35
|
+
|
|
36
|
+
const scored: Scored[] = [];
|
|
37
|
+
for (const item of items) {
|
|
38
|
+
const score = scoreItem(item, q);
|
|
39
|
+
if (score > 0) scored.push({ ...item, score });
|
|
40
|
+
}
|
|
41
|
+
scored.sort(
|
|
42
|
+
(a, b) => b.score - a.score || a.title.length - b.title.length ||
|
|
43
|
+
a.title.localeCompare(b.title),
|
|
44
|
+
);
|
|
45
|
+
return scored.slice(0, limit);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function scoreItem(item: SearchItem, q: string): number {
|
|
49
|
+
const title = item.title.toLowerCase();
|
|
50
|
+
if (title.startsWith(q)) return 100;
|
|
51
|
+
if (wordStart(title, q)) return 80;
|
|
52
|
+
if (title.includes(q)) return 60;
|
|
53
|
+
|
|
54
|
+
const haystack = `${item.subtitle ?? ""} ${item.keywords ?? ""}`.toLowerCase();
|
|
55
|
+
if (wordStart(haystack, q)) return 40;
|
|
56
|
+
if (haystack.includes(q)) return 20;
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function wordStart(haystack: string, needle: string): boolean {
|
|
61
|
+
let from = 0;
|
|
62
|
+
for (;;) {
|
|
63
|
+
const at = haystack.indexOf(needle, from);
|
|
64
|
+
if (at === -1) return false;
|
|
65
|
+
if (at === 0 || /[\s._@/-]/.test(haystack[at - 1])) return true;
|
|
66
|
+
from = at + 1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Move through a list with arrow keys, wrapping at both ends. */
|
|
71
|
+
export function moveSelection(
|
|
72
|
+
current: number,
|
|
73
|
+
delta: number,
|
|
74
|
+
length: number,
|
|
75
|
+
): number {
|
|
76
|
+
if (length === 0) return 0;
|
|
77
|
+
return (current + delta + length) % length;
|
|
78
|
+
}
|