@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,164 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { Link, callFn, useRouter } from "@pylonsync/react";
|
|
5
|
+
import { ArrowLeft } from "lucide-react";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import { Select } from "@/components/ui/select";
|
|
8
|
+
import { PageHeader } from "@/components/page-header";
|
|
9
|
+
import { EmptyState } from "@/components/empty-state";
|
|
10
|
+
import { Thread } from "@/components/thread";
|
|
11
|
+
import { PriorityBadge } from "@/components/priority-badge";
|
|
12
|
+
import { SlaIndicator } from "@/components/sla-indicator";
|
|
13
|
+
import { Avatar } from "@/components/avatar";
|
|
14
|
+
import { relativeTime } from "@/lib/format";
|
|
15
|
+
import { PRIORITIES, STATUSES, ticketNumber } from "@/lib/tickets";
|
|
16
|
+
import { Workspace } from "../../workspace";
|
|
17
|
+
|
|
18
|
+
export function TicketView({
|
|
19
|
+
email,
|
|
20
|
+
ticketId,
|
|
21
|
+
}: {
|
|
22
|
+
email: string;
|
|
23
|
+
ticketId: string;
|
|
24
|
+
}) {
|
|
25
|
+
const router = useRouter();
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Workspace email={email} pathname="/">
|
|
29
|
+
{(data) => {
|
|
30
|
+
const ticket = data.tickets.find((t) => t.id === ticketId);
|
|
31
|
+
|
|
32
|
+
if (!ticket) {
|
|
33
|
+
return (
|
|
34
|
+
<>
|
|
35
|
+
<PageHeader title="Ticket" />
|
|
36
|
+
<EmptyState
|
|
37
|
+
title={data.loading ? "Loading…" : "Ticket not found"}
|
|
38
|
+
description={
|
|
39
|
+
data.loading
|
|
40
|
+
? undefined
|
|
41
|
+
: "It may have been deleted, or the link is wrong."
|
|
42
|
+
}
|
|
43
|
+
action={
|
|
44
|
+
data.loading ? undefined : (
|
|
45
|
+
<Button size="sm" variant="secondary" asChild>
|
|
46
|
+
<Link href="/">Back to inbox</Link>
|
|
47
|
+
</Button>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
/>
|
|
51
|
+
</>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const customer = data.customers.find((c) => c.id === ticket.customerId);
|
|
56
|
+
const messages = data.messages.filter((m) => m.ticketId === ticket.id);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<>
|
|
60
|
+
<PageHeader title={ticket.subject}>
|
|
61
|
+
<SlaIndicator ticket={ticket} />
|
|
62
|
+
<div className="w-28">
|
|
63
|
+
<Select
|
|
64
|
+
aria-label="Priority"
|
|
65
|
+
value={ticket.priority}
|
|
66
|
+
className="h-7 text-[12px]"
|
|
67
|
+
onChange={(event) =>
|
|
68
|
+
void callFn("setTicketState", {
|
|
69
|
+
ticketId: ticket.id,
|
|
70
|
+
priority: event.target.value,
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
>
|
|
74
|
+
{PRIORITIES.map((p) => (
|
|
75
|
+
<option key={p.id} value={p.id}>
|
|
76
|
+
{p.label}
|
|
77
|
+
</option>
|
|
78
|
+
))}
|
|
79
|
+
</Select>
|
|
80
|
+
</div>
|
|
81
|
+
<div className="w-28">
|
|
82
|
+
<Select
|
|
83
|
+
aria-label="Status"
|
|
84
|
+
value={ticket.status}
|
|
85
|
+
className="h-7 text-[12px]"
|
|
86
|
+
onChange={(event) =>
|
|
87
|
+
void callFn("setTicketState", {
|
|
88
|
+
ticketId: ticket.id,
|
|
89
|
+
status: event.target.value,
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
>
|
|
93
|
+
{STATUSES.map((s) => (
|
|
94
|
+
<option key={s.id} value={s.id}>
|
|
95
|
+
{s.label}
|
|
96
|
+
</option>
|
|
97
|
+
))}
|
|
98
|
+
</Select>
|
|
99
|
+
</div>
|
|
100
|
+
</PageHeader>
|
|
101
|
+
|
|
102
|
+
<div className="flex items-center gap-3 border-b border-border px-6 py-2.5 text-[12px] text-muted-foreground">
|
|
103
|
+
<Link
|
|
104
|
+
href="/"
|
|
105
|
+
className="inline-flex items-center gap-1.5 transition-colors hover:text-foreground"
|
|
106
|
+
>
|
|
107
|
+
<ArrowLeft className="size-3.5" />
|
|
108
|
+
Inbox
|
|
109
|
+
</Link>
|
|
110
|
+
<span aria-hidden="true">·</span>
|
|
111
|
+
<span className="tabular">
|
|
112
|
+
{ticketNumber(ticket.id, ticket.createdAt)}
|
|
113
|
+
</span>
|
|
114
|
+
<span aria-hidden="true">·</span>
|
|
115
|
+
<PriorityBadge priority={ticket.priority} />
|
|
116
|
+
{customer ? (
|
|
117
|
+
<>
|
|
118
|
+
<span aria-hidden="true">·</span>
|
|
119
|
+
<span className="flex items-center gap-1.5">
|
|
120
|
+
<Avatar name={customer.name} size="sm" />
|
|
121
|
+
{customer.name}
|
|
122
|
+
{customer.company ? ` · ${customer.company}` : ""}
|
|
123
|
+
</span>
|
|
124
|
+
</>
|
|
125
|
+
) : null}
|
|
126
|
+
<span aria-hidden="true">·</span>
|
|
127
|
+
<span>Opened {relativeTime(ticket.createdAt)}</span>
|
|
128
|
+
|
|
129
|
+
<div className="ml-auto w-40">
|
|
130
|
+
<Select
|
|
131
|
+
aria-label="Assignee"
|
|
132
|
+
value={ticket.assigneeId ?? ""}
|
|
133
|
+
className="h-7 text-[12px]"
|
|
134
|
+
onChange={(event) =>
|
|
135
|
+
void callFn("setTicketState", {
|
|
136
|
+
ticketId: ticket.id,
|
|
137
|
+
assigneeId: event.target.value,
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
>
|
|
141
|
+
<option value="">Unassigned</option>
|
|
142
|
+
{data.agents.map((agent) => (
|
|
143
|
+
<option key={agent.id} value={agent.id}>
|
|
144
|
+
{agent.displayName || agent.email}
|
|
145
|
+
</option>
|
|
146
|
+
))}
|
|
147
|
+
</Select>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<Thread
|
|
152
|
+
messages={messages}
|
|
153
|
+
authorName={data.agentName}
|
|
154
|
+
customerName={customer?.name ?? null}
|
|
155
|
+
onSend={(body, internal) =>
|
|
156
|
+
callFn("replyToTicket", { ticketId: ticket.id, body, internal })
|
|
157
|
+
}
|
|
158
|
+
/>
|
|
159
|
+
</>
|
|
160
|
+
);
|
|
161
|
+
}}
|
|
162
|
+
</Workspace>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { callFn, db, useRouter } from "@pylonsync/react";
|
|
5
|
+
import { useAuth } from "@pylonsync/client";
|
|
6
|
+
import { Sidebar } from "@/components/sidebar";
|
|
7
|
+
import { CommandPalette } from "@/components/command-palette";
|
|
8
|
+
import type { SearchItem } from "@/lib/search";
|
|
9
|
+
import { counts, isOpen } from "@/lib/tickets";
|
|
10
|
+
|
|
11
|
+
export interface CustomerRow {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
email: string;
|
|
15
|
+
company?: string | null;
|
|
16
|
+
createdAt?: string | null;
|
|
17
|
+
}
|
|
18
|
+
export interface TicketRow {
|
|
19
|
+
id: string;
|
|
20
|
+
subject: string;
|
|
21
|
+
status: string;
|
|
22
|
+
priority: string;
|
|
23
|
+
customerId?: string | null;
|
|
24
|
+
assigneeId?: string | null;
|
|
25
|
+
firstRespondedAt?: string | null;
|
|
26
|
+
createdAt?: string | null;
|
|
27
|
+
updatedAt?: string | null;
|
|
28
|
+
}
|
|
29
|
+
export interface MessageRow {
|
|
30
|
+
id: string;
|
|
31
|
+
ticketId: string;
|
|
32
|
+
body: string;
|
|
33
|
+
fromCustomer?: boolean | null;
|
|
34
|
+
internal?: boolean | null;
|
|
35
|
+
authorId?: string | null;
|
|
36
|
+
createdAt?: string | null;
|
|
37
|
+
}
|
|
38
|
+
export interface UserRow {
|
|
39
|
+
id: string;
|
|
40
|
+
email: string;
|
|
41
|
+
displayName?: string | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface WorkspaceData {
|
|
45
|
+
customers: CustomerRow[];
|
|
46
|
+
tickets: TicketRow[];
|
|
47
|
+
messages: MessageRow[];
|
|
48
|
+
agents: UserRow[];
|
|
49
|
+
customerName: (id: string | null | undefined) => string | null;
|
|
50
|
+
agentName: (id: string | null | undefined) => string | null;
|
|
51
|
+
loading: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The app shell, and the ONLY place that touches `db`.
|
|
56
|
+
*
|
|
57
|
+
* Every view below receives plain data through the render prop and reports
|
|
58
|
+
* changes through callbacks, which is what lets the components in `components/`
|
|
59
|
+
* render from fixtures in a test with nothing mocked.
|
|
60
|
+
*
|
|
61
|
+
* Each `db.useQuery` is live: a ticket another agent assigns, or a reply they
|
|
62
|
+
* send, arrives here and re-renders the queue. Your own writes take the same
|
|
63
|
+
* path, so there's one behaviour to reason about rather than two.
|
|
64
|
+
*/
|
|
65
|
+
export function Workspace({
|
|
66
|
+
email,
|
|
67
|
+
pathname,
|
|
68
|
+
children,
|
|
69
|
+
}: {
|
|
70
|
+
email: string;
|
|
71
|
+
pathname: string;
|
|
72
|
+
children: (data: WorkspaceData) => React.ReactNode;
|
|
73
|
+
}) {
|
|
74
|
+
const router = useRouter();
|
|
75
|
+
const { signOut } = useAuth();
|
|
76
|
+
const { data: tickets, loading } = db.useQuery<TicketRow>("Ticket");
|
|
77
|
+
const { data: customers } = db.useQuery<CustomerRow>("Customer");
|
|
78
|
+
const { data: messages } = db.useQuery<MessageRow>("Message");
|
|
79
|
+
const { data: users } = db.useQuery<UserRow>("User");
|
|
80
|
+
|
|
81
|
+
const [paletteOpen, setPaletteOpen] = useState(false);
|
|
82
|
+
const seeded = useRef(false);
|
|
83
|
+
|
|
84
|
+
const ticketList = tickets ?? [];
|
|
85
|
+
const customerList = customers ?? [];
|
|
86
|
+
const agentList = users ?? [];
|
|
87
|
+
|
|
88
|
+
// A brand-new helpdesk gets a realistic queue once, so the inbox never opens
|
|
89
|
+
// empty. `seedWorkspace` no-ops when anything exists; the ref stops a
|
|
90
|
+
// re-render from firing a second call mid-flight.
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (loading || seeded.current) return;
|
|
93
|
+
if (ticketList.length > 0) {
|
|
94
|
+
seeded.current = true;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
seeded.current = true;
|
|
98
|
+
callFn("seedWorkspace", {}).catch(() => {
|
|
99
|
+
// A failed seed leaves an empty inbox, which is usable — the queue has an
|
|
100
|
+
// empty state that offers to open the first ticket.
|
|
101
|
+
seeded.current = false;
|
|
102
|
+
});
|
|
103
|
+
}, [loading, ticketList.length]);
|
|
104
|
+
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
function onKey(event: KeyboardEvent) {
|
|
107
|
+
const target = event.target as HTMLElement | null;
|
|
108
|
+
const typing =
|
|
109
|
+
!!target &&
|
|
110
|
+
(target.tagName === "INPUT" ||
|
|
111
|
+
target.tagName === "TEXTAREA" ||
|
|
112
|
+
target.isContentEditable);
|
|
113
|
+
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
|
|
114
|
+
event.preventDefault();
|
|
115
|
+
setPaletteOpen((open) => !open);
|
|
116
|
+
} else if (event.key === "/" && !typing) {
|
|
117
|
+
event.preventDefault();
|
|
118
|
+
setPaletteOpen(true);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
window.addEventListener("keydown", onKey);
|
|
122
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
123
|
+
}, []);
|
|
124
|
+
|
|
125
|
+
const lookups = useMemo(() => {
|
|
126
|
+
const customerById = new Map(customerList.map((c) => [c.id, c] as const));
|
|
127
|
+
const userById = new Map(agentList.map((u) => [u.id, u] as const));
|
|
128
|
+
return { customerById, userById };
|
|
129
|
+
}, [customerList, agentList]);
|
|
130
|
+
|
|
131
|
+
const searchIndex = useMemo<SearchItem[]>(
|
|
132
|
+
() => [
|
|
133
|
+
...ticketList.map((ticket) => ({
|
|
134
|
+
id: `ticket:${ticket.id}`,
|
|
135
|
+
type: "deal" as const,
|
|
136
|
+
title: ticket.subject,
|
|
137
|
+
subtitle: lookups.customerById.get(ticket.customerId ?? "")?.name,
|
|
138
|
+
href: `/tickets/${ticket.id}`,
|
|
139
|
+
})),
|
|
140
|
+
...customerList.map((customer) => ({
|
|
141
|
+
id: `customer:${customer.id}`,
|
|
142
|
+
type: "contact" as const,
|
|
143
|
+
title: customer.name,
|
|
144
|
+
subtitle: customer.company ?? undefined,
|
|
145
|
+
href: "/customers",
|
|
146
|
+
keywords: customer.email,
|
|
147
|
+
})),
|
|
148
|
+
],
|
|
149
|
+
[ticketList, customerList, lookups],
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const queue = counts(ticketList);
|
|
153
|
+
|
|
154
|
+
const data: WorkspaceData = {
|
|
155
|
+
customers: customerList,
|
|
156
|
+
tickets: ticketList,
|
|
157
|
+
messages: messages ?? [],
|
|
158
|
+
agents: agentList,
|
|
159
|
+
customerName: (id) => lookups.customerById.get(id ?? "")?.name ?? null,
|
|
160
|
+
agentName: (id) => {
|
|
161
|
+
const user = lookups.userById.get(id ?? "");
|
|
162
|
+
return user?.displayName || user?.email || null;
|
|
163
|
+
},
|
|
164
|
+
loading,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<div className="flex h-screen overflow-hidden">
|
|
169
|
+
<Sidebar
|
|
170
|
+
workspace="Helpdesk"
|
|
171
|
+
email={email}
|
|
172
|
+
pathname={pathname}
|
|
173
|
+
counts={{
|
|
174
|
+
"/": ticketList.filter(isOpen).length,
|
|
175
|
+
"/customers": customerList.length,
|
|
176
|
+
}}
|
|
177
|
+
onOpenCommand={() => setPaletteOpen(true)}
|
|
178
|
+
onSignOut={async () => {
|
|
179
|
+
await signOut();
|
|
180
|
+
window.location.assign("/login");
|
|
181
|
+
}}
|
|
182
|
+
/>
|
|
183
|
+
<main className="flex min-w-0 flex-1 flex-col">{children(data)}</main>
|
|
184
|
+
|
|
185
|
+
<CommandPalette
|
|
186
|
+
open={paletteOpen}
|
|
187
|
+
items={searchIndex}
|
|
188
|
+
actions={[
|
|
189
|
+
{
|
|
190
|
+
id: "new-ticket",
|
|
191
|
+
label: "New ticket",
|
|
192
|
+
run: () => router.push("/?new=ticket"),
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
id: "unassigned",
|
|
196
|
+
label: `Unassigned (${queue.unassigned})`,
|
|
197
|
+
run: () => router.push("/?filter=unassigned"),
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
id: "go-customers",
|
|
201
|
+
label: "Go to Customers",
|
|
202
|
+
run: () => router.push("/customers"),
|
|
203
|
+
},
|
|
204
|
+
]}
|
|
205
|
+
onClose={() => setPaletteOpen(false)}
|
|
206
|
+
onSelect={(item) => router.push(item.href)}
|
|
207
|
+
/>
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import {
|
|
2
|
+
entity,
|
|
3
|
+
field,
|
|
4
|
+
policy,
|
|
5
|
+
auth,
|
|
6
|
+
buildManifest,
|
|
7
|
+
discoverAppRoutes,
|
|
8
|
+
font,
|
|
9
|
+
} from "@pylonsync/sdk";
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// helpdesk — a support inbox for a small team: customers write in, agents triage
|
|
13
|
+
// by priority and SLA, and every reply lands on a shared thread.
|
|
14
|
+
//
|
|
15
|
+
// The realtime hook: the queue subscribes to Ticket, so a new ticket appears at
|
|
16
|
+
// the top of everyone's inbox as it arrives, and an assignment shows up before
|
|
17
|
+
// two agents start the same reply.
|
|
18
|
+
//
|
|
19
|
+
// • Customer — the person who wrote in. Holds email, so never public.
|
|
20
|
+
// • Ticket — subject, status, priority, assignee. The queue.
|
|
21
|
+
// • Message — one entry on a thread; `internal` marks an agent-only note.
|
|
22
|
+
// • User — an agent (email/password).
|
|
23
|
+
//
|
|
24
|
+
// TRUST MODEL: an internal tool for ONE support team. Every entity is readable
|
|
25
|
+
// and writable by any SIGNED-IN user and by nobody else. There is no customer
|
|
26
|
+
// portal here — adding one means a separate, narrower policy scoped to the
|
|
27
|
+
// customer's own tickets, NOT loosening these.
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
const TEAM = "auth.userId != null";
|
|
31
|
+
|
|
32
|
+
const Customer = entity(
|
|
33
|
+
"Customer",
|
|
34
|
+
{
|
|
35
|
+
name: field.string(),
|
|
36
|
+
email: field.string(),
|
|
37
|
+
company: field.string().optional(),
|
|
38
|
+
createdAt: field.datetime().defaultNow(),
|
|
39
|
+
},
|
|
40
|
+
{ indexes: [{ name: "by_email", fields: ["email"], unique: false }] },
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const Ticket = entity(
|
|
44
|
+
"Ticket",
|
|
45
|
+
{
|
|
46
|
+
subject: field.string(),
|
|
47
|
+
// "open" | "pending" | "solved" | "closed" — see STATUSES in lib/tickets.ts.
|
|
48
|
+
status: field.string().default("open"),
|
|
49
|
+
// "urgent" | "high" | "normal" | "low" — sets the first-response window.
|
|
50
|
+
priority: field.string().default("normal"),
|
|
51
|
+
customerId: field.id("Customer").optional(),
|
|
52
|
+
assigneeId: field.id("User").optional(),
|
|
53
|
+
// When an agent first replied. Null means the customer is still waiting,
|
|
54
|
+
// which is what the SLA state is computed from.
|
|
55
|
+
firstRespondedAt: field.datetime().optional(),
|
|
56
|
+
createdAt: field.datetime().defaultNow(),
|
|
57
|
+
updatedAt: field.datetime().defaultNow(),
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
indexes: [
|
|
61
|
+
{ name: "by_status", fields: ["status"], unique: false },
|
|
62
|
+
{ name: "by_assignee", fields: ["assigneeId"], unique: false },
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const Message = entity(
|
|
68
|
+
"Message",
|
|
69
|
+
{
|
|
70
|
+
ticketId: field.id("Ticket"),
|
|
71
|
+
body: field.string(),
|
|
72
|
+
// True when the customer wrote it rather than an agent.
|
|
73
|
+
fromCustomer: field.boolean().default(false),
|
|
74
|
+
// An internal note stays with the team if you later add a customer portal.
|
|
75
|
+
internal: field.boolean().default(false),
|
|
76
|
+
authorId: field.id("User").readonly().optional(),
|
|
77
|
+
createdAt: field.datetime().defaultNow(),
|
|
78
|
+
},
|
|
79
|
+
{ indexes: [{ name: "by_ticket", fields: ["ticketId"], unique: false }] },
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const User = entity(
|
|
83
|
+
"User",
|
|
84
|
+
{
|
|
85
|
+
email: field.string(),
|
|
86
|
+
displayName: field.string().optional(),
|
|
87
|
+
passwordHash: field.string().serverOnly().optional(),
|
|
88
|
+
avatarColor: field.string().optional(),
|
|
89
|
+
emailVerified: field.datetime().optional(),
|
|
90
|
+
createdAt: field.datetime().defaultNow(),
|
|
91
|
+
},
|
|
92
|
+
{ indexes: [{ name: "by_email", fields: ["email"], unique: true }] },
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const teamPolicy = (name: string, entityName: string) =>
|
|
96
|
+
policy({
|
|
97
|
+
name,
|
|
98
|
+
entity: entityName,
|
|
99
|
+
allowRead: TEAM,
|
|
100
|
+
allowInsert: TEAM,
|
|
101
|
+
allowUpdate: TEAM,
|
|
102
|
+
allowDelete: TEAM,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const userPolicy = policy({
|
|
106
|
+
name: "user_team_read",
|
|
107
|
+
entity: "User",
|
|
108
|
+
allowRead: TEAM,
|
|
109
|
+
allowInsert: "false",
|
|
110
|
+
allowUpdate: "false",
|
|
111
|
+
allowDelete: "false",
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const manifest = buildManifest({
|
|
115
|
+
name: "__APP_NAME__",
|
|
116
|
+
version: "0.1.0",
|
|
117
|
+
entities: [Customer, Ticket, Message, User],
|
|
118
|
+
queries: [],
|
|
119
|
+
actions: [],
|
|
120
|
+
policies: [
|
|
121
|
+
teamPolicy("customer_team", "Customer"),
|
|
122
|
+
teamPolicy("ticket_team", "Ticket"),
|
|
123
|
+
teamPolicy("message_team", "Message"),
|
|
124
|
+
userPolicy,
|
|
125
|
+
],
|
|
126
|
+
auth: auth(),
|
|
127
|
+
fonts: [
|
|
128
|
+
font({
|
|
129
|
+
family: "Inter",
|
|
130
|
+
variable: "--font-sans",
|
|
131
|
+
weights: ["400", "500", "600", "700"],
|
|
132
|
+
subsets: ["latin"],
|
|
133
|
+
display: "swap",
|
|
134
|
+
preload: true,
|
|
135
|
+
}),
|
|
136
|
+
],
|
|
137
|
+
routes: await discoverAppRoutes(),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
console.log(JSON.stringify(manifest, null, 2));
|
|
141
|
+
|
|
142
|
+
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
|
+
}
|