@pylonsync/create-pylon 0.19.1 → 0.20.0
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 +30 -7
- package/package.json +1 -1
- package/templates/agency/.env.example +7 -0
- package/templates/agency/README.md +28 -3
- package/templates/agency/app/(marketing)/contact-form.tsx +20 -18
- package/templates/agency/app/(marketing)/page.tsx +3 -3
- package/templates/agency/app/(marketing)/work/page.tsx +3 -3
- package/templates/agency/app/auth-form.tsx +4 -4
- package/templates/agency/app/dashboard/dashboard-client.tsx +110 -64
- package/templates/agency/app/dashboard/page.tsx +14 -16
- package/templates/agency/app/icon.svg +3 -0
- package/templates/agency/app/login/page.tsx +12 -14
- package/templates/agency/components/marketing.tsx +12 -5
- package/templates/agency/functions/seedStudioBackoffice.ts +42 -11
- package/templates/agency/lib/agency.ts +22 -0
- package/templates/agency/lib/demo.ts +118 -0
- package/templates/agency/lib/site.config.ts +19 -13
- package/templates/agency/public/favicon.ico +0 -0
- package/templates/agency/tests/agency.test.ts +57 -0
- package/templates/ai-chat/.env.example +13 -27
- package/templates/ai-chat/README.md +53 -60
- package/templates/ai-chat/app/(chat)/c/[id]/page.tsx +19 -0
- package/templates/ai-chat/app/(chat)/chat-client.tsx +657 -622
- package/templates/ai-chat/app/(chat)/layout.tsx +6 -43
- package/templates/ai-chat/app/(chat)/not-found.tsx +6 -10
- package/templates/ai-chat/app/(chat)/page.tsx +7 -11
- package/templates/ai-chat/app/auth-form.tsx +45 -38
- package/templates/ai-chat/app/error.tsx +9 -12
- package/templates/ai-chat/app/globals.css +464 -102
- package/templates/ai-chat/app/layout.tsx +11 -19
- package/templates/ai-chat/app/login/page.tsx +39 -24
- package/templates/ai-chat/app.ts +118 -43
- package/templates/ai-chat/components/chat/composer.tsx +193 -0
- package/templates/ai-chat/components/chat/empty-state.tsx +65 -0
- package/templates/ai-chat/components/chat/icons.tsx +198 -0
- package/templates/ai-chat/components/chat/markdown.tsx +164 -0
- package/templates/ai-chat/components/chat/message.tsx +183 -0
- package/templates/ai-chat/components/chat/setup.tsx +71 -0
- package/templates/ai-chat/components/chat/sidebar.tsx +288 -0
- package/templates/ai-chat/functions/_beginTurn.ts +145 -0
- package/templates/ai-chat/functions/_finishTurn.ts +55 -0
- package/templates/ai-chat/functions/assistantStatus.ts +13 -0
- package/templates/ai-chat/functions/claimGuestHistory.ts +39 -0
- package/templates/ai-chat/functions/createConversation.ts +28 -0
- package/templates/ai-chat/functions/deleteConversation.ts +15 -20
- package/templates/ai-chat/functions/prepareGuestClaim.ts +32 -0
- package/templates/ai-chat/functions/renameConversation.ts +19 -0
- package/templates/ai-chat/functions/respond.ts +88 -0
- package/templates/ai-chat/functions/seedExamples.ts +46 -0
- package/templates/ai-chat/functions/stopResponse.ts +30 -0
- package/templates/ai-chat/lib/brand-icon.ts +7 -0
- package/templates/ai-chat/lib/chat.ts +277 -0
- package/templates/ai-chat/lib/claim.ts +15 -0
- package/templates/ai-chat/lib/examples.ts +179 -0
- package/templates/ai-chat/lib/highlight.ts +107 -0
- package/templates/ai-chat/lib/markdown.ts +317 -0
- package/templates/ai-chat/lib/provider.ts +38 -0
- package/templates/ai-chat/lib/site.config.ts +55 -39
- package/templates/ai-chat/tests/chat.test.ts +174 -0
- package/templates/ai-chat/tests/components.test.tsx +57 -0
- package/templates/ai-chat/tests/markdown.test.ts +88 -0
- package/templates/ai-chat/tests/provider.test.ts +32 -0
- package/templates/backend/consumer/apps/api/app/layout.tsx +33 -0
- package/templates/backend/consumer/apps/api/app/page.tsx +54 -0
- package/templates/backend/consumer/apps/api/app.ts +166 -0
- package/templates/backend/consumer/apps/api/functions/addComment.ts +24 -0
- package/templates/backend/consumer/apps/api/functions/createPost.ts +19 -36
- package/templates/backend/consumer/apps/api/functions/deletePost.ts +15 -13
- package/templates/backend/consumer/apps/api/functions/myProfile.ts +5 -8
- package/templates/backend/consumer/apps/api/functions/seedDemo.ts +95 -0
- package/templates/backend/consumer/apps/api/functions/setFollow.ts +36 -0
- package/templates/backend/consumer/apps/api/functions/setLike.ts +36 -0
- package/templates/backend/consumer/apps/api/functions/upsertProfile.ts +43 -37
- package/templates/backend/consumer/apps/api/lib/seed.ts +89 -0
- package/templates/backend/consumer/apps/api/lib/social.ts +50 -0
- package/templates/backend/consumer/apps/api/package.json +8 -2
- package/templates/backend/consumer/apps/api/public/images/posts/arch.jpg +0 -0
- package/templates/backend/consumer/apps/api/public/images/posts/coffee.jpg +0 -0
- package/templates/backend/consumer/apps/api/public/images/posts/dog.jpg +0 -0
- package/templates/backend/consumer/apps/api/public/images/posts/lisbon.jpg +0 -0
- package/templates/backend/consumer/apps/api/public/images/posts/ramen.jpg +0 -0
- package/templates/backend/consumer/apps/api/public/images/posts/run.jpg +0 -0
- package/templates/backend/consumer/apps/api/public/images/posts/sunset.jpg +0 -0
- package/templates/backend/consumer/apps/api/tsconfig.json +12 -2
- package/templates/chat/README.md +43 -21
- package/templates/chat/app/chat-client.tsx +495 -85
- package/templates/chat/app/error.tsx +1 -1
- package/templates/chat/app/globals.css +170 -54
- package/templates/chat/app/layout.tsx +12 -9
- package/templates/chat/app/not-found.tsx +4 -2
- package/templates/chat/app/page.tsx +9 -19
- package/templates/chat/app.ts +96 -17
- package/templates/chat/components/chat/avatar.tsx +45 -0
- package/templates/chat/components/chat/composer.tsx +110 -0
- package/templates/chat/components/chat/message-list.tsx +285 -0
- package/templates/chat/components/chat/sidebar.tsx +190 -0
- package/templates/chat/functions/joinChat.ts +83 -0
- package/templates/chat/functions/renameMember.ts +25 -0
- package/templates/chat/lib/chat.ts +188 -0
- package/templates/chat/lib/seed.ts +78 -0
- package/templates/chat/tests/chat.test.ts +159 -0
- package/templates/chat/tests/composer.test.tsx +54 -0
- package/templates/consumer/README.md +3 -3
- package/templates/consumer/app/globals.css +10 -3
- package/templates/consumer/app/icon.svg +1 -0
- package/templates/consumer/components/social/avatar.tsx +25 -11
- package/templates/consumer/functions/seedFeed.ts +3 -3
- package/templates/consumer/lib/seed.ts +43 -85
- package/templates/consumer/public/favicon.ico +0 -0
- package/templates/consumer/public/images/posts/arch.jpg +0 -0
- package/templates/consumer/public/images/posts/coffee.jpg +0 -0
- package/templates/consumer/public/images/posts/dog.jpg +0 -0
- package/templates/consumer/public/images/posts/lisbon.jpg +0 -0
- package/templates/consumer/public/images/posts/ramen.jpg +0 -0
- package/templates/consumer/public/images/posts/run.jpg +0 -0
- package/templates/consumer/public/images/posts/sunset.jpg +0 -0
- package/templates/consumer/tests/seed.test.ts +39 -0
- package/templates/creator/.env.example +7 -0
- package/templates/creator/README.md +24 -5
- package/templates/creator/app/(marketing)/newsletter-signup.tsx +33 -12
- package/templates/creator/app/auth-form.tsx +7 -7
- package/templates/creator/app/dashboard/dashboard-client.tsx +137 -100
- package/templates/creator/app/dashboard/page.tsx +18 -18
- package/templates/creator/app/icon.svg +2 -0
- package/templates/creator/app/login/page.tsx +14 -14
- package/templates/creator/app.ts +4 -4
- package/templates/creator/functions/seedDemo.ts +32 -0
- package/templates/creator/functions/subscribe.ts +4 -15
- package/templates/creator/lib/demo.ts +94 -0
- package/templates/creator/lib/site.config.ts +15 -10
- package/templates/creator/lib/stats.ts +35 -0
- package/templates/creator/public/favicon.ico +0 -0
- package/templates/creator/tests/counter.test.ts +92 -0
- package/templates/creator/tests/live-count.test.tsx +45 -0
- package/templates/crm/README.md +22 -1
- package/templates/crm/app/apple-icon.png +0 -0
- package/templates/crm/app/companies/companies-view.tsx +32 -14
- package/templates/crm/app/companies/page.tsx +2 -1
- package/templates/crm/app/contacts/contacts-view.tsx +34 -10
- package/templates/crm/app/contacts/page.tsx +2 -1
- package/templates/crm/app/deals/[id]/deal-view.tsx +137 -98
- package/templates/crm/app/deals/[id]/page.tsx +2 -1
- package/templates/crm/app/globals.css +29 -14
- package/templates/crm/app/icon.svg +1 -0
- package/templates/crm/app/layout.tsx +7 -5
- package/templates/crm/app/page.tsx +2 -1
- package/templates/crm/app/pipeline-view.tsx +5 -5
- package/templates/crm/app/workspace.tsx +29 -17
- package/templates/crm/app.ts +5 -5
- package/templates/crm/components/activity-timeline.tsx +14 -4
- package/templates/crm/components/app-shell.tsx +94 -0
- package/templates/crm/components/auth-form.tsx +35 -5
- package/templates/crm/components/avatar.tsx +5 -1
- package/templates/crm/components/board-skeleton.tsx +5 -5
- package/templates/crm/components/brand-mark.tsx +25 -0
- package/templates/crm/components/command-palette.tsx +17 -17
- package/templates/crm/components/data-table.tsx +97 -36
- package/templates/crm/components/deal-card.tsx +23 -27
- package/templates/crm/components/filter-tabs.tsx +101 -0
- package/templates/crm/components/metrics-bar.tsx +6 -6
- package/templates/crm/components/nav.tsx +15 -0
- package/templates/crm/components/page-header.tsx +43 -7
- package/templates/crm/components/pipeline-board.tsx +16 -10
- package/templates/crm/components/require-auth.tsx +24 -26
- package/templates/crm/components/sidebar.tsx +54 -49
- package/templates/crm/components/stage-badge.tsx +4 -2
- package/templates/crm/components/ui/button.tsx +5 -4
- package/templates/crm/components/ui/dialog.tsx +1 -1
- package/templates/crm/components/ui/input.tsx +1 -1
- package/templates/crm/components/ui/select.tsx +1 -1
- package/templates/crm/components/ui/textarea.tsx +1 -1
- package/templates/crm/functions/seedWorkspace.ts +32 -17
- package/templates/crm/lib/brand.ts +9 -0
- package/templates/crm/lib/demo-team.ts +51 -0
- package/templates/crm/lib/pipeline.ts +37 -0
- package/templates/crm/lib/search.ts +4 -3
- package/templates/crm/lib/seed.ts +53 -8
- package/templates/crm/public/favicon.ico +0 -0
- package/templates/crm/tests/components.test.tsx +5 -5
- package/templates/crm/tests/demo-team.test.ts +18 -0
- package/templates/crm/tests/pipeline.test.ts +59 -3
- package/templates/directory/.env.example +7 -0
- package/templates/directory/README.md +22 -3
- package/templates/directory/app/(marketing)/directory-browse.tsx +30 -12
- package/templates/directory/app/(marketing)/page.tsx +6 -4
- package/templates/directory/app/auth-form.tsx +4 -4
- package/templates/directory/app/dashboard/dashboard-client.tsx +177 -90
- package/templates/directory/app/dashboard/page.tsx +16 -14
- package/templates/directory/app/icon.svg +4 -0
- package/templates/directory/app/login/page.tsx +16 -15
- package/templates/directory/functions/seedDemo.ts +43 -0
- package/templates/directory/lib/demo.ts +145 -0
- package/templates/directory/lib/directory.ts +27 -0
- package/templates/directory/lib/site.config.ts +21 -16
- package/templates/directory/public/favicon.ico +0 -0
- package/templates/directory/tests/directory.test.ts +46 -0
- package/templates/expo/consumer/apps/expo/App.tsx +360 -288
- package/templates/expo/consumer/apps/expo/app.json +9 -1
- package/templates/expo/consumer/apps/expo/package.json +2 -1
- package/templates/helpdesk/README.md +21 -0
- package/templates/helpdesk/app/apple-icon.png +0 -0
- package/templates/helpdesk/app/customers/customers-view.tsx +23 -7
- package/templates/helpdesk/app/customers/page.tsx +2 -1
- package/templates/helpdesk/app/globals.css +29 -14
- package/templates/helpdesk/app/icon.svg +1 -0
- package/templates/helpdesk/app/inbox-view.tsx +66 -47
- package/templates/helpdesk/app/layout.tsx +7 -5
- package/templates/helpdesk/app/page.tsx +4 -7
- package/templates/helpdesk/app/tickets/[id]/page.tsx +2 -1
- package/templates/helpdesk/app/tickets/[id]/ticket-view.tsx +134 -97
- package/templates/helpdesk/app/workspace.tsx +33 -22
- package/templates/helpdesk/app.ts +2 -2
- package/templates/helpdesk/components/app-shell.tsx +94 -0
- package/templates/helpdesk/components/auth-form.tsx +35 -5
- package/templates/helpdesk/components/avatar.tsx +5 -1
- package/templates/helpdesk/components/brand-mark.tsx +28 -0
- package/templates/helpdesk/components/command-palette.tsx +17 -17
- package/templates/helpdesk/components/data-table.tsx +97 -36
- package/templates/helpdesk/components/filter-tabs.tsx +101 -0
- package/templates/helpdesk/components/nav.tsx +14 -0
- package/templates/helpdesk/components/page-header.tsx +43 -7
- package/templates/helpdesk/components/priority-badge.tsx +49 -6
- package/templates/helpdesk/components/require-auth.tsx +24 -26
- package/templates/helpdesk/components/sidebar.tsx +54 -48
- package/templates/helpdesk/components/sla-indicator.tsx +5 -8
- package/templates/helpdesk/components/thread.tsx +7 -7
- package/templates/helpdesk/components/ticket-list.tsx +79 -19
- package/templates/helpdesk/components/ui/button.tsx +5 -4
- package/templates/helpdesk/components/ui/dialog.tsx +1 -1
- package/templates/helpdesk/components/ui/input.tsx +1 -1
- package/templates/helpdesk/components/ui/select.tsx +1 -1
- package/templates/helpdesk/components/ui/textarea.tsx +1 -1
- package/templates/helpdesk/functions/seedWorkspace.ts +25 -15
- package/templates/helpdesk/lib/brand.ts +9 -0
- package/templates/helpdesk/lib/demo-team.ts +51 -0
- package/templates/helpdesk/lib/search.ts +4 -3
- package/templates/helpdesk/lib/seed.ts +43 -2
- package/templates/helpdesk/lib/tickets.ts +68 -20
- package/templates/helpdesk/public/favicon.ico +0 -0
- package/templates/helpdesk/tests/components.test.tsx +10 -0
- package/templates/helpdesk/tests/demo-team.test.ts +18 -0
- package/templates/helpdesk/tests/tickets.test.ts +70 -0
- package/templates/imessage/.env.example +49 -0
- package/templates/imessage/AGENTS.md +153 -0
- package/templates/imessage/CLAUDE.md +7 -0
- package/templates/imessage/README.md +187 -0
- package/templates/imessage/app/console.tsx +382 -0
- package/templates/imessage/app/contacts/page.tsx +13 -0
- package/templates/imessage/app/error.tsx +16 -0
- package/templates/imessage/app/gate.tsx +31 -0
- package/templates/imessage/app/globals.css +183 -0
- package/templates/imessage/app/layout.tsx +17 -0
- package/templates/imessage/app/login/login-form.tsx +115 -0
- package/templates/imessage/app/login/page.tsx +30 -0
- package/templates/imessage/app/not-found.tsx +13 -0
- package/templates/imessage/app/page.tsx +14 -0
- package/templates/imessage/app/robots.ts +6 -0
- package/templates/imessage/app/setup/page.tsx +13 -0
- package/templates/imessage/app/sign-out.tsx +20 -0
- package/templates/imessage/app.ts +299 -0
- package/templates/imessage/bunfig +4 -0
- package/templates/imessage/components/avatar.tsx +46 -0
- package/templates/imessage/components/contact-panel.tsx +263 -0
- package/templates/imessage/components/contacts-view.tsx +172 -0
- package/templates/imessage/components/conversation-list.tsx +129 -0
- package/templates/imessage/components/nav-rail.tsx +67 -0
- package/templates/imessage/components/setup-view.tsx +408 -0
- package/templates/imessage/components/thread.tsx +229 -0
- package/templates/imessage/components/ui/badge.tsx +37 -0
- package/templates/imessage/components/ui/button.tsx +56 -0
- package/templates/imessage/components/ui/card.tsx +90 -0
- package/templates/imessage/components/ui/dialog.tsx +110 -0
- package/templates/imessage/components/ui/input.tsx +30 -0
- package/templates/imessage/components/ui/label.tsx +21 -0
- package/templates/imessage/components/ui/select.tsx +39 -0
- package/templates/imessage/components/ui/table.tsx +124 -0
- package/templates/imessage/components/ui/textarea.tsx +24 -0
- package/templates/imessage/components.json +20 -0
- package/templates/imessage/functions/answerNow.ts +34 -0
- package/templates/imessage/functions/assistant.ts +96 -0
- package/templates/imessage/functions/beginTurn.ts +101 -0
- package/templates/imessage/functions/cancelReminder.ts +13 -0
- package/templates/imessage/functions/claimOutbound.ts +44 -0
- package/templates/imessage/functions/clearDemo.ts +18 -0
- package/templates/imessage/functions/deleteNote.ts +11 -0
- package/templates/imessage/functions/deliverOutbound.ts +55 -0
- package/templates/imessage/functions/finishTurn.ts +84 -0
- package/templates/imessage/functions/fireReminder.ts +42 -0
- package/templates/imessage/functions/ingestInbound.ts +22 -0
- package/templates/imessage/functions/ownerAccount.ts +13 -0
- package/templates/imessage/functions/processTurn.ts +74 -0
- package/templates/imessage/functions/relayApply.ts +102 -0
- package/templates/imessage/functions/relaySync.ts +63 -0
- package/templates/imessage/functions/releaseOutbound.ts +18 -0
- package/templates/imessage/functions/resolveTurn.ts +25 -0
- package/templates/imessage/functions/saveContact.ts +27 -0
- package/templates/imessage/functions/seedDemo.ts +70 -0
- package/templates/imessage/functions/sendOwnerMessage.ts +28 -0
- package/templates/imessage/functions/sendblueWebhook.ts +49 -0
- package/templates/imessage/functions/setContactStatus.ts +17 -0
- package/templates/imessage/functions/settleOutbound.ts +35 -0
- package/templates/imessage/functions/setupStatus.ts +33 -0
- package/templates/imessage/functions/sweep.ts +58 -0
- package/templates/imessage/functions/toolCancelReminder.ts +17 -0
- package/templates/imessage/functions/toolForgetNote.ts +15 -0
- package/templates/imessage/functions/toolListNotes.ts +16 -0
- package/templates/imessage/functions/toolListReminders.ts +17 -0
- package/templates/imessage/functions/toolSaveNote.ts +22 -0
- package/templates/imessage/functions/toolSetReminder.ts +55 -0
- package/templates/imessage/functions/updateSettings.ts +48 -0
- package/templates/imessage/gitignore +11 -0
- package/templates/imessage/lib/agent-call.ts +251 -0
- package/templates/imessage/lib/assistant.ts +74 -0
- package/templates/imessage/lib/chunk.ts +135 -0
- package/templates/imessage/lib/demo.ts +68 -0
- package/templates/imessage/lib/format.ts +170 -0
- package/templates/imessage/lib/gate.ts +104 -0
- package/templates/imessage/lib/handles.ts +67 -0
- package/templates/imessage/lib/provider.ts +38 -0
- package/templates/imessage/lib/relay-protocol.ts +95 -0
- package/templates/imessage/lib/reminders.ts +41 -0
- package/templates/imessage/lib/secrets.ts +59 -0
- package/templates/imessage/lib/sendblue.ts +173 -0
- package/templates/imessage/lib/store.ts +407 -0
- package/templates/imessage/lib/transport.ts +135 -0
- package/templates/imessage/lib/utils.ts +10 -0
- package/templates/imessage/package.json +40 -0
- package/templates/imessage/relay/.env.example +14 -0
- package/templates/imessage/relay/applescript.ts +70 -0
- package/templates/imessage/relay/chatdb.ts +165 -0
- package/templates/imessage/relay/client.ts +41 -0
- package/templates/imessage/relay/main.ts +137 -0
- package/templates/imessage/relay/state.ts +35 -0
- package/templates/imessage/tests/agent-call.test.ts +112 -0
- package/templates/imessage/tests/applescript.test.ts +88 -0
- package/templates/imessage/tests/chatdb.test.ts +139 -0
- package/templates/imessage/tests/chunk.test.ts +61 -0
- package/templates/imessage/tests/fake-ctx.ts +88 -0
- package/templates/imessage/tests/ingest.test.ts +141 -0
- package/templates/imessage/tests/lib.test.ts +99 -0
- package/templates/imessage/tests/review-fixes.test.ts +138 -0
- package/templates/imessage/tests/secrets.test.ts +36 -0
- package/templates/imessage/tests/sendblue.test.ts +143 -0
- package/templates/imessage/tests/setup.ts +5 -0
- package/templates/imessage/tsconfig.json +18 -0
- package/templates/inventory/README.md +21 -0
- package/templates/inventory/app/apple-icon.png +0 -0
- package/templates/inventory/app/globals.css +29 -14
- package/templates/inventory/app/icon.svg +1 -0
- package/templates/inventory/app/layout.tsx +7 -5
- package/templates/inventory/app/movements/movements-view.tsx +32 -10
- package/templates/inventory/app/movements/page.tsx +2 -1
- package/templates/inventory/app/page.tsx +4 -6
- package/templates/inventory/app/products/[id]/page.tsx +2 -1
- package/templates/inventory/app/products/[id]/product-view.tsx +55 -25
- package/templates/inventory/app/products-view.tsx +74 -33
- package/templates/inventory/app/workspace.tsx +34 -16
- package/templates/inventory/app.ts +10 -2
- package/templates/inventory/components/app-shell.tsx +94 -0
- package/templates/inventory/components/auth-form.tsx +35 -5
- package/templates/inventory/components/avatar.tsx +5 -1
- package/templates/inventory/components/brand-mark.tsx +25 -0
- package/templates/inventory/components/category-tile.tsx +49 -0
- package/templates/inventory/components/command-palette.tsx +17 -17
- package/templates/inventory/components/data-table.tsx +97 -36
- package/templates/inventory/components/filter-tabs.tsx +101 -0
- package/templates/inventory/components/nav.tsx +14 -0
- package/templates/inventory/components/page-header.tsx +43 -7
- package/templates/inventory/components/require-auth.tsx +24 -26
- package/templates/inventory/components/sidebar.tsx +54 -48
- package/templates/inventory/components/stock-level.tsx +30 -14
- package/templates/inventory/components/summary-bar.tsx +8 -15
- package/templates/inventory/components/ui/button.tsx +5 -4
- package/templates/inventory/components/ui/dialog.tsx +1 -1
- package/templates/inventory/components/ui/input.tsx +1 -1
- package/templates/inventory/components/ui/select.tsx +1 -1
- package/templates/inventory/components/ui/textarea.tsx +1 -1
- package/templates/inventory/functions/seedWorkspace.ts +18 -8
- package/templates/inventory/lib/brand.ts +9 -0
- package/templates/inventory/lib/demo-team.ts +51 -0
- package/templates/inventory/lib/search.ts +4 -3
- package/templates/inventory/lib/seed.ts +29 -2
- package/templates/inventory/lib/stock.ts +32 -0
- package/templates/inventory/public/favicon.ico +0 -0
- package/templates/inventory/tests/demo-team.test.ts +18 -0
- package/templates/inventory/tests/stock.test.ts +29 -0
- package/templates/invoices/README.md +16 -0
- package/templates/invoices/app/apple-icon.png +0 -0
- package/templates/invoices/app/clients/clients-view.tsx +18 -6
- package/templates/invoices/app/clients/page.tsx +2 -1
- package/templates/invoices/app/globals.css +29 -14
- package/templates/invoices/app/icon.svg +1 -0
- package/templates/invoices/app/invoices/[id]/invoice-view.tsx +72 -48
- package/templates/invoices/app/invoices/[id]/page.tsx +2 -1
- package/templates/invoices/app/invoices-view.tsx +81 -32
- package/templates/invoices/app/layout.tsx +7 -5
- package/templates/invoices/app/page.tsx +4 -6
- package/templates/invoices/app/workspace.tsx +26 -16
- package/templates/invoices/app.ts +2 -2
- package/templates/invoices/components/app-shell.tsx +94 -0
- package/templates/invoices/components/auth-form.tsx +35 -5
- package/templates/invoices/components/avatar.tsx +5 -1
- package/templates/invoices/components/brand-mark.tsx +26 -0
- package/templates/invoices/components/command-palette.tsx +17 -17
- package/templates/invoices/components/data-table.tsx +97 -36
- package/templates/invoices/components/filter-tabs.tsx +101 -0
- package/templates/invoices/components/line-items.tsx +19 -14
- package/templates/invoices/components/nav.tsx +14 -0
- package/templates/invoices/components/page-header.tsx +43 -7
- package/templates/invoices/components/require-auth.tsx +24 -26
- package/templates/invoices/components/sidebar.tsx +54 -48
- package/templates/invoices/components/status-badge.tsx +7 -7
- package/templates/invoices/components/summary-bar.tsx +5 -5
- package/templates/invoices/components/ui/button.tsx +5 -4
- package/templates/invoices/components/ui/dialog.tsx +1 -1
- package/templates/invoices/components/ui/input.tsx +1 -1
- package/templates/invoices/components/ui/select.tsx +1 -1
- package/templates/invoices/components/ui/textarea.tsx +1 -1
- package/templates/invoices/functions/recordPayment.ts +3 -0
- package/templates/invoices/lib/billing.ts +40 -0
- package/templates/invoices/lib/brand.ts +9 -0
- package/templates/invoices/lib/search.ts +4 -3
- package/templates/invoices/public/favicon.ico +0 -0
- package/templates/invoices/tests/billing.test.ts +27 -0
- package/templates/ios/consumer/apps/ios/Package.swift +4 -3
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ActivityView.swift +107 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/AppModel.swift +171 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/AuthView.swift +182 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/CommentsView.swift +136 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ComposeView.swift +152 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ExploreView.swift +117 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/FeedView.swift +175 -159
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/MainTabView.swift +54 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Media.swift +271 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +101 -4
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/PostCard.swift +245 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileEditorView.swift +208 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileView.swift +183 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/RootView.swift +25 -31
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/SocialStore.swift +285 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/StoryViewer.swift +155 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Theme.swift +82 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +4 -47
- package/templates/ios/consumer/apps/ios/project.yml +13 -9
- package/templates/local-service/.env.example +7 -0
- package/templates/local-service/README.md +26 -2
- package/templates/local-service/app/(marketing)/booking-widget.tsx +48 -9
- package/templates/local-service/app/(marketing)/page.tsx +66 -42
- package/templates/local-service/app/auth-form.tsx +8 -8
- package/templates/local-service/app/dashboard/dashboard-client.tsx +119 -92
- package/templates/local-service/app/dashboard/page.tsx +14 -14
- package/templates/local-service/app/icon.svg +2 -0
- package/templates/local-service/app/login/page.tsx +15 -15
- package/templates/local-service/functions/seedDemo.ts +49 -0
- package/templates/local-service/lib/demo.ts +135 -0
- package/templates/local-service/lib/hours.ts +44 -0
- package/templates/local-service/lib/site.config.ts +8 -3
- package/templates/local-service/lib/slots.ts +3 -0
- package/templates/local-service/public/favicon.ico +0 -0
- package/templates/local-service/tests/booking.test.ts +104 -0
- package/templates/marketplace/README.md +23 -15
- package/templates/marketplace/app/globals.css +172 -114
- package/templates/marketplace/app/layout.tsx +75 -30
- package/templates/marketplace/app/listing/[id]/page.tsx +210 -123
- package/templates/marketplace/app/me/page.tsx +4 -4
- package/templates/marketplace/app/page.tsx +221 -298
- package/templates/marketplace/app/sell/page.tsx +8 -11
- package/templates/marketplace/app.ts +32 -7
- package/templates/marketplace/client/ActivityFeed.tsx +311 -0
- package/templates/marketplace/client/AuthNav.tsx +24 -16
- package/templates/marketplace/client/ListingCard.tsx +118 -0
- package/templates/marketplace/client/LiveGrid.tsx +92 -0
- package/templates/marketplace/client/LiveListingStatus.tsx +32 -24
- package/templates/marketplace/client/LoginCard.tsx +114 -136
- package/templates/marketplace/client/MarketProvider.tsx +3 -5
- package/templates/marketplace/client/MyMarket.tsx +371 -185
- package/templates/marketplace/client/OfferPanel.tsx +421 -287
- package/templates/marketplace/client/SellForm.tsx +271 -192
- package/templates/marketplace/client/ThemeToggle.tsx +1 -0
- package/templates/marketplace/client/WatchButton.tsx +16 -16
- package/templates/marketplace/client/market.ts +61 -14
- package/templates/marketplace/functions/seedMarket.ts +143 -36
- package/templates/marketplace/functions/seedOffers.ts +101 -0
- package/templates/marketplace/lib/catalog.ts +72 -1
- package/templates/marketplace/public/images/listings/ceramic-lamp-640.webp +0 -0
- package/templates/marketplace/public/images/listings/ceramic-lamp.webp +0 -0
- package/templates/marketplace/public/images/listings/duffel-640.webp +0 -0
- package/templates/marketplace/public/images/listings/duffel.webp +0 -0
- package/templates/marketplace/public/images/listings/film-camera-640.webp +0 -0
- package/templates/marketplace/public/images/listings/film-camera.webp +0 -0
- package/templates/marketplace/public/images/listings/lounge-chair-640.webp +0 -0
- package/templates/marketplace/public/images/listings/lounge-chair.webp +0 -0
- package/templates/marketplace/public/images/listings/road-bike-640.webp +0 -0
- package/templates/marketplace/public/images/listings/road-bike.webp +0 -0
- package/templates/marketplace/public/images/listings/slipper-chair-640.webp +0 -0
- package/templates/marketplace/public/images/listings/slipper-chair.webp +0 -0
- package/templates/marketplace/public/images/listings/turntable-640.webp +0 -0
- package/templates/marketplace/public/images/listings/turntable.webp +0 -0
- package/templates/marketplace/public/images/listings/wardrobe-640.webp +0 -0
- package/templates/marketplace/public/images/listings/wardrobe.webp +0 -0
- package/templates/marketplace/public/images/listings/watch-640.webp +0 -0
- package/templates/marketplace/public/images/listings/watch.webp +0 -0
- package/templates/marketplace/public/images/listings/weekender-bag-640.webp +0 -0
- package/templates/marketplace/public/images/listings/weekender-bag.webp +0 -0
- package/templates/marketplace/tests/example.test.ts +64 -2
- package/templates/marketplace/tests/listing-card.test.tsx +61 -0
- package/templates/projects/README.md +24 -3
- package/templates/projects/app/apple-icon.png +0 -0
- package/templates/projects/app/clients/clients-view.tsx +20 -6
- package/templates/projects/app/clients/page.tsx +2 -1
- package/templates/projects/app/globals.css +29 -14
- package/templates/projects/app/icon.svg +1 -0
- package/templates/projects/app/layout.tsx +7 -5
- package/templates/projects/app/page.tsx +3 -5
- package/templates/projects/app/projects/[id]/page.tsx +2 -1
- package/templates/projects/app/projects/[id]/project-view.tsx +45 -30
- package/templates/projects/app/projects-view.tsx +105 -52
- package/templates/projects/app/workspace.tsx +41 -20
- package/templates/projects/app.ts +2 -2
- package/templates/projects/components/app-shell.tsx +94 -0
- package/templates/projects/components/auth-form.tsx +35 -5
- package/templates/projects/components/avatar.tsx +5 -1
- package/templates/projects/components/board-skeleton.tsx +3 -3
- package/templates/projects/components/brand-mark.tsx +25 -0
- package/templates/projects/components/command-palette.tsx +17 -17
- package/templates/projects/components/data-table.tsx +97 -36
- package/templates/projects/components/filter-tabs.tsx +101 -0
- package/templates/projects/components/meter.tsx +48 -0
- package/templates/projects/components/nav.tsx +14 -0
- package/templates/projects/components/page-header.tsx +43 -7
- package/templates/projects/components/require-auth.tsx +24 -26
- package/templates/projects/components/sidebar.tsx +54 -48
- package/templates/projects/components/summary-bar.tsx +49 -0
- package/templates/projects/components/task-board.tsx +10 -9
- package/templates/projects/components/time-dialog.tsx +29 -5
- package/templates/projects/components/ui/button.tsx +5 -4
- package/templates/projects/components/ui/dialog.tsx +1 -1
- package/templates/projects/components/ui/input.tsx +1 -1
- package/templates/projects/components/ui/select.tsx +1 -1
- package/templates/projects/components/ui/textarea.tsx +1 -1
- package/templates/projects/functions/seedWorkspace.ts +26 -12
- package/templates/projects/lib/brand.ts +9 -0
- package/templates/projects/lib/demo-team.ts +51 -0
- package/templates/projects/lib/search.ts +4 -3
- package/templates/projects/lib/seed.ts +35 -1
- package/templates/projects/lib/work.ts +63 -0
- package/templates/projects/public/favicon.ico +0 -0
- package/templates/projects/tests/demo-team.test.ts +18 -0
- package/templates/projects/tests/work.test.ts +65 -0
- package/templates/restaurant/.env.example +7 -0
- package/templates/restaurant/README.md +29 -3
- package/templates/restaurant/app/(marketing)/layout.tsx +4 -2
- package/templates/restaurant/app/(marketing)/page.tsx +47 -23
- package/templates/restaurant/app/(marketing)/reservation-widget.tsx +37 -7
- package/templates/restaurant/app/auth-form.tsx +8 -8
- package/templates/restaurant/app/dashboard/dashboard-client.tsx +241 -109
- package/templates/restaurant/app/dashboard/page.tsx +14 -14
- package/templates/restaurant/app/icon.svg +4 -0
- package/templates/restaurant/app/login/page.tsx +15 -15
- package/templates/restaurant/functions/seedDemo.ts +44 -0
- package/templates/restaurant/lib/demo.ts +144 -0
- package/templates/restaurant/lib/hours.ts +45 -0
- package/templates/restaurant/lib/site.config.ts +3 -3
- package/templates/restaurant/public/favicon.ico +0 -0
- package/templates/restaurant/tests/reservations.test.ts +67 -0
- package/templates/saas/README.md +54 -35
- package/templates/saas/app/(auth)/auth-form.tsx +25 -23
- package/templates/saas/app/(auth)/forgot-password/page.tsx +1 -1
- package/templates/saas/app/(auth)/layout.tsx +27 -33
- package/templates/saas/app/(auth)/login/page.tsx +1 -1
- package/templates/saas/app/(auth)/reset-password/page.tsx +1 -1
- package/templates/saas/app/(auth)/signup/page.tsx +1 -1
- package/templates/saas/app/(marketing)/layout.tsx +127 -253
- package/templates/saas/app/(marketing)/page.tsx +133 -372
- package/templates/saas/app/(marketing)/pricing/page.tsx +21 -19
- package/templates/saas/app/(marketing)/products/[slug]/page.tsx +40 -95
- package/templates/saas/app/dashboard/board-client.tsx +416 -0
- package/templates/saas/app/dashboard/dashboard-client.tsx +30 -615
- package/templates/saas/app/dashboard/layout.tsx +27 -13
- package/templates/saas/app/dashboard/overview-client.tsx +332 -0
- package/templates/saas/app/dashboard/page.tsx +29 -35
- package/templates/saas/app/dashboard/projects/[id]/page.tsx +29 -0
- package/templates/saas/app/dashboard/projects/page.tsx +11 -7
- package/templates/saas/app/dashboard/projects-client.tsx +398 -0
- package/templates/saas/app/globals.css +5 -4
- package/templates/saas/app/icon.svg +1 -0
- package/templates/saas/app/onboarding/onboarding-client.tsx +93 -26
- package/templates/saas/app/onboarding/page.tsx +6 -2
- package/templates/saas/app.ts +68 -13
- package/templates/saas/components/dashboard-shell.tsx +212 -132
- package/templates/saas/components/marketing.tsx +137 -274
- package/templates/saas/components/pricing-table.tsx +12 -11
- package/templates/saas/components/setup-checklist.tsx +31 -29
- package/templates/saas/components/task-ui.tsx +199 -0
- package/templates/saas/functions/createProject.ts +7 -13
- package/templates/saas/functions/createTask.ts +67 -0
- package/templates/saas/functions/deleteProject.ts +17 -0
- package/templates/saas/functions/deleteTask.ts +14 -0
- package/templates/saas/functions/seedWorkspace.ts +46 -0
- package/templates/saas/functions/setProjectStatus.ts +41 -0
- package/templates/saas/functions/updateProfile.ts +13 -0
- package/templates/saas/functions/updateProject.ts +4 -5
- package/templates/saas/functions/updateTask.ts +88 -0
- package/templates/saas/lib/members.ts +53 -0
- package/templates/saas/lib/names.ts +53 -0
- package/templates/saas/lib/plans.ts +11 -0
- package/templates/saas/lib/sample-workspace.ts +117 -0
- package/templates/saas/lib/site.config.ts +200 -463
- package/templates/saas/lib/tasks.ts +101 -0
- package/templates/saas/lib/use-live.ts +25 -0
- package/templates/saas/public/favicon.ico +0 -0
- package/templates/saas/public/screenshots/board.png +0 -0
- package/templates/saas/public/screenshots/members.png +0 -0
- package/templates/saas/public/screenshots/overview.png +0 -0
- package/templates/saas/public/screenshots/projects.png +0 -0
- package/templates/saas/scripts/capture-screenshots.mjs +156 -0
- package/templates/saas/tests/names.test.ts +35 -0
- package/templates/saas/tests/plans.test.ts +12 -0
- package/templates/saas/tests/sample-workspace.test.ts +46 -0
- package/templates/saas/tests/tasks.test.ts +81 -0
- package/templates/shop/README.md +25 -13
- package/templates/shop/app/(marketing)/layout.tsx +94 -67
- package/templates/shop/app/(marketing)/not-found.tsx +13 -12
- package/templates/shop/app/(marketing)/page.tsx +120 -59
- package/templates/shop/app/(marketing)/shop-client.tsx +364 -237
- package/templates/shop/app/(marketing)/success/page.tsx +75 -22
- package/templates/shop/app/globals.css +94 -1
- package/templates/shop/app/layout.tsx +4 -4
- package/templates/shop/app/login/page.tsx +4 -4
- package/templates/shop/app.ts +18 -6
- package/templates/shop/components/cart-button.tsx +33 -0
- package/templates/shop/components/marketing.tsx +17 -87
- package/templates/shop/components/shelf-count.tsx +36 -0
- package/templates/shop/functions/checkout.ts +17 -1
- package/templates/shop/lib/cart-store.ts +57 -0
- package/templates/shop/lib/shelf-store.ts +35 -0
- package/templates/shop/lib/shop.ts +27 -0
- package/templates/shop/lib/site.config.ts +108 -98
- package/templates/shop/public/images/hero.jpg +0 -0
- package/templates/shop/public/images/products/cedar-smoke.jpg +0 -0
- package/templates/shop/public/images/products/fig-leaf.jpg +0 -0
- package/templates/shop/public/images/products/linen.jpg +0 -0
- package/templates/shop/public/images/products/sea-salt.jpg +0 -0
- package/templates/shop/public/images/products/tobacco-oak.jpg +0 -0
- package/templates/shop/tests/shop.test.ts +64 -0
- package/templates/waitlist/.env.example +7 -0
- package/templates/waitlist/README.md +29 -13
- package/templates/waitlist/app/(marketing)/page.tsx +9 -7
- package/templates/waitlist/app/(marketing)/waitlist-hero.tsx +64 -31
- package/templates/waitlist/app/auth-form.tsx +8 -8
- package/templates/waitlist/app/dashboard/dashboard-client.tsx +132 -95
- package/templates/waitlist/app/dashboard/page.tsx +17 -16
- package/templates/waitlist/app/icon.svg +2 -0
- package/templates/waitlist/app/login/page.tsx +14 -14
- package/templates/waitlist/app.ts +6 -6
- package/templates/waitlist/functions/joinWaitlist.ts +6 -18
- package/templates/waitlist/functions/seedDemo.ts +32 -0
- package/templates/waitlist/lib/demo.ts +94 -0
- package/templates/waitlist/lib/launch.ts +19 -0
- package/templates/waitlist/lib/site.config.ts +14 -9
- package/templates/waitlist/lib/stats.ts +35 -0
- package/templates/waitlist/public/favicon.ico +0 -0
- package/templates/waitlist/tests/counter.test.ts +107 -0
- package/templates/waitlist/tests/live-count.test.tsx +48 -0
- package/templates/ai-chat/tests/example.test.ts +0 -12
- package/templates/backend/consumer/apps/api/functions/feed.ts +0 -57
- package/templates/backend/consumer/apps/api/functions/profilePosts.ts +0 -17
- package/templates/backend/consumer/apps/api/functions/toggleLike.ts +0 -48
- package/templates/backend/consumer/apps/api/schema.ts +0 -143
- package/templates/chat/functions/_keep.ts +0 -13
- package/templates/chat/tests/example.test.ts +0 -12
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +0 -68
- package/templates/marketplace/client/LiveTicker.tsx +0 -116
- package/templates/marketplace/client/ScrollToListingsLink.tsx +0 -29
- package/templates/marketplace/public/images/marketplace-hero.webp +0 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { ArrowUp } from "lucide-react";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
import { MAX_MESSAGE_LENGTH } from "@/lib/chat";
|
|
7
|
+
|
|
8
|
+
interface ComposerProps {
|
|
9
|
+
channelName: string;
|
|
10
|
+
/** Changes when the channel changes; the draft clears and the field refocuses. */
|
|
11
|
+
channelKey: string;
|
|
12
|
+
onSend: (text: string) => void;
|
|
13
|
+
/** Called on each keystroke that leaves text in the field. */
|
|
14
|
+
onTyping?: () => void;
|
|
15
|
+
/** Called when the field is emptied or the message is sent. */
|
|
16
|
+
onStopTyping?: () => void;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Message field. Enter sends, Shift+Enter adds a line, and the field grows
|
|
22
|
+
* up to about eight lines. Enter during IME composition (Japanese, Chinese,
|
|
23
|
+
* Korean input) confirms the composition instead of sending.
|
|
24
|
+
*/
|
|
25
|
+
export function Composer({
|
|
26
|
+
channelName,
|
|
27
|
+
channelKey,
|
|
28
|
+
onSend,
|
|
29
|
+
onTyping,
|
|
30
|
+
onStopTyping,
|
|
31
|
+
disabled,
|
|
32
|
+
}: ComposerProps) {
|
|
33
|
+
const [text, setText] = useState("");
|
|
34
|
+
const ref = useRef<HTMLTextAreaElement>(null);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
setText("");
|
|
38
|
+
// Focus on desktop only: on a phone, focusing opens the keyboard over the conversation.
|
|
39
|
+
if (window.matchMedia("(pointer: fine)").matches) ref.current?.focus();
|
|
40
|
+
}, [channelKey]);
|
|
41
|
+
|
|
42
|
+
// Grow with the content, capped by max-height in the class list.
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
const el = ref.current;
|
|
45
|
+
if (!el) return;
|
|
46
|
+
el.style.height = "auto";
|
|
47
|
+
el.style.height = `${el.scrollHeight}px`;
|
|
48
|
+
}, [text]);
|
|
49
|
+
|
|
50
|
+
const canSend = text.trim().length > 0 && !disabled;
|
|
51
|
+
|
|
52
|
+
function submit() {
|
|
53
|
+
const value = text.trim();
|
|
54
|
+
if (!value || disabled) return;
|
|
55
|
+
onSend(value);
|
|
56
|
+
setText("");
|
|
57
|
+
onStopTyping?.();
|
|
58
|
+
ref.current?.focus();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<form
|
|
63
|
+
onSubmit={(e) => {
|
|
64
|
+
e.preventDefault();
|
|
65
|
+
submit();
|
|
66
|
+
}}
|
|
67
|
+
className="composer group/composer relative flex items-end gap-2 rounded-2xl bg-card p-1.5 pl-4 shadow-composer transition-shadow duration-200 ease-out-soft focus-within:shadow-composer-focus"
|
|
68
|
+
>
|
|
69
|
+
<label htmlFor="composer" className="sr-only">
|
|
70
|
+
Message #{channelName}
|
|
71
|
+
</label>
|
|
72
|
+
<textarea
|
|
73
|
+
id="composer"
|
|
74
|
+
ref={ref}
|
|
75
|
+
rows={1}
|
|
76
|
+
value={text}
|
|
77
|
+
maxLength={MAX_MESSAGE_LENGTH}
|
|
78
|
+
placeholder={`Message #${channelName}`}
|
|
79
|
+
autoComplete="off"
|
|
80
|
+
enterKeyHint="send"
|
|
81
|
+
onChange={(e) => {
|
|
82
|
+
setText(e.target.value);
|
|
83
|
+
if (e.target.value.trim()) onTyping?.();
|
|
84
|
+
else onStopTyping?.();
|
|
85
|
+
}}
|
|
86
|
+
onKeyDown={(e) => {
|
|
87
|
+
if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
|
|
88
|
+
e.preventDefault();
|
|
89
|
+
submit();
|
|
90
|
+
}
|
|
91
|
+
}}
|
|
92
|
+
onBlur={() => onStopTyping?.()}
|
|
93
|
+
className="max-h-48 min-h-9 flex-1 resize-none bg-transparent py-[7px] text-base leading-[22px] sm:text-[15px] text-foreground outline-none placeholder:text-muted-foreground/80"
|
|
94
|
+
/>
|
|
95
|
+
<button
|
|
96
|
+
type="submit"
|
|
97
|
+
disabled={!canSend}
|
|
98
|
+
aria-label="Send message"
|
|
99
|
+
className={cn(
|
|
100
|
+
"inline-flex size-9 shrink-0 items-center justify-center rounded-xl transition-all duration-200 ease-out-soft",
|
|
101
|
+
canSend
|
|
102
|
+
? "bg-signal text-signal-foreground shadow-sm hover:brightness-105 active:scale-95"
|
|
103
|
+
: "bg-muted text-muted-foreground/60",
|
|
104
|
+
)}
|
|
105
|
+
>
|
|
106
|
+
<ArrowUp className="size-[18px]" strokeWidth={2} aria-hidden />
|
|
107
|
+
</button>
|
|
108
|
+
</form>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { ArrowDown, Trash2 } from "lucide-react";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
import {
|
|
7
|
+
dayLabel,
|
|
8
|
+
groupMessages,
|
|
9
|
+
timeLabel,
|
|
10
|
+
type ChatMember,
|
|
11
|
+
type ChatMessage,
|
|
12
|
+
} from "@/lib/chat";
|
|
13
|
+
import { Avatar } from "./avatar";
|
|
14
|
+
|
|
15
|
+
interface MessageListProps {
|
|
16
|
+
channelName: string;
|
|
17
|
+
channelTopic: string;
|
|
18
|
+
/** Sorted oldest first. */
|
|
19
|
+
messages: ChatMessage[];
|
|
20
|
+
members: Map<string, ChatMember>;
|
|
21
|
+
selfId: string;
|
|
22
|
+
onDelete: (id: string) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Distance from the bottom, in px, that still counts as "at the bottom". */
|
|
26
|
+
const STICK_THRESHOLD = 96;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The conversation for one channel. Render it with `key={channelId}` so a
|
|
30
|
+
* channel switch starts at the bottom with fresh scroll state. Keeps the newest message in view while the reader is at
|
|
31
|
+
* the bottom, and always after they send. When they have scrolled up to read
|
|
32
|
+
* history, new messages from others show a "New messages" button instead of
|
|
33
|
+
* yanking the scroll position.
|
|
34
|
+
*/
|
|
35
|
+
export function MessageList({
|
|
36
|
+
channelName,
|
|
37
|
+
channelTopic,
|
|
38
|
+
messages,
|
|
39
|
+
members,
|
|
40
|
+
selfId,
|
|
41
|
+
onDelete,
|
|
42
|
+
}: MessageListProps) {
|
|
43
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
44
|
+
const atBottomRef = useRef(true);
|
|
45
|
+
const lastCountRef = useRef(0);
|
|
46
|
+
const lastIdRef = useRef<string | null>(null);
|
|
47
|
+
// Rows already on screen when the list first filled. They render without
|
|
48
|
+
// the entrance animation; rows that arrive later get it.
|
|
49
|
+
const [knownIds] = useState(() => new Set(messages.map((m) => m.id)));
|
|
50
|
+
const [unseen, setUnseen] = useState(0);
|
|
51
|
+
|
|
52
|
+
const groups = useMemo(() => groupMessages(messages), [messages]);
|
|
53
|
+
// No entrance animation while the list is still empty: the first rows to
|
|
54
|
+
// arrive are history, not new messages.
|
|
55
|
+
const animateNew = lastCountRef.current > 0;
|
|
56
|
+
|
|
57
|
+
// Follow new rows when the reader is at the bottom or sent the row. The
|
|
58
|
+
// first fill (on mount, or when the replica loads) jumps without animating.
|
|
59
|
+
useLayoutEffect(() => {
|
|
60
|
+
const el = scrollRef.current;
|
|
61
|
+
const previous = lastCountRef.current;
|
|
62
|
+
const added = messages.length - previous;
|
|
63
|
+
lastCountRef.current = messages.length;
|
|
64
|
+
if (!el || added <= 0) return;
|
|
65
|
+
if (previous === 0) {
|
|
66
|
+
lastIdRef.current = messages[messages.length - 1]?.id ?? null;
|
|
67
|
+
for (const m of messages) knownIds.add(m.id);
|
|
68
|
+
el.scrollTop = el.scrollHeight;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const last = messages[messages.length - 1];
|
|
72
|
+
const previousLastId = lastIdRef.current;
|
|
73
|
+
lastIdRef.current = last?.id ?? null;
|
|
74
|
+
// Rows that sorted in above the newest one are history catching up, not
|
|
75
|
+
// new messages: keep the view pinned if it was, and count nothing.
|
|
76
|
+
if (last?.id === previousLastId) {
|
|
77
|
+
if (atBottomRef.current) el.scrollTop = el.scrollHeight;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
// An instant pin: a smooth scroll can emit a mid-animation scroll event
|
|
81
|
+
// that reads as "not at the bottom" when the next row lands.
|
|
82
|
+
if (atBottomRef.current || last?.authorId === selfId) {
|
|
83
|
+
el.scrollTop = el.scrollHeight;
|
|
84
|
+
atBottomRef.current = true;
|
|
85
|
+
setUnseen(0);
|
|
86
|
+
} else {
|
|
87
|
+
const fromOthers = messages.slice(-added).filter((m) => m.authorId !== selfId).length;
|
|
88
|
+
if (fromOthers > 0) setUnseen((n) => n + fromOthers);
|
|
89
|
+
}
|
|
90
|
+
}, [messages, selfId, knownIds]);
|
|
91
|
+
|
|
92
|
+
// Stay pinned to the newest message when the viewport shrinks (the phone
|
|
93
|
+
// keyboard opens) or the composer grows.
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
const el = scrollRef.current;
|
|
96
|
+
if (!el || typeof ResizeObserver === "undefined") return;
|
|
97
|
+
const observer = new ResizeObserver(() => {
|
|
98
|
+
if (atBottomRef.current) el.scrollTop = el.scrollHeight;
|
|
99
|
+
});
|
|
100
|
+
observer.observe(el);
|
|
101
|
+
return () => observer.disconnect();
|
|
102
|
+
}, []);
|
|
103
|
+
|
|
104
|
+
function onScroll() {
|
|
105
|
+
const el = scrollRef.current;
|
|
106
|
+
if (!el) return;
|
|
107
|
+
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < STICK_THRESHOLD;
|
|
108
|
+
atBottomRef.current = atBottom;
|
|
109
|
+
if (atBottom && unseen) setUnseen(0);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function jumpToLatest() {
|
|
113
|
+
const el = scrollRef.current;
|
|
114
|
+
el?.scrollTo({ top: el.scrollHeight, behavior: "smooth" });
|
|
115
|
+
setUnseen(0);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let previousDay: string | null = null;
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<div className="relative min-h-0 flex-1">
|
|
122
|
+
<div
|
|
123
|
+
ref={scrollRef}
|
|
124
|
+
onScroll={onScroll}
|
|
125
|
+
role="log"
|
|
126
|
+
aria-live="polite"
|
|
127
|
+
aria-label={`Messages in #${channelName}`}
|
|
128
|
+
className="scroll-area h-full overflow-y-auto overscroll-contain"
|
|
129
|
+
>
|
|
130
|
+
<div className="flex min-h-full max-w-4xl flex-col justify-end px-3 pt-10 pb-4 sm:px-5">
|
|
131
|
+
<ChannelIntro name={channelName} topic={channelTopic} empty={messages.length === 0} />
|
|
132
|
+
|
|
133
|
+
{groups.map((group) => {
|
|
134
|
+
const first = group.messages[0];
|
|
135
|
+
const showDay = group.day !== null && group.day !== previousDay;
|
|
136
|
+
if (group.day !== null) previousDay = group.day;
|
|
137
|
+
const author = members.get(group.authorId);
|
|
138
|
+
const name = author?.name ?? "Someone";
|
|
139
|
+
const hue = author?.hue ?? 250;
|
|
140
|
+
const isSelf = group.authorId === selfId;
|
|
141
|
+
return (
|
|
142
|
+
<React.Fragment key={group.key}>
|
|
143
|
+
{showDay && first.createdAt ? <DayDivider iso={first.createdAt} /> : null}
|
|
144
|
+
<section className="group/msggroup mt-3 first:mt-0" aria-label={`${name}`}>
|
|
145
|
+
{group.messages.map((m, i) => (
|
|
146
|
+
<MessageRow
|
|
147
|
+
key={m.id}
|
|
148
|
+
message={m}
|
|
149
|
+
head={i === 0}
|
|
150
|
+
name={name}
|
|
151
|
+
hue={hue}
|
|
152
|
+
isSelf={isSelf}
|
|
153
|
+
animate={animateNew && !knownIds.has(m.id)}
|
|
154
|
+
onDelete={onDelete}
|
|
155
|
+
/>
|
|
156
|
+
))}
|
|
157
|
+
</section>
|
|
158
|
+
</React.Fragment>
|
|
159
|
+
);
|
|
160
|
+
})}
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<button
|
|
165
|
+
type="button"
|
|
166
|
+
onClick={jumpToLatest}
|
|
167
|
+
aria-hidden={unseen === 0}
|
|
168
|
+
tabIndex={unseen === 0 ? -1 : 0}
|
|
169
|
+
className={cn(
|
|
170
|
+
"absolute bottom-3 left-1/2 inline-flex -translate-x-1/2 items-center gap-1.5 rounded-full bg-foreground px-3.5 py-1.5 text-[13px] font-medium text-background shadow-float transition-all duration-300 ease-out-soft",
|
|
171
|
+
unseen > 0 ? "translate-y-0 opacity-100" : "pointer-events-none translate-y-2 opacity-0",
|
|
172
|
+
)}
|
|
173
|
+
>
|
|
174
|
+
<ArrowDown className="size-3.5" strokeWidth={2} aria-hidden />
|
|
175
|
+
{unseen === 1 ? "1 new message" : `${unseen} new messages`}
|
|
176
|
+
</button>
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function ChannelIntro({ name, topic, empty }: { name: string; topic: string; empty: boolean }) {
|
|
182
|
+
return (
|
|
183
|
+
<div className="mb-8 px-2 sm:px-3">
|
|
184
|
+
<div className="mb-3 inline-flex size-11 items-center justify-center rounded-xl bg-muted text-xl font-medium text-muted-foreground">
|
|
185
|
+
#
|
|
186
|
+
</div>
|
|
187
|
+
<h2 className="text-[22px] font-semibold tracking-tight text-foreground">
|
|
188
|
+
This is the start of #{name}
|
|
189
|
+
</h2>
|
|
190
|
+
<p className="mt-1 text-[15px] text-muted-foreground">
|
|
191
|
+
{empty ? `${topic}. Send the first message.` : topic}
|
|
192
|
+
</p>
|
|
193
|
+
</div>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function DayDivider({ iso }: { iso: string }) {
|
|
198
|
+
return (
|
|
199
|
+
<div className="relative mt-6 mb-2 flex items-center justify-center" role="separator">
|
|
200
|
+
<span aria-hidden className="absolute inset-x-2 top-1/2 h-px bg-border sm:inset-x-3" />
|
|
201
|
+
<span className="relative rounded-full bg-background px-3 py-0.5 text-[12px] font-medium text-muted-foreground ring-1 ring-border">
|
|
202
|
+
{dayLabel(iso)}
|
|
203
|
+
</span>
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
interface MessageRowProps {
|
|
209
|
+
message: ChatMessage;
|
|
210
|
+
head: boolean;
|
|
211
|
+
name: string;
|
|
212
|
+
hue: number;
|
|
213
|
+
isSelf: boolean;
|
|
214
|
+
animate: boolean;
|
|
215
|
+
onDelete: (id: string) => void;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function MessageRow({ message, head, name, hue, isSelf, animate, onDelete }: MessageRowProps) {
|
|
219
|
+
const pending = !message.createdAt;
|
|
220
|
+
return (
|
|
221
|
+
<div
|
|
222
|
+
className={cn(
|
|
223
|
+
"group/msg relative flex gap-3 rounded-lg px-2 py-0.5 transition-colors duration-150 hover:bg-hover sm:px-3",
|
|
224
|
+
head ? "pt-1.5" : "",
|
|
225
|
+
animate && "animate-message-in",
|
|
226
|
+
)}
|
|
227
|
+
>
|
|
228
|
+
<div className="w-9 shrink-0">
|
|
229
|
+
{head ? (
|
|
230
|
+
<Avatar name={name} hue={hue} className="mt-0.5" />
|
|
231
|
+
) : message.createdAt ? (
|
|
232
|
+
<time
|
|
233
|
+
dateTime={message.createdAt}
|
|
234
|
+
className="block pt-[3px] text-right font-mono text-[10.5px] leading-[22px] text-muted-foreground opacity-0 transition-opacity duration-150 group-hover/msg:opacity-100"
|
|
235
|
+
>
|
|
236
|
+
{timeLabel(message.createdAt).replace(/\s?[AP]M$/i, "")}
|
|
237
|
+
</time>
|
|
238
|
+
) : null}
|
|
239
|
+
</div>
|
|
240
|
+
<div className="min-w-0 flex-1">
|
|
241
|
+
{head ? (
|
|
242
|
+
<div className="flex items-baseline gap-2">
|
|
243
|
+
<span className="truncate text-[15px] font-semibold text-foreground">{name}</span>
|
|
244
|
+
{isSelf ? (
|
|
245
|
+
<span className="shrink-0 text-[12px] text-muted-foreground">you</span>
|
|
246
|
+
) : null}
|
|
247
|
+
{message.createdAt ? (
|
|
248
|
+
<time
|
|
249
|
+
dateTime={message.createdAt}
|
|
250
|
+
title={new Date(message.createdAt).toLocaleString()}
|
|
251
|
+
className="shrink-0 font-mono text-[11px] text-muted-foreground"
|
|
252
|
+
>
|
|
253
|
+
{timeLabel(message.createdAt)}
|
|
254
|
+
</time>
|
|
255
|
+
) : null}
|
|
256
|
+
</div>
|
|
257
|
+
) : null}
|
|
258
|
+
<p
|
|
259
|
+
className={cn(
|
|
260
|
+
"text-[15px] leading-[22px] whitespace-pre-wrap text-foreground/90 [overflow-wrap:anywhere] transition-opacity duration-200",
|
|
261
|
+
pending && "opacity-55",
|
|
262
|
+
)}
|
|
263
|
+
>
|
|
264
|
+
{message.text}
|
|
265
|
+
{pending ? (
|
|
266
|
+
<span className="ml-2 align-middle font-mono text-[10.5px] text-muted-foreground">
|
|
267
|
+
Sending
|
|
268
|
+
</span>
|
|
269
|
+
) : null}
|
|
270
|
+
</p>
|
|
271
|
+
</div>
|
|
272
|
+
{isSelf && !pending ? (
|
|
273
|
+
<button
|
|
274
|
+
type="button"
|
|
275
|
+
onClick={() => onDelete(message.id)}
|
|
276
|
+
aria-label="Delete message"
|
|
277
|
+
title="Delete message"
|
|
278
|
+
className="delete-action absolute top-1 right-2 inline-flex size-7 items-center justify-center rounded-md bg-card text-muted-foreground shadow-sm ring-1 ring-border transition-opacity duration-150 hover:text-destructive"
|
|
279
|
+
>
|
|
280
|
+
<Trash2 className="size-3.5" strokeWidth={1.75} aria-hidden />
|
|
281
|
+
</button>
|
|
282
|
+
) : null}
|
|
283
|
+
</div>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState } from "react";
|
|
4
|
+
import { Check, Hash, Pencil, X } from "lucide-react";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
import { MAX_NAME_LENGTH, type ChatMember } from "@/lib/chat";
|
|
7
|
+
import { Avatar } from "./avatar";
|
|
8
|
+
|
|
9
|
+
export interface SidebarChannel {
|
|
10
|
+
id: string;
|
|
11
|
+
slug: string;
|
|
12
|
+
name: string;
|
|
13
|
+
unread: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface SidebarProps {
|
|
17
|
+
workspace: string;
|
|
18
|
+
channels: SidebarChannel[];
|
|
19
|
+
activeSlug: string;
|
|
20
|
+
onSelect: (slug: string) => void;
|
|
21
|
+
online: ChatMember[];
|
|
22
|
+
self: ChatMember | undefined;
|
|
23
|
+
onRename: (name: string) => Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function Sidebar({
|
|
27
|
+
workspace,
|
|
28
|
+
channels,
|
|
29
|
+
activeSlug,
|
|
30
|
+
onSelect,
|
|
31
|
+
online,
|
|
32
|
+
self,
|
|
33
|
+
onRename,
|
|
34
|
+
}: SidebarProps) {
|
|
35
|
+
return (
|
|
36
|
+
<nav
|
|
37
|
+
aria-label="Workspace"
|
|
38
|
+
className="flex h-full flex-col bg-sidebar text-sidebar-foreground [--avatar-ring:var(--sidebar)]"
|
|
39
|
+
>
|
|
40
|
+
<div className="flex h-14 shrink-0 items-center gap-2.5 px-4">
|
|
41
|
+
<span
|
|
42
|
+
aria-hidden
|
|
43
|
+
className="inline-flex size-7 items-center justify-center rounded-lg bg-signal text-[13px] font-semibold text-signal-foreground"
|
|
44
|
+
>
|
|
45
|
+
{workspace.slice(0, 1).toUpperCase()}
|
|
46
|
+
</span>
|
|
47
|
+
<span className="truncate text-[15px] font-semibold tracking-tight">{workspace}</span>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div className="scroll-area min-h-0 flex-1 overflow-y-auto px-2 pb-4">
|
|
51
|
+
<h2 className="px-2 pt-3 pb-1.5 text-[12.5px] font-medium text-sidebar-muted">Channels</h2>
|
|
52
|
+
<ul className="space-y-px">
|
|
53
|
+
{channels.map((c) => {
|
|
54
|
+
const active = c.slug === activeSlug;
|
|
55
|
+
return (
|
|
56
|
+
<li key={c.id}>
|
|
57
|
+
<button
|
|
58
|
+
type="button"
|
|
59
|
+
onClick={() => onSelect(c.slug)}
|
|
60
|
+
aria-current={active ? "page" : undefined}
|
|
61
|
+
className={cn(
|
|
62
|
+
"flex h-8 w-full items-center gap-2 rounded-md px-2 text-left text-[14.5px] transition-colors duration-150",
|
|
63
|
+
active
|
|
64
|
+
? "bg-sidebar-accent text-sidebar-accent-foreground"
|
|
65
|
+
: c.unread > 0
|
|
66
|
+
? "font-semibold text-sidebar-foreground hover:bg-sidebar-accent/60"
|
|
67
|
+
: "text-sidebar-muted-strong hover:bg-sidebar-accent/60 hover:text-sidebar-foreground",
|
|
68
|
+
)}
|
|
69
|
+
>
|
|
70
|
+
<Hash className="size-4 shrink-0 opacity-60" strokeWidth={1.75} aria-hidden />
|
|
71
|
+
<span className="flex-1 truncate">{c.name}</span>
|
|
72
|
+
{c.unread > 0 && !active ? (
|
|
73
|
+
<span className="min-w-5 rounded-full bg-signal px-1.5 text-center text-[11px] leading-5 font-semibold text-signal-foreground tabular-nums">
|
|
74
|
+
{c.unread > 99 ? "99+" : c.unread}
|
|
75
|
+
<span className="sr-only"> unread</span>
|
|
76
|
+
</span>
|
|
77
|
+
) : null}
|
|
78
|
+
</button>
|
|
79
|
+
</li>
|
|
80
|
+
);
|
|
81
|
+
})}
|
|
82
|
+
</ul>
|
|
83
|
+
|
|
84
|
+
<h2 className="px-2 pt-6 pb-1.5 text-[12.5px] font-medium text-sidebar-muted">
|
|
85
|
+
Online <span className="tabular-nums">{online.length}</span>
|
|
86
|
+
</h2>
|
|
87
|
+
<ul className="space-y-px">
|
|
88
|
+
{online.map((m) => (
|
|
89
|
+
<li key={m.userId} className="flex h-9 items-center gap-2.5 rounded-md px-2 text-[14px]">
|
|
90
|
+
<Avatar name={m.name} hue={m.hue} size="sm" online />
|
|
91
|
+
<span className="truncate text-sidebar-muted-strong">{m.name}</span>
|
|
92
|
+
{m.userId === self?.userId ? (
|
|
93
|
+
<span className="text-[12px] text-sidebar-muted">you</span>
|
|
94
|
+
) : null}
|
|
95
|
+
</li>
|
|
96
|
+
))}
|
|
97
|
+
</ul>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
{self ? <SelfCard self={self} onRename={onRename} /> : null}
|
|
101
|
+
</nav>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function SelfCard({ self, onRename }: { self: ChatMember; onRename: (name: string) => Promise<void> }) {
|
|
106
|
+
const [editing, setEditing] = useState(false);
|
|
107
|
+
const [draft, setDraft] = useState(self.name);
|
|
108
|
+
const [error, setError] = useState<string | null>(null);
|
|
109
|
+
const [saving, setSaving] = useState(false);
|
|
110
|
+
|
|
111
|
+
async function save() {
|
|
112
|
+
setSaving(true);
|
|
113
|
+
setError(null);
|
|
114
|
+
try {
|
|
115
|
+
await onRename(draft);
|
|
116
|
+
setEditing(false);
|
|
117
|
+
} catch (e) {
|
|
118
|
+
setError(e instanceof Error ? e.message : "Could not save the name.");
|
|
119
|
+
} finally {
|
|
120
|
+
setSaving(false);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<div className="shrink-0 border-t border-sidebar-border p-2 pb-[max(0.5rem,env(safe-area-inset-bottom))]">
|
|
126
|
+
{editing ? (
|
|
127
|
+
<form
|
|
128
|
+
onSubmit={(e) => {
|
|
129
|
+
e.preventDefault();
|
|
130
|
+
void save();
|
|
131
|
+
}}
|
|
132
|
+
className="flex items-center gap-1.5 rounded-lg bg-sidebar-accent p-1.5"
|
|
133
|
+
>
|
|
134
|
+
<label htmlFor="display-name" className="sr-only">
|
|
135
|
+
Display name
|
|
136
|
+
</label>
|
|
137
|
+
<input
|
|
138
|
+
id="display-name"
|
|
139
|
+
autoFocus
|
|
140
|
+
value={draft}
|
|
141
|
+
maxLength={MAX_NAME_LENGTH}
|
|
142
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
143
|
+
onKeyDown={(e) => {
|
|
144
|
+
if (e.key === "Escape") setEditing(false);
|
|
145
|
+
}}
|
|
146
|
+
className="h-8 min-w-0 flex-1 rounded-md bg-sidebar px-2 text-[14px] text-sidebar-foreground outline-none ring-1 ring-sidebar-border focus:ring-sidebar-ring"
|
|
147
|
+
/>
|
|
148
|
+
<button
|
|
149
|
+
type="submit"
|
|
150
|
+
disabled={saving}
|
|
151
|
+
aria-label="Save name"
|
|
152
|
+
className="inline-flex size-8 items-center justify-center rounded-md text-sidebar-foreground hover:bg-sidebar"
|
|
153
|
+
>
|
|
154
|
+
<Check className="size-4" strokeWidth={1.75} aria-hidden />
|
|
155
|
+
</button>
|
|
156
|
+
<button
|
|
157
|
+
type="button"
|
|
158
|
+
onClick={() => setEditing(false)}
|
|
159
|
+
aria-label="Cancel"
|
|
160
|
+
className="inline-flex size-8 items-center justify-center rounded-md text-sidebar-muted hover:bg-sidebar"
|
|
161
|
+
>
|
|
162
|
+
<X className="size-4" strokeWidth={1.75} aria-hidden />
|
|
163
|
+
</button>
|
|
164
|
+
</form>
|
|
165
|
+
) : (
|
|
166
|
+
<div className="flex items-center gap-2.5 rounded-lg p-1.5">
|
|
167
|
+
<Avatar name={self.name} hue={self.hue} size="sm" online />
|
|
168
|
+
<div className="min-w-0 flex-1 leading-tight">
|
|
169
|
+
<div className="truncate text-[14px] font-medium">{self.name}</div>
|
|
170
|
+
<div className="text-[12px] text-sidebar-muted">Guest</div>
|
|
171
|
+
</div>
|
|
172
|
+
<button
|
|
173
|
+
type="button"
|
|
174
|
+
onClick={() => {
|
|
175
|
+
setDraft(self.name);
|
|
176
|
+
setError(null);
|
|
177
|
+
setEditing(true);
|
|
178
|
+
}}
|
|
179
|
+
aria-label="Edit display name"
|
|
180
|
+
title="Edit display name"
|
|
181
|
+
className="inline-flex size-8 items-center justify-center rounded-md text-sidebar-muted transition-colors hover:bg-sidebar-accent hover:text-sidebar-foreground"
|
|
182
|
+
>
|
|
183
|
+
<Pencil className="size-3.5" strokeWidth={1.75} aria-hidden />
|
|
184
|
+
</button>
|
|
185
|
+
</div>
|
|
186
|
+
)}
|
|
187
|
+
{error ? <p className="px-2 pt-1 text-[12px] text-red-300">{error}</p> : null}
|
|
188
|
+
</div>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { mutation, type DbWriter } from "@pylonsync/functions";
|
|
2
|
+
import { guestHue, guestName } from "../lib/chat";
|
|
3
|
+
import { includeDemoConversation, seedChannels, seedMessages, seedPeople } from "../lib/seed";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Called by the client once per page load, after the guest session exists.
|
|
7
|
+
*
|
|
8
|
+
* 1. On the first call against an empty database, creates the channels from
|
|
9
|
+
* lib/seed.ts and, when `includeDemoConversation` is on, the four demo
|
|
10
|
+
* people and their conversation.
|
|
11
|
+
* 2. Creates the caller's Member row (a generated name like "Amber Otter" and
|
|
12
|
+
* an avatar hue) if it does not exist yet.
|
|
13
|
+
* 3. Returns the server time, which the client uses as the starting "read up
|
|
14
|
+
* to" mark for channels it has not opened yet.
|
|
15
|
+
*
|
|
16
|
+
* Both steps are idempotent. The advisory lock keeps two first visitors from
|
|
17
|
+
* seeding twice on Postgres; SQLite already serializes writers.
|
|
18
|
+
*
|
|
19
|
+
* Channel and Member deny client writes, so these writes use `ctx.db.unsafe`,
|
|
20
|
+
* which keeps them working under PYLON_STRICT_FN_POLICIES. Every value comes
|
|
21
|
+
* from lib/seed.ts or the caller's session, never from arguments.
|
|
22
|
+
*/
|
|
23
|
+
export default mutation({
|
|
24
|
+
auth: "guest",
|
|
25
|
+
args: {},
|
|
26
|
+
async handler(ctx): Promise<{ memberId: string; seeded: boolean; now: string }> {
|
|
27
|
+
const userId = ctx.auth.userId;
|
|
28
|
+
if (!userId) throw ctx.error("AUTH_REQUIRED", "Start a guest session first.");
|
|
29
|
+
await ctx.db.advisoryLock("chat_join");
|
|
30
|
+
|
|
31
|
+
let seeded = false;
|
|
32
|
+
const channels = await ctx.db.query("Channel", { $limit: 1 });
|
|
33
|
+
if (channels.length === 0) {
|
|
34
|
+
const channelIds = await seedChannelRows(ctx.db);
|
|
35
|
+
if (includeDemoConversation) {
|
|
36
|
+
// Demo messages carry the demo authors' ids, and field.owner() lets
|
|
37
|
+
// only an admin context write an owner id that isn't the caller's.
|
|
38
|
+
await ctx.auth.elevate({ admin: true, reason: "seed demo chat conversation" });
|
|
39
|
+
await seedConversation(ctx.db, channelIds);
|
|
40
|
+
}
|
|
41
|
+
seeded = true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const existing = await ctx.db.lookup("Member", "userId", userId);
|
|
45
|
+
const now = new Date().toISOString();
|
|
46
|
+
if (existing) return { memberId: existing.id as string, seeded, now };
|
|
47
|
+
|
|
48
|
+
const memberId = await ctx.db.unsafe.insert("Member", {
|
|
49
|
+
userId,
|
|
50
|
+
name: guestName(userId),
|
|
51
|
+
hue: guestHue(userId),
|
|
52
|
+
});
|
|
53
|
+
return { memberId, seeded, now };
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
async function seedChannelRows(db: DbWriter): Promise<Map<string, string>> {
|
|
58
|
+
const channelIds = new Map<string, string>();
|
|
59
|
+
for (const [position, c] of seedChannels.entries()) {
|
|
60
|
+
const id = await db.unsafe.insert("Channel", { ...c, position });
|
|
61
|
+
channelIds.set(c.slug, id);
|
|
62
|
+
}
|
|
63
|
+
return channelIds;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function seedConversation(db: DbWriter, channelIds: Map<string, string>) {
|
|
67
|
+
for (const p of seedPeople) {
|
|
68
|
+
await db.unsafe.insert("Member", { userId: p.userId, name: p.name, hue: p.hue });
|
|
69
|
+
}
|
|
70
|
+
const now = Date.now();
|
|
71
|
+
for (const m of seedMessages) {
|
|
72
|
+
const channelId = channelIds.get(m.channel);
|
|
73
|
+
if (!channelId) continue;
|
|
74
|
+
// Demo authors are not session users, so these rows set authorId and
|
|
75
|
+
// createdAt directly.
|
|
76
|
+
await db.insert("Message", {
|
|
77
|
+
channelId,
|
|
78
|
+
authorId: m.author,
|
|
79
|
+
text: m.text,
|
|
80
|
+
createdAt: new Date(now - m.minutesAgo * 60_000).toISOString(),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { mutation, v } from "@pylonsync/functions";
|
|
2
|
+
import { cleanName, MAX_NAME_LENGTH } from "../lib/chat";
|
|
3
|
+
import { seedPeople } from "../lib/seed";
|
|
4
|
+
|
|
5
|
+
/** Changes the caller's display name. Other people see it on every message at once. */
|
|
6
|
+
export default mutation({
|
|
7
|
+
auth: "guest",
|
|
8
|
+
args: { name: v.string() },
|
|
9
|
+
async handler(ctx, { name }) {
|
|
10
|
+
const clean = cleanName(name);
|
|
11
|
+
if (!clean) {
|
|
12
|
+
throw ctx.error("INVALID_NAME", `Use 1 to ${MAX_NAME_LENGTH} characters.`);
|
|
13
|
+
}
|
|
14
|
+
const taken = seedPeople.some((p) => p.name.toLowerCase() === clean.toLowerCase());
|
|
15
|
+
if (taken) throw ctx.error("NAME_TAKEN", "Someone in this workspace already uses that name.");
|
|
16
|
+
const userId = ctx.auth.userId;
|
|
17
|
+
if (!userId) throw ctx.error("AUTH_REQUIRED", "Start a guest session first.");
|
|
18
|
+
const member = await ctx.db.lookup("Member", "userId", userId);
|
|
19
|
+
if (!member) throw ctx.error("NOT_JOINED", "Call joinChat first.");
|
|
20
|
+
// Member denies client writes; `unsafe` keeps this working under
|
|
21
|
+
// PYLON_STRICT_FN_POLICIES. The row is the caller's own, found by session id.
|
|
22
|
+
await ctx.db.unsafe.update("Member", member.id as string, { name: clean });
|
|
23
|
+
return { name: clean };
|
|
24
|
+
},
|
|
25
|
+
});
|