@pylonsync/create-pylon 0.19.1 → 0.21.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/_root/AGENTS.md +1 -1
- package/templates/agency/.env.example +7 -0
- package/templates/agency/AGENTS.md +1 -1
- 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/AGENTS.md +1 -1
- 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/ai-studio/AGENTS.md +1 -1
- 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/barebones/AGENTS.md +1 -1
- package/templates/chat/AGENTS.md +1 -1
- 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/AGENTS.md +1 -1
- 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/AGENTS.md +1 -1
- 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/AGENTS.md +1 -1
- 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/AGENTS.md +1 -1
- 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/AGENTS.md +1 -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/AGENTS.md +1 -1
- 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/AGENTS.md +1 -1
- 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/AGENTS.md +1 -1
- 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/AGENTS.md +1 -1
- 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/AGENTS.md +1 -1
- 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/AGENTS.md +1 -1
- 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/AGENTS.md +1 -1
- 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/AGENTS.md +1 -1
- 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/todo/AGENTS.md +1 -1
- package/templates/waitlist/.env.example +7 -0
- package/templates/waitlist/AGENTS.md +1 -1
- 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,188 @@
|
|
|
1
|
+
// Pure chat helpers: names, colors, grouping, and labels. No Pylon imports,
|
|
2
|
+
// so tests/chat.test.ts runs them without a server and the server functions
|
|
3
|
+
// and the client share one copy.
|
|
4
|
+
|
|
5
|
+
export interface ChatMessage {
|
|
6
|
+
id: string;
|
|
7
|
+
channelId: string;
|
|
8
|
+
authorId: string;
|
|
9
|
+
text: string;
|
|
10
|
+
/** Absent on an optimistic row until the server stamps it. */
|
|
11
|
+
createdAt?: string | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ChatMember {
|
|
15
|
+
id: string;
|
|
16
|
+
userId: string;
|
|
17
|
+
name: string;
|
|
18
|
+
hue: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const MAX_MESSAGE_LENGTH = 4000;
|
|
22
|
+
export const MAX_NAME_LENGTH = 32;
|
|
23
|
+
|
|
24
|
+
/** Messages from one author closer together than this share one header. */
|
|
25
|
+
export const GROUP_GAP_MS = 5 * 60 * 1000;
|
|
26
|
+
|
|
27
|
+
const ADJECTIVES = [
|
|
28
|
+
"Amber", "Brisk", "Cobalt", "Dusky", "Ember", "Fern", "Gilded", "Hazel",
|
|
29
|
+
"Indigo", "Juniper", "Kestrel", "Lunar", "Maple", "Nimble", "Ochre", "Pewter",
|
|
30
|
+
"Quiet", "Russet", "Saffron", "Tidal", "Umber", "Velvet", "Willow", "Zephyr",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const ANIMALS = [
|
|
34
|
+
"Otter", "Heron", "Lynx", "Finch", "Marten", "Ibis", "Badger", "Wren",
|
|
35
|
+
"Puffin", "Stoat", "Crane", "Hare", "Magpie", "Newt", "Osprey", "Plover",
|
|
36
|
+
"Raven", "Seal", "Tern", "Vole", "Wolf", "Yak", "Gecko", "Moth",
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/** FNV-1a: a small, stable string hash. The same id always maps to the same name. */
|
|
40
|
+
export function hashString(input: string): number {
|
|
41
|
+
let h = 0x811c9dc5;
|
|
42
|
+
for (let i = 0; i < input.length; i++) {
|
|
43
|
+
h ^= input.charCodeAt(i);
|
|
44
|
+
h = Math.imul(h, 0x01000193);
|
|
45
|
+
}
|
|
46
|
+
return h >>> 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** "Amber Otter" style display name for a guest, derived from the user id. */
|
|
50
|
+
export function guestName(userId: string): string {
|
|
51
|
+
const h = hashString(userId);
|
|
52
|
+
const adjective = ADJECTIVES[h % ADJECTIVES.length];
|
|
53
|
+
const animal = ANIMALS[Math.floor(h / ADJECTIVES.length) % ANIMALS.length];
|
|
54
|
+
return `${adjective} ${animal}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Avatar hue (0–359) derived from the user id. */
|
|
58
|
+
export function guestHue(userId: string): number {
|
|
59
|
+
return hashString(`hue:${userId}`) % 360;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Trims, collapses whitespace, and validates a display name. Returns null when invalid. */
|
|
63
|
+
export function cleanName(raw: string): string | null {
|
|
64
|
+
// \p{Cf} covers zero-width and bidi control characters, which could make a
|
|
65
|
+
// name invisible or reorder the text around it.
|
|
66
|
+
const name = raw.replace(/\p{Cf}/gu, "").replace(/\s+/g, " ").trim();
|
|
67
|
+
if (name.length === 0 || name.length > MAX_NAME_LENGTH) return null;
|
|
68
|
+
return name;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Up to two initials: "Maya Chen" → "MC", "theo" → "T". */
|
|
72
|
+
export function initials(name: string): string {
|
|
73
|
+
const parts = name.trim().split(/\s+/).filter(Boolean);
|
|
74
|
+
if (parts.length === 0) return "?";
|
|
75
|
+
// Array.from splits by code point, so an emoji stays whole.
|
|
76
|
+
const first = Array.from(parts[0])[0] ?? "";
|
|
77
|
+
const last = parts.length > 1 ? (Array.from(parts[parts.length - 1])[0] ?? "") : "";
|
|
78
|
+
return (first + last).toUpperCase();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Chronological order. Rows the server has not stamped yet sort last. Equal
|
|
83
|
+
* timestamps (Postgres stores whole seconds) fall back to the id, which is
|
|
84
|
+
* time-ordered, so every client shows the same order.
|
|
85
|
+
*/
|
|
86
|
+
export function sortMessages<T extends ChatMessage>(messages: readonly T[]): T[] {
|
|
87
|
+
return [...messages].sort((a, b) => {
|
|
88
|
+
if (!a.createdAt && !b.createdAt) return 0;
|
|
89
|
+
if (!a.createdAt) return 1;
|
|
90
|
+
if (!b.createdAt) return -1;
|
|
91
|
+
return a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface MessageGroup<T extends ChatMessage = ChatMessage> {
|
|
96
|
+
key: string;
|
|
97
|
+
authorId: string;
|
|
98
|
+
/** Local-day key ("2026-09-26") of the first message; null while pending. */
|
|
99
|
+
day: string | null;
|
|
100
|
+
messages: T[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Local calendar day of an ISO timestamp, as "YYYY-MM-DD". */
|
|
104
|
+
export function dayKey(iso: string): string {
|
|
105
|
+
const d = new Date(iso);
|
|
106
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
107
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
108
|
+
return `${d.getFullYear()}-${m}-${day}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Groups sorted messages into runs by one author. A new group starts when the
|
|
113
|
+
* author changes, the local day changes, or the gap exceeds GROUP_GAP_MS. A
|
|
114
|
+
* pending row (no createdAt) joins the previous group when the author matches.
|
|
115
|
+
*/
|
|
116
|
+
export function groupMessages<T extends ChatMessage>(
|
|
117
|
+
sorted: readonly T[],
|
|
118
|
+
gapMs: number = GROUP_GAP_MS,
|
|
119
|
+
): MessageGroup<T>[] {
|
|
120
|
+
const groups: MessageGroup<T>[] = [];
|
|
121
|
+
let lastTime: number | null = null;
|
|
122
|
+
for (const m of sorted) {
|
|
123
|
+
const prev = groups[groups.length - 1];
|
|
124
|
+
const time = m.createdAt ? Date.parse(m.createdAt) : null;
|
|
125
|
+
const day = m.createdAt ? dayKey(m.createdAt) : null;
|
|
126
|
+
const sameAuthor = prev?.authorId === m.authorId;
|
|
127
|
+
const sameDay = day === null || prev?.day === null || prev?.day === day;
|
|
128
|
+
const closeInTime = time === null || lastTime === null || time - lastTime <= gapMs;
|
|
129
|
+
if (prev && sameAuthor && sameDay && closeInTime) {
|
|
130
|
+
prev.messages.push(m);
|
|
131
|
+
} else {
|
|
132
|
+
groups.push({ key: m.id, authorId: m.authorId, day, messages: [m] });
|
|
133
|
+
}
|
|
134
|
+
if (time !== null) lastTime = time;
|
|
135
|
+
}
|
|
136
|
+
return groups;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** "Today", "Yesterday", "Wednesday", or "Monday, September 21". */
|
|
140
|
+
export function dayLabel(iso: string, now: Date = new Date()): string {
|
|
141
|
+
const d = new Date(iso);
|
|
142
|
+
const startOf = (x: Date) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime();
|
|
143
|
+
const diffDays = Math.round((startOf(now) - startOf(d)) / 86_400_000);
|
|
144
|
+
if (diffDays === 0) return "Today";
|
|
145
|
+
if (diffDays === 1) return "Yesterday";
|
|
146
|
+
if (diffDays > 1 && diffDays < 7) {
|
|
147
|
+
return d.toLocaleDateString("en-US", { weekday: "long" });
|
|
148
|
+
}
|
|
149
|
+
return d.toLocaleDateString("en-US", {
|
|
150
|
+
weekday: "long",
|
|
151
|
+
month: "long",
|
|
152
|
+
day: "numeric",
|
|
153
|
+
...(d.getFullYear() === now.getFullYear() ? {} : { year: "numeric" }),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** "9:41 AM" in the viewer's locale. */
|
|
158
|
+
export function timeLabel(iso: string): string {
|
|
159
|
+
return new Date(iso).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** "Maya is typing", "Maya and Theo are typing", "Maya, Theo, and 2 others are typing". */
|
|
163
|
+
export function typingLabel(names: readonly string[]): string {
|
|
164
|
+
if (names.length === 0) return "";
|
|
165
|
+
if (names.length === 1) return `${names[0]} is typing`;
|
|
166
|
+
if (names.length === 2) return `${names[0]} and ${names[1]} are typing`;
|
|
167
|
+
if (names.length === 3) return `${names[0]}, ${names[1]}, and ${names[2]} are typing`;
|
|
168
|
+
return `${names[0]}, ${names[1]}, and ${names.length - 2} others are typing`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Messages from other people in a channel, newer than `lastSeen`. A channel the
|
|
173
|
+
* viewer has no mark for counts as read, so a first visit shows no badges.
|
|
174
|
+
*/
|
|
175
|
+
export function unreadCount(
|
|
176
|
+
messages: readonly ChatMessage[],
|
|
177
|
+
channelId: string,
|
|
178
|
+
lastSeen: string | undefined,
|
|
179
|
+
selfId: string,
|
|
180
|
+
): number {
|
|
181
|
+
if (!lastSeen) return 0;
|
|
182
|
+
let n = 0;
|
|
183
|
+
for (const m of messages) {
|
|
184
|
+
if (m.channelId !== channelId || m.authorId === selfId || !m.createdAt) continue;
|
|
185
|
+
if (m.createdAt > lastSeen) n++;
|
|
186
|
+
}
|
|
187
|
+
return n;
|
|
188
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Workspace loaded on first boot by functions/joinChat.ts. The channels are
|
|
2
|
+
// always created. With `includeDemoConversation` on, four demo people (a team
|
|
3
|
+
// shipping an offline-maps update for a trail-running app) and their messages
|
|
4
|
+
// are added too. Set it to false to start with empty channels.
|
|
5
|
+
//
|
|
6
|
+
// `minutesAgo` is relative to the moment the seed runs, so the conversation
|
|
7
|
+
// always reads as yesterday plus this morning.
|
|
8
|
+
|
|
9
|
+
export const includeDemoConversation = true;
|
|
10
|
+
|
|
11
|
+
export interface SeedChannel {
|
|
12
|
+
slug: string;
|
|
13
|
+
name: string;
|
|
14
|
+
topic: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SeedPerson {
|
|
18
|
+
/** Stable author id for seeded rows. Real users have session ids instead. */
|
|
19
|
+
userId: string;
|
|
20
|
+
name: string;
|
|
21
|
+
hue: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SeedMessage {
|
|
25
|
+
channel: string;
|
|
26
|
+
author: string;
|
|
27
|
+
minutesAgo: number;
|
|
28
|
+
text: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const seedChannels: SeedChannel[] = [
|
|
32
|
+
{ slug: "general", name: "general", topic: "Anything that doesn't fit elsewhere" },
|
|
33
|
+
{ slug: "launch", name: "launch", topic: "Ridgeline 2.4 ships Thursday at 10:00" },
|
|
34
|
+
{ slug: "design", name: "design", topic: "Screens, icons, and App Store art" },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
export const seedPeople: SeedPerson[] = [
|
|
38
|
+
{ userId: "seed_maya", name: "Maya Chen", hue: 28 },
|
|
39
|
+
{ userId: "seed_theo", name: "Theo Okafor", hue: 250 },
|
|
40
|
+
{ userId: "seed_priya", name: "Priya Raman", hue: 150 },
|
|
41
|
+
{ userId: "seed_jonas", name: "Jonas Weber", hue: 330 },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const DAY = 24 * 60;
|
|
45
|
+
|
|
46
|
+
export const seedMessages: SeedMessage[] = [
|
|
47
|
+
// #launch, yesterday afternoon
|
|
48
|
+
{ channel: "launch", author: "seed_priya", minutesAgo: DAY + 190, text: "Status check for 2.4. We still have three open items: the iPad crash, the App Store screenshots, and the release notes." },
|
|
49
|
+
{ channel: "launch", author: "seed_maya", minutesAgo: DAY + 186, text: "The iPad crash is in the tile cache. When you download a region bigger than about 2 GB, the eviction pass runs on the main thread and the watchdog kills the app." },
|
|
50
|
+
{ channel: "launch", author: "seed_maya", minutesAgo: DAY + 185, text: "Moving eviction to a background queue now. Should have a build tonight." },
|
|
51
|
+
{ channel: "launch", author: "seed_jonas", minutesAgo: DAY + 171, text: "I can reproduce it every time on the 10th gen iPad with the Cascades region. Happy to run it again once the build is up." },
|
|
52
|
+
{ channel: "launch", author: "seed_priya", minutesAgo: DAY + 168, text: "Great. If it slips past Wednesday noon we move the release to next week. I'd rather not ship offline maps that crash offline." },
|
|
53
|
+
{ channel: "launch", author: "seed_theo", minutesAgo: DAY + 122, text: "Screenshots are 80% done. Waiting on a clean capture of the elevation profile with the new colors." },
|
|
54
|
+
{ channel: "launch", author: "seed_maya", minutesAgo: DAY + 34, text: "TestFlight build 412 is up. Eviction runs in the background, and I added a progress bar so a big cleanup doesn't look frozen." },
|
|
55
|
+
{ channel: "launch", author: "seed_jonas", minutesAgo: DAY + 21, text: "Installing now." },
|
|
56
|
+
|
|
57
|
+
// #launch, this morning
|
|
58
|
+
{ channel: "launch", author: "seed_jonas", minutesAgo: 58, text: "Build 412 survived six full downloads of the Cascades region on the iPad. No crash, memory stays under 600 MB." },
|
|
59
|
+
{ channel: "launch", author: "seed_jonas", minutesAgo: 57, text: "One small thing: the progress bar says 100% for about four seconds before it disappears." },
|
|
60
|
+
{ channel: "launch", author: "seed_maya", minutesAgo: 49, text: "That's the index rebuild after the last tile lands. I'll change the label to \"Finishing up\" for that step instead of hiding it." },
|
|
61
|
+
{ channel: "launch", author: "seed_priya", minutesAgo: 44, text: "Perfect. That takes the crash off the list 🎉" },
|
|
62
|
+
{ channel: "launch", author: "seed_priya", minutesAgo: 43, text: "Remaining: screenshots (Theo) and release notes (me). Submitting for review Wednesday at 3." },
|
|
63
|
+
{ channel: "launch", author: "seed_theo", minutesAgo: 12, text: "Screenshots are uploaded to the shared folder: 6.9\" and 13\" iPad, light and dark. Can someone check the Spanish captions?" },
|
|
64
|
+
{ channel: "launch", author: "seed_priya", minutesAgo: 6, text: "On it after standup." },
|
|
65
|
+
|
|
66
|
+
// #design
|
|
67
|
+
{ channel: "design", author: "seed_theo", minutesAgo: DAY + 240, text: "New elevation colors: climbs go from sand to rust, descents from sky to deep blue. The old green-to-red read like a heart rate chart." },
|
|
68
|
+
{ channel: "design", author: "seed_maya", minutesAgo: DAY + 231, text: "Much easier to read in sunlight. Is the rust still readable on the dark map style?" },
|
|
69
|
+
{ channel: "design", author: "seed_theo", minutesAgo: DAY + 226, text: "I bumped its lightness 8% in dark mode. Checked it on a phone outside at noon." },
|
|
70
|
+
{ channel: "design", author: "seed_theo", minutesAgo: 95, text: "Offline badge is final: a small filled cloud with a slash, bottom-left of the region card. No more \"Saved\" text label." },
|
|
71
|
+
{ channel: "design", author: "seed_jonas", minutesAgo: 88, text: "Support will like that. Half the tickets last month were people not knowing a region was already downloaded." },
|
|
72
|
+
|
|
73
|
+
// #general
|
|
74
|
+
{ channel: "general", author: "seed_priya", minutesAgo: DAY + 300, text: "Welcome to the team chat. #launch is for the 2.4 release, #design for screens and art." },
|
|
75
|
+
{ channel: "general", author: "seed_jonas", minutesAgo: DAY + 75, text: "Trail run Saturday at 8 at the Mill Creek trailhead. 12 km, easy pace, coffee after." },
|
|
76
|
+
{ channel: "general", author: "seed_theo", minutesAgo: DAY + 70, text: "In, as long as easy pace means easy pace." },
|
|
77
|
+
{ channel: "general", author: "seed_maya", minutesAgo: 30, text: "Standup moved to 10:30 today, Priya is on a call with the App Review team." },
|
|
78
|
+
];
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
cleanName,
|
|
4
|
+
dayLabel,
|
|
5
|
+
groupMessages,
|
|
6
|
+
guestHue,
|
|
7
|
+
guestName,
|
|
8
|
+
initials,
|
|
9
|
+
sortMessages,
|
|
10
|
+
typingLabel,
|
|
11
|
+
unreadCount,
|
|
12
|
+
type ChatMessage,
|
|
13
|
+
} from "../lib/chat";
|
|
14
|
+
import { seedChannels, seedMessages, seedPeople } from "../lib/seed";
|
|
15
|
+
|
|
16
|
+
const at = (iso: string, authorId = "a", id = iso, channelId = "c1"): ChatMessage => ({
|
|
17
|
+
id,
|
|
18
|
+
channelId,
|
|
19
|
+
authorId,
|
|
20
|
+
text: "x",
|
|
21
|
+
createdAt: iso,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("guest identity", () => {
|
|
25
|
+
test("the same id always gets the same name and hue", () => {
|
|
26
|
+
expect(guestName("guest_abc")).toBe(guestName("guest_abc"));
|
|
27
|
+
expect(guestHue("guest_abc")).toBe(guestHue("guest_abc"));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("names are two words and hues are angles", () => {
|
|
31
|
+
for (const id of ["guest_1", "guest_2", "guest_ffff", "u_42"]) {
|
|
32
|
+
expect(guestName(id)).toMatch(/^[A-Z][a-z]+ [A-Z][a-z]+$/);
|
|
33
|
+
const hue = guestHue(id);
|
|
34
|
+
expect(hue).toBeGreaterThanOrEqual(0);
|
|
35
|
+
expect(hue).toBeLessThan(360);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("cleanName", () => {
|
|
41
|
+
test("trims and collapses whitespace", () => {
|
|
42
|
+
expect(cleanName(" Maya Chen ")).toBe("Maya Chen");
|
|
43
|
+
});
|
|
44
|
+
test("removes zero-width and bidi control characters", () => {
|
|
45
|
+
expect(cleanName("Ma\u200bya\u202e")).toBe("Maya");
|
|
46
|
+
expect(cleanName("\u200b\u200b")).toBeNull();
|
|
47
|
+
});
|
|
48
|
+
test("rejects empty and over-long names", () => {
|
|
49
|
+
expect(cleanName(" ")).toBeNull();
|
|
50
|
+
expect(cleanName("x".repeat(33))).toBeNull();
|
|
51
|
+
expect(cleanName("x".repeat(32))).toBe("x".repeat(32));
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("initials", () => {
|
|
56
|
+
expect(initials("Maya Chen")).toBe("MC");
|
|
57
|
+
expect(initials("theo")).toBe("T");
|
|
58
|
+
expect(initials("Priya de la Raman")).toBe("PR");
|
|
59
|
+
expect(initials(" ")).toBe("?");
|
|
60
|
+
expect(initials("🎉 Party")).toBe("🎉P");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("sortMessages", () => {
|
|
64
|
+
test("orders by time and puts unsent rows last", () => {
|
|
65
|
+
const pending: ChatMessage = { id: "p", channelId: "c1", authorId: "a", text: "x" };
|
|
66
|
+
const sorted = sortMessages([at("2026-09-26T10:05:00Z"), pending, at("2026-09-26T10:00:00Z")]);
|
|
67
|
+
expect(sorted.map((m) => m.id)).toEqual(["2026-09-26T10:00:00Z", "2026-09-26T10:05:00Z", "p"]);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("equal timestamps fall back to id order", () => {
|
|
72
|
+
const sorted = sortMessages([
|
|
73
|
+
at("2026-09-26T10:00:00Z", "a", "b-id"),
|
|
74
|
+
at("2026-09-26T10:00:00Z", "a", "a-id"),
|
|
75
|
+
]);
|
|
76
|
+
expect(sorted.map((m) => m.id)).toEqual(["a-id", "b-id"]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("groupMessages", () => {
|
|
80
|
+
test("joins consecutive messages from one author within five minutes", () => {
|
|
81
|
+
const groups = groupMessages([
|
|
82
|
+
at("2026-09-26T10:00:00Z", "a"),
|
|
83
|
+
at("2026-09-26T10:03:00Z", "a"),
|
|
84
|
+
at("2026-09-26T10:04:00Z", "b"),
|
|
85
|
+
at("2026-09-26T10:20:00Z", "b"),
|
|
86
|
+
]);
|
|
87
|
+
expect(groups.map((g) => [g.authorId, g.messages.length])).toEqual([
|
|
88
|
+
["a", 2],
|
|
89
|
+
["b", 1],
|
|
90
|
+
["b", 1],
|
|
91
|
+
]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("starts a new group on a new day", () => {
|
|
95
|
+
const groups = groupMessages([
|
|
96
|
+
at(new Date(2026, 8, 25, 23, 58).toISOString(), "a"),
|
|
97
|
+
at(new Date(2026, 8, 26, 0, 1).toISOString(), "a"),
|
|
98
|
+
]);
|
|
99
|
+
expect(groups).toHaveLength(2);
|
|
100
|
+
expect(groups[0].day).not.toBe(groups[1].day);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("a pending row joins the sender's last group", () => {
|
|
104
|
+
const pending: ChatMessage = { id: "p", channelId: "c1", authorId: "a", text: "x", createdAt: null };
|
|
105
|
+
const groups = groupMessages([at("2026-09-26T10:00:00Z", "a"), pending]);
|
|
106
|
+
expect(groups).toHaveLength(1);
|
|
107
|
+
expect(groups[0].messages.map((m) => m.id)).toEqual(["2026-09-26T10:00:00Z", "p"]);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("dayLabel", () => {
|
|
112
|
+
const now = new Date(2026, 8, 26, 12, 0);
|
|
113
|
+
test("today and yesterday", () => {
|
|
114
|
+
expect(dayLabel(new Date(2026, 8, 26, 8, 0).toISOString(), now)).toBe("Today");
|
|
115
|
+
expect(dayLabel(new Date(2026, 8, 25, 23, 0).toISOString(), now)).toBe("Yesterday");
|
|
116
|
+
});
|
|
117
|
+
test("weekday within a week, full date after", () => {
|
|
118
|
+
expect(dayLabel(new Date(2026, 8, 22, 9, 0).toISOString(), now)).toBe("Tuesday");
|
|
119
|
+
expect(dayLabel(new Date(2026, 8, 14, 9, 0).toISOString(), now)).toBe("Monday, September 14");
|
|
120
|
+
expect(dayLabel(new Date(2025, 8, 14, 9, 0).toISOString(), now)).toBe("Sunday, September 14, 2025");
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("typingLabel", () => {
|
|
125
|
+
expect(typingLabel([])).toBe("");
|
|
126
|
+
expect(typingLabel(["Maya"])).toBe("Maya is typing");
|
|
127
|
+
expect(typingLabel(["Maya", "Theo"])).toBe("Maya and Theo are typing");
|
|
128
|
+
expect(typingLabel(["Maya", "Theo", "Priya"])).toBe("Maya, Theo, and Priya are typing");
|
|
129
|
+
expect(typingLabel(["Maya", "Theo", "Priya", "Jonas"])).toBe("Maya, Theo, and 2 others are typing");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("unreadCount", () => {
|
|
133
|
+
const msgs = [
|
|
134
|
+
at("2026-09-26T10:00:00Z", "other", "1"),
|
|
135
|
+
at("2026-09-26T10:05:00Z", "other", "2"),
|
|
136
|
+
at("2026-09-26T10:06:00Z", "me", "3"),
|
|
137
|
+
at("2026-09-26T10:07:00Z", "other", "4", "c2"),
|
|
138
|
+
];
|
|
139
|
+
test("counts other people's messages after the mark in that channel", () => {
|
|
140
|
+
expect(unreadCount(msgs, "c1", "2026-09-26T10:01:00Z", "me")).toBe(1);
|
|
141
|
+
});
|
|
142
|
+
test("a channel without a mark counts as read", () => {
|
|
143
|
+
expect(unreadCount(msgs, "c1", undefined, "me")).toBe(0);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("seed", () => {
|
|
148
|
+
test("every seeded message points at a seeded channel and person", () => {
|
|
149
|
+
const channels = new Set(seedChannels.map((c) => c.slug));
|
|
150
|
+
const people = new Set(seedPeople.map((p) => p.userId));
|
|
151
|
+
for (const m of seedMessages) {
|
|
152
|
+
expect(channels.has(m.channel)).toBe(true);
|
|
153
|
+
expect(people.has(m.author)).toBe(true);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
test("seed ids cannot collide with session user ids", () => {
|
|
157
|
+
for (const p of seedPeople) expect(p.userId.startsWith("seed_")).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { afterEach, expect, mock, test } from "bun:test";
|
|
2
|
+
// This project uses the classic JSX transform, so .tsx tests import React.
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
|
5
|
+
import { Composer } from "../components/chat/composer";
|
|
6
|
+
|
|
7
|
+
afterEach(cleanup);
|
|
8
|
+
|
|
9
|
+
function setup() {
|
|
10
|
+
const onSend = mock((_text: string) => {});
|
|
11
|
+
const onTyping = mock(() => {});
|
|
12
|
+
const onStopTyping = mock(() => {});
|
|
13
|
+
render(
|
|
14
|
+
<Composer
|
|
15
|
+
channelName="launch"
|
|
16
|
+
channelKey="c1"
|
|
17
|
+
onSend={onSend}
|
|
18
|
+
onTyping={onTyping}
|
|
19
|
+
onStopTyping={onStopTyping}
|
|
20
|
+
/>,
|
|
21
|
+
);
|
|
22
|
+
const field = screen.getByPlaceholderText("Message #launch") as HTMLTextAreaElement;
|
|
23
|
+
return { field, onSend, onTyping, onStopTyping };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test("Enter sends the trimmed text and clears the field", () => {
|
|
27
|
+
const { field, onSend, onStopTyping } = setup();
|
|
28
|
+
fireEvent.change(field, { target: { value: " ship it " } });
|
|
29
|
+
fireEvent.keyDown(field, { key: "Enter" });
|
|
30
|
+
expect(onSend).toHaveBeenCalledWith("ship it");
|
|
31
|
+
expect(field.value).toBe("");
|
|
32
|
+
expect(onStopTyping).toHaveBeenCalled();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("Shift+Enter does not send", () => {
|
|
36
|
+
const { field, onSend } = setup();
|
|
37
|
+
fireEvent.change(field, { target: { value: "line one" } });
|
|
38
|
+
fireEvent.keyDown(field, { key: "Enter", shiftKey: true });
|
|
39
|
+
expect(onSend).not.toHaveBeenCalled();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("whitespace-only text does not send and the button stays disabled", () => {
|
|
43
|
+
const { field, onSend } = setup();
|
|
44
|
+
fireEvent.change(field, { target: { value: " " } });
|
|
45
|
+
fireEvent.keyDown(field, { key: "Enter" });
|
|
46
|
+
expect(onSend).not.toHaveBeenCalled();
|
|
47
|
+
expect((screen.getByRole("button", { name: "Send message" }) as HTMLButtonElement).disabled).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("typing reports activity", () => {
|
|
51
|
+
const { field, onTyping } = setup();
|
|
52
|
+
fireEvent.change(field, { target: { value: "h" } });
|
|
53
|
+
expect(onTyping).toHaveBeenCalled();
|
|
54
|
+
});
|
|
@@ -62,7 +62,7 @@ this pass before the user has to ask.
|
|
|
62
62
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
63
63
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
64
64
|
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
65
|
-
- **`ctx.llm.stream(request, onEvent)` streams tokens as they generate** and still resolves with the full response, so `stop_reason === "tool_use"` drives an agent tool loop. Events are `text_delta` / `tool_use_start` / `tool_input_delta` / `done`. Same auth + model-allowlist gating as `ctx.llm.complete`.
|
|
65
|
+
- **`ctx.llm.stream(request, onEvent)` streams tokens as they generate** and still resolves with the full response, so `stop_reason === "tool_use"` drives an agent tool loop. Events are `text_delta` / `tool_use_start` / `tool_input_delta` / `done`. Same auth + model-allowlist gating as `ctx.llm.complete`. The call timeout is an idle timeout (`PYLON_FN_CALL_TIMEOUT`, 30s default; Stack0 Cloud 300s): each stream event resets it, and the total run is capped at 10× the timeout. Set `timeout: <secs>` on the def when a step (a slow tool call) can be silent longer than that.
|
|
66
66
|
- **`ctx.stream.write(text)` streams to the caller — and every fn stream is RESUMABLE**: the server buffers frames under a stream id (`streamFn`'s `onStreamId`), so a dropped connection or closed tab catches up via `resumeStream(id)`, including the final result after the handler finished. `ctx.rooms.broadcast(room, topic, data)` fans out to every CURRENTLY-CONNECTED subscriber (second device, second tab) but does not replay missed messages — use the stream id for anything that must survive a gap. Broadcast resolves `{ delivered: false }` when the room has no members — a no-op, not an error. Clients receive with `useRoomMessages(room, cb)` from `@pylonsync/react`; `useRoom` is the send side.
|
|
67
67
|
- **It's `db.useQueryOne`, not `useOne`.** Validators and field types have aliases: `v.bool`/`v.boolean`, `v.float`/`v.number`.
|
|
68
68
|
- **Use the supported file and scheduling APIs.** Files go through `<FileUpload>` and `/api/files/*`; an upload is readable by its uploader only, so pass `visibility: "public"` to `uploadFile` or `<FileUpload>` for files every visitor sees; there is no `ctx.files`. One-shot work uses `ctx.scheduler.runAfter`, `runAt`, or `cancel`; there is no `defineWorkflow` or `defineJob`. Recurring work uses `cron("0 * * * *", "fnName")` in `buildManifest({ crons: [...] })`, imported from `@pylonsync/sdk`. Make the target function `internal: true`. It runs with anonymous auth, but its own `ctx.db.*` calls are server-side and bypass policies. Use `ctx.auth.elevate({ admin: true, reason: "..." })`, with a mandatory reason, only when chaining another internal function through `ctx.scheduler`.
|
|
@@ -10,7 +10,7 @@ saved posts. Likes, comments, and follows update live in every open tab.
|
|
|
10
10
|
__RUN_DEV__
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
Open http://localhost:4321. The first visit loads
|
|
13
|
+
Open http://localhost:4321. The first visit loads eight demo people with
|
|
14
14
|
their posts, likes, comments, and follows, and gives you a guest profile.
|
|
15
15
|
Post a photo with Create, like with a double tap, and open a second tab to
|
|
16
16
|
watch counts change.
|
|
@@ -30,7 +30,7 @@ lib/seed.ts the demo people and posts
|
|
|
30
30
|
lib/site.ts the app's name
|
|
31
31
|
functions/ ensureProfile, updateProfile, createPost,
|
|
32
32
|
addComment, seedFeed
|
|
33
|
-
public/images/
|
|
33
|
+
public/images/posts/ demo photos
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
## How it works
|
|
@@ -55,7 +55,7 @@ public/images/ demo avatars and photos
|
|
|
55
55
|
move a guest's profile to the account on sign-in.
|
|
56
56
|
- **Scale:** `components/social/use-social.ts` reads every row, which is fine
|
|
57
57
|
for a demo. For a large network, query per screen with `where` and `limit`.
|
|
58
|
-
- **Launch:** delete `functions/seedFeed.ts`, `lib/seed.ts`, and
|
|
58
|
+
- **Launch:** delete `functions/seedFeed.ts`, `lib/seed.ts`, `tests/seed.test.ts`, and
|
|
59
59
|
`public/images/`, and remove the `seedFeed` call in `session.tsx`.
|
|
60
60
|
|
|
61
61
|
## Deploy
|
|
@@ -16,9 +16,12 @@
|
|
|
16
16
|
on <html>. */
|
|
17
17
|
:root {
|
|
18
18
|
--radius: 0.625rem;
|
|
19
|
-
/* The app's own colors: links and the follow button, and a liked heart.
|
|
20
|
-
|
|
21
|
-
--
|
|
19
|
+
/* The app's own colors: links and the follow button, and a liked heart.
|
|
20
|
+
They match the iOS app in apps/ios. */
|
|
21
|
+
--brand: #0095f6;
|
|
22
|
+
--like: #ff3040;
|
|
23
|
+
/* The ring around someone who posted in the last 24 hours. */
|
|
24
|
+
--story-ring: conic-gradient(from 180deg, #fcba45, #fa6133, #de2678, #8c38d9, #fcba45);
|
|
22
25
|
--background: oklch(1 0 0);
|
|
23
26
|
--foreground: oklch(0.141 0.005 285.823);
|
|
24
27
|
--card: oklch(1 0 0);
|
|
@@ -147,6 +150,10 @@
|
|
|
147
150
|
}
|
|
148
151
|
|
|
149
152
|
/* The wordmark. Instrument Serif is declared in app.ts. */
|
|
153
|
+
.bg-story-ring {
|
|
154
|
+
background: var(--story-ring);
|
|
155
|
+
}
|
|
156
|
+
|
|
150
157
|
.font-display {
|
|
151
158
|
font-family: var(--font-display, "Iowan Old Style", Georgia, serif);
|
|
152
159
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><defs><linearGradient id="g" x1="0" y1="1" x2="1" y2="0"><stop offset="0" stop-color="#fcba45"/><stop offset=".35" stop-color="#fa6133"/><stop offset=".7" stop-color="#de2678"/><stop offset="1" stop-color="#8c38d9"/></linearGradient></defs><rect width="64" height="64" rx="14" fill="url(#g)"/><text x="32" y="45" text-anchor="middle" font-family="Georgia, 'Times New Roman', serif" font-style="italic" font-weight="700" font-size="40" fill="#ffffff">G</text></svg>
|
|
@@ -4,7 +4,17 @@ import React from "react";
|
|
|
4
4
|
import { cn } from "@/lib/utils";
|
|
5
5
|
import type { Profile } from "@/lib/social";
|
|
6
6
|
|
|
7
|
-
/**
|
|
7
|
+
/** The same tints, in the same order, as the iOS app's AvatarView. */
|
|
8
|
+
const TINTS = ["#ff9500", "#ff2d55", "#af52de", "#5856d6", "#30b0c7", "#34c759", "#007aff", "#ff3b30"];
|
|
9
|
+
|
|
10
|
+
/** A stable tint per username. Same hash as the iOS app, so a person has one color everywhere. */
|
|
11
|
+
export function avatarTint(username: string): string {
|
|
12
|
+
let h = 0;
|
|
13
|
+
for (const ch of username) h = (Math.imul(h, 31) + (ch.codePointAt(0) ?? 0)) & 0xffff;
|
|
14
|
+
return TINTS[h % TINTS.length];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** A round profile photo, or the first letter of the name on a tinted circle. */
|
|
8
18
|
export function Avatar({
|
|
9
19
|
profile,
|
|
10
20
|
size = 32,
|
|
@@ -13,11 +23,12 @@ export function Avatar({
|
|
|
13
23
|
}: {
|
|
14
24
|
profile: Pick<Profile, "username" | "displayName" | "avatarUrl"> | null | undefined;
|
|
15
25
|
size?: number;
|
|
16
|
-
/**
|
|
26
|
+
/** The gradient ring, for people who posted in the last 24 hours. */
|
|
17
27
|
ring?: boolean;
|
|
18
28
|
className?: string;
|
|
19
29
|
}) {
|
|
20
30
|
const letter = (profile?.displayName || profile?.username || "?").trim().charAt(0).toUpperCase();
|
|
31
|
+
const tint = avatarTint(profile?.username ?? "");
|
|
21
32
|
const inner = profile?.avatarUrl ? (
|
|
22
33
|
<img
|
|
23
34
|
src={profile.avatarUrl}
|
|
@@ -29,22 +40,25 @@ export function Avatar({
|
|
|
29
40
|
/>
|
|
30
41
|
) : (
|
|
31
42
|
<span
|
|
32
|
-
|
|
33
|
-
|
|
43
|
+
aria-hidden
|
|
44
|
+
className="grid size-full place-items-center rounded-full font-semibold text-white"
|
|
45
|
+
style={{
|
|
46
|
+
fontSize: Math.max(10, Math.round(size * 0.42)),
|
|
47
|
+
background: `linear-gradient(to bottom, color-mix(in srgb, ${tint} 78%, white), ${tint})`,
|
|
48
|
+
}}
|
|
34
49
|
>
|
|
35
50
|
{letter}
|
|
36
51
|
</span>
|
|
37
52
|
);
|
|
53
|
+
const pad = ring ? Math.max(2, Math.round(size * 0.05)) : 0;
|
|
38
54
|
return (
|
|
39
55
|
<span
|
|
40
|
-
className={cn(
|
|
41
|
-
|
|
42
|
-
ring ? "bg-brand p-[2px]" : "",
|
|
43
|
-
className,
|
|
44
|
-
)}
|
|
45
|
-
style={{ width: size + (ring ? 4 : 0), height: size + (ring ? 4 : 0) }}
|
|
56
|
+
className={cn("inline-block shrink-0 rounded-full", ring ? "bg-story-ring" : "", className)}
|
|
57
|
+
style={{ width: size + pad * 4, height: size + pad * 4, padding: pad }}
|
|
46
58
|
>
|
|
47
|
-
<span className=
|
|
59
|
+
<span className="block size-full rounded-full bg-background" style={{ padding: pad }}>
|
|
60
|
+
{inner}
|
|
61
|
+
</span>
|
|
48
62
|
</span>
|
|
49
63
|
);
|
|
50
64
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { mutation } from "@pylonsync/functions";
|
|
2
|
-
import { shapeSeed } from "../lib/seed";
|
|
2
|
+
import { SEED_PROFILES, demoUserId, shapeSeed } from "../lib/seed";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Load the demo people, posts, likes, comments, and follows once. The feed
|
|
@@ -8,7 +8,7 @@ import { shapeSeed } from "../lib/seed";
|
|
|
8
8
|
*
|
|
9
9
|
* Public so an anonymous first visitor seeds it. `unsafe.insert` writes past
|
|
10
10
|
* the policies, which only accept the caller's own id, so the posts read as
|
|
11
|
-
*
|
|
11
|
+
* eight different people. It only writes lib/seed.ts. Delete this function
|
|
12
12
|
* and lib/seed.ts when you launch.
|
|
13
13
|
*/
|
|
14
14
|
export default mutation<Record<string, never>, { seeded: boolean }>({
|
|
@@ -16,7 +16,7 @@ export default mutation<Record<string, never>, { seeded: boolean }>({
|
|
|
16
16
|
args: {},
|
|
17
17
|
async handler(ctx) {
|
|
18
18
|
await ctx.db.advisoryLock("consumer_seed_feed");
|
|
19
|
-
const existing = await ctx.db.unsafe.query("Profile", { userId:
|
|
19
|
+
const existing = await ctx.db.unsafe.query("Profile", { userId: demoUserId(SEED_PROFILES[0].username), $limit: 1 });
|
|
20
20
|
if (existing.length > 0) return { seeded: false };
|
|
21
21
|
|
|
22
22
|
const seed = shapeSeed();
|