@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,285 @@
|
|
|
1
|
+
import Combine
|
|
2
|
+
import Foundation
|
|
3
|
+
import PylonClient
|
|
4
|
+
import PylonSync
|
|
5
|
+
import PylonSwiftUI
|
|
6
|
+
|
|
7
|
+
/// The live social graph. Five subscriptions (Profile, Post, Like, Comment,
|
|
8
|
+
/// Follow) on the sync engine, joined in memory. A row written by any
|
|
9
|
+
/// client reaches every open app over the engine's WebSocket, and every
|
|
10
|
+
/// view that reads this store redraws.
|
|
11
|
+
///
|
|
12
|
+
/// Writes go through server functions. The server writes the row, the
|
|
13
|
+
/// change comes back through sync, and the view updates. Likes and follows
|
|
14
|
+
/// also show at once through a pending state that clears when the synced
|
|
15
|
+
/// row arrives.
|
|
16
|
+
@MainActor
|
|
17
|
+
final class SocialStore: ObservableObject {
|
|
18
|
+
let engine: SyncEngine
|
|
19
|
+
let client: PylonClient
|
|
20
|
+
let media: MediaService
|
|
21
|
+
|
|
22
|
+
@Published var myProfileId: String?
|
|
23
|
+
@Published private(set) var profiles: [String: Profile] = [:]
|
|
24
|
+
/// Newest first.
|
|
25
|
+
@Published private(set) var posts: [Post] = []
|
|
26
|
+
@Published private(set) var likesByPost: [String: [Like]] = [:]
|
|
27
|
+
/// Oldest first within a post.
|
|
28
|
+
@Published private(set) var commentsByPost: [String: [Comment]] = [:]
|
|
29
|
+
@Published private(set) var follows: [Follow] = []
|
|
30
|
+
@Published private(set) var isLoading = true
|
|
31
|
+
/// Post ids the user liked or unliked whose synced row has not arrived yet.
|
|
32
|
+
@Published private(set) var pendingLikes: [String: Bool] = [:]
|
|
33
|
+
@Published private(set) var pendingFollows: [String: Bool] = [:]
|
|
34
|
+
|
|
35
|
+
private let profileQuery: PylonQuery<Profile>
|
|
36
|
+
private let postQuery: PylonQuery<Post>
|
|
37
|
+
private let likeQuery: PylonQuery<Like>
|
|
38
|
+
private let commentQuery: PylonQuery<Comment>
|
|
39
|
+
private let followQuery: PylonQuery<Follow>
|
|
40
|
+
private var subscriptions: Set<AnyCancellable> = []
|
|
41
|
+
|
|
42
|
+
init(engine: SyncEngine, client: PylonClient, media: MediaService) {
|
|
43
|
+
self.engine = engine
|
|
44
|
+
self.client = client
|
|
45
|
+
self.media = media
|
|
46
|
+
profileQuery = PylonQuery(engine: engine, entity: "Profile")
|
|
47
|
+
postQuery = PylonQuery(engine: engine, entity: "Post")
|
|
48
|
+
likeQuery = PylonQuery(engine: engine, entity: "Like")
|
|
49
|
+
commentQuery = PylonQuery(engine: engine, entity: "Comment")
|
|
50
|
+
followQuery = PylonQuery(engine: engine, entity: "Follow")
|
|
51
|
+
|
|
52
|
+
profileQuery.$rows
|
|
53
|
+
.sink { [weak self] rows in
|
|
54
|
+
self?.profiles = Dictionary(rows.map { ($0.id, $0) }, uniquingKeysWith: { _, new in new })
|
|
55
|
+
}
|
|
56
|
+
.store(in: &subscriptions)
|
|
57
|
+
postQuery.$rows
|
|
58
|
+
.sink { [weak self] rows in
|
|
59
|
+
self?.posts = rows.sorted { $0.createdAt > $1.createdAt }
|
|
60
|
+
}
|
|
61
|
+
.store(in: &subscriptions)
|
|
62
|
+
likeQuery.$rows
|
|
63
|
+
.sink { [weak self] rows in
|
|
64
|
+
guard let self else { return }
|
|
65
|
+
likesByPost = Dictionary(grouping: rows, by: \.postId)
|
|
66
|
+
pendingLikes = pendingLikes.filter { postId, liked in
|
|
67
|
+
self.syncedLike(postId) != liked
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
.store(in: &subscriptions)
|
|
71
|
+
commentQuery.$rows
|
|
72
|
+
.sink { [weak self] rows in
|
|
73
|
+
self?.commentsByPost = Dictionary(grouping: rows, by: \.postId)
|
|
74
|
+
.mapValues { $0.sorted { $0.createdAt < $1.createdAt } }
|
|
75
|
+
}
|
|
76
|
+
.store(in: &subscriptions)
|
|
77
|
+
followQuery.$rows
|
|
78
|
+
.sink { [weak self] rows in
|
|
79
|
+
guard let self else { return }
|
|
80
|
+
follows = rows
|
|
81
|
+
pendingFollows = pendingFollows.filter { profileId, following in
|
|
82
|
+
self.syncedFollow(profileId) != following
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
.store(in: &subscriptions)
|
|
86
|
+
postQuery.$loading
|
|
87
|
+
.sink { [weak self] loading in self?.isLoading = loading }
|
|
88
|
+
.store(in: &subscriptions)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// MARK: - Reads
|
|
92
|
+
|
|
93
|
+
var me: Profile? { myProfileId.flatMap { profiles[$0] } }
|
|
94
|
+
|
|
95
|
+
func profile(_ id: String) -> Profile? { profiles[id] }
|
|
96
|
+
|
|
97
|
+
func posts(by profileId: String) -> [Post] {
|
|
98
|
+
posts.filter { $0.authorId == profileId }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
func post(_ id: String) -> Post? { posts.first { $0.id == id } }
|
|
102
|
+
|
|
103
|
+
func isLiked(_ postId: String) -> Bool {
|
|
104
|
+
pendingLikes[postId] ?? syncedLike(postId)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
func likeCount(_ postId: String) -> Int {
|
|
108
|
+
let synced = likesByPost[postId]?.count ?? 0
|
|
109
|
+
guard let pending = pendingLikes[postId], pending != syncedLike(postId) else { return synced }
|
|
110
|
+
return synced + (pending ? 1 : -1)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// A liker to name under the photo: someone the user follows first, else the latest.
|
|
114
|
+
func featuredLiker(_ postId: String) -> Profile? {
|
|
115
|
+
let likes = (likesByPost[postId] ?? []).filter { $0.profileId != myProfileId }
|
|
116
|
+
let following = Set(followingIds(of: myProfileId))
|
|
117
|
+
let pick = likes.first { following.contains($0.profileId) } ?? likes.max { $0.createdAt < $1.createdAt }
|
|
118
|
+
return pick.flatMap { profiles[$0.profileId] }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
func comments(_ postId: String) -> [Comment] { commentsByPost[postId] ?? [] }
|
|
122
|
+
|
|
123
|
+
func followerIds(of profileId: String) -> [String] {
|
|
124
|
+
follows.filter { $0.followingId == profileId }.map(\.followerId)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
func followingIds(of profileId: String?) -> [String] {
|
|
128
|
+
guard let profileId else { return [] }
|
|
129
|
+
return follows.filter { $0.followerId == profileId }.map(\.followingId)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
func isFollowing(_ profileId: String) -> Bool {
|
|
133
|
+
pendingFollows[profileId] ?? syncedFollow(profileId)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
func followerCount(_ profileId: String) -> Int {
|
|
137
|
+
let synced = followerIds(of: profileId).count
|
|
138
|
+
guard let pending = pendingFollows[profileId], pending != syncedFollow(profileId) else { return synced }
|
|
139
|
+
return synced + (pending ? 1 : -1)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/// People with a post in the last 24 hours, the user first, then newest.
|
|
143
|
+
var storyAuthors: [Profile] {
|
|
144
|
+
let cutoff = Date.now.addingTimeInterval(-86_400)
|
|
145
|
+
var seen = Set<String>()
|
|
146
|
+
var out: [Profile] = []
|
|
147
|
+
for post in posts where post.date > cutoff {
|
|
148
|
+
guard !seen.contains(post.authorId), post.authorId != myProfileId,
|
|
149
|
+
let author = profiles[post.authorId] else { continue }
|
|
150
|
+
seen.insert(post.authorId)
|
|
151
|
+
out.append(author)
|
|
152
|
+
}
|
|
153
|
+
return out
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
func hasRecentPost(_ profileId: String) -> Bool {
|
|
157
|
+
let cutoff = Date.now.addingTimeInterval(-86_400)
|
|
158
|
+
return posts.contains { $0.authorId == profileId && $0.date > cutoff }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
func recentPosts(by profileId: String) -> [Post] {
|
|
162
|
+
let cutoff = Date.now.addingTimeInterval(-86_400)
|
|
163
|
+
return posts.filter { $0.authorId == profileId && $0.date > cutoff }.reversed()
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/// Likes, comments, and follows aimed at the user, newest first.
|
|
167
|
+
var activity: [ActivityItem] {
|
|
168
|
+
guard let me = myProfileId else { return [] }
|
|
169
|
+
let mine = Set(posts.filter { $0.authorId == me }.map(\.id))
|
|
170
|
+
var items: [ActivityItem] = []
|
|
171
|
+
for postId in mine {
|
|
172
|
+
for like in likesByPost[postId] ?? [] where like.profileId != me {
|
|
173
|
+
items.append(ActivityItem(id: "like-\(like.id)", kind: .like, actorId: like.profileId, postId: postId, text: nil, createdAt: like.createdAt))
|
|
174
|
+
}
|
|
175
|
+
for comment in commentsByPost[postId] ?? [] where comment.profileId != me {
|
|
176
|
+
items.append(ActivityItem(id: "comment-\(comment.id)", kind: .comment, actorId: comment.profileId, postId: postId, text: comment.text, createdAt: comment.createdAt))
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
for follow in follows where follow.followingId == me {
|
|
180
|
+
items.append(ActivityItem(id: "follow-\(follow.id)", kind: .follow, actorId: follow.followerId, postId: nil, text: nil, createdAt: follow.createdAt))
|
|
181
|
+
}
|
|
182
|
+
return items.sorted { $0.createdAt > $1.createdAt }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private func syncedLike(_ postId: String) -> Bool {
|
|
186
|
+
guard let me = myProfileId else { return false }
|
|
187
|
+
return likesByPost[postId]?.contains { $0.profileId == me } ?? false
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private func syncedFollow(_ profileId: String) -> Bool {
|
|
191
|
+
guard let me = myProfileId else { return false }
|
|
192
|
+
return follows.contains { $0.followerId == me && $0.followingId == profileId }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// MARK: - Writes
|
|
196
|
+
|
|
197
|
+
func toggleLike(_ postId: String) {
|
|
198
|
+
let next = !isLiked(postId)
|
|
199
|
+
setLike(postId, liked: next)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/// Double tap only ever likes.
|
|
203
|
+
func like(_ postId: String) {
|
|
204
|
+
if !isLiked(postId) { setLike(postId, liked: true) }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/// Records the tap at once, then sends the wanted state. The server call
|
|
208
|
+
/// sets the state rather than flipping it, so taps that arrive out of
|
|
209
|
+
/// order still end in the last tap's state.
|
|
210
|
+
private func setLike(_ postId: String, liked: Bool) {
|
|
211
|
+
pendingLikes[postId] = liked
|
|
212
|
+
Task {
|
|
213
|
+
do {
|
|
214
|
+
_ = try await client.callFn("setLike", args: SetLikeArgs(postId: postId, liked: liked), as: SetLikeResult.self)
|
|
215
|
+
} catch {
|
|
216
|
+
if pendingLikes[postId] == liked { pendingLikes[postId] = nil }
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
func toggleFollow(_ profileId: String) {
|
|
222
|
+
let following = !isFollowing(profileId)
|
|
223
|
+
pendingFollows[profileId] = following
|
|
224
|
+
Task {
|
|
225
|
+
do {
|
|
226
|
+
_ = try await client.callFn("setFollow", args: SetFollowArgs(profileId: profileId, following: following), as: SetFollowResult.self)
|
|
227
|
+
} catch {
|
|
228
|
+
if pendingFollows[profileId] == following { pendingFollows[profileId] = nil }
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
func addComment(_ postId: String, text: String) async throws {
|
|
234
|
+
_ = try await client.callFn("addComment", args: AddCommentArgs(postId: postId, text: text), as: IdResult.self)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
func deletePost(_ postId: String) async throws {
|
|
238
|
+
_ = try await client.callFn("deletePost", args: DeletePostArgs(id: postId), as: IdResult.self)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
func createPost(jpeg: Data, caption: String) async throws {
|
|
242
|
+
let imageUrl = try await media.uploadPublicJPEG(jpeg)
|
|
243
|
+
_ = try await client.callFn("createPost", args: CreatePostArgs(imageUrl: imageUrl, caption: caption), as: IdResult.self)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
func saveProfile(handle: String, displayName: String, bio: String, avatarJPEG: Data?) async throws -> Profile {
|
|
247
|
+
var avatarUrl: String?
|
|
248
|
+
if let avatarJPEG {
|
|
249
|
+
avatarUrl = try await media.uploadPublicJPEG(avatarJPEG)
|
|
250
|
+
}
|
|
251
|
+
return try await client.callFn(
|
|
252
|
+
"upsertProfile",
|
|
253
|
+
args: UpsertProfileArgs(handle: handle, displayName: displayName, bio: bio, avatarUrl: avatarUrl),
|
|
254
|
+
as: Profile.self
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
struct ActivityItem: Identifiable, Hashable {
|
|
260
|
+
enum Kind { case like, comment, follow }
|
|
261
|
+
let id: String
|
|
262
|
+
let kind: Kind
|
|
263
|
+
let actorId: String
|
|
264
|
+
let postId: String?
|
|
265
|
+
let text: String?
|
|
266
|
+
let createdAt: String
|
|
267
|
+
|
|
268
|
+
var date: Date { Timestamp.parse(createdAt) }
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/// A readable sentence for an error from a server function or the network.
|
|
272
|
+
func friendlyMessage(_ error: Error) -> String {
|
|
273
|
+
if let pylon = error as? PylonError {
|
|
274
|
+
switch pylon {
|
|
275
|
+
case .http(_, _, let message?) where !message.isEmpty:
|
|
276
|
+
return message.prefix(1).uppercased() + message.dropFirst()
|
|
277
|
+
case .transport:
|
|
278
|
+
return "Cannot reach the server. Check your connection and try again."
|
|
279
|
+
default:
|
|
280
|
+
return "Something went wrong. Try again."
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if let local = error as? LocalizedError, let text = local.errorDescription { return text }
|
|
284
|
+
return "Something went wrong. Try again."
|
|
285
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
/// Full-screen playback of each person's posts from the last 24 hours.
|
|
4
|
+
/// Every photo shows for five seconds. Tap the right side for the next
|
|
5
|
+
/// photo, the left side for the previous one, or swipe down to close.
|
|
6
|
+
struct StoryViewer: View {
|
|
7
|
+
@EnvironmentObject private var social: SocialStore
|
|
8
|
+
@Environment(\.dismiss) private var dismiss
|
|
9
|
+
|
|
10
|
+
let authors: [Profile]
|
|
11
|
+
@State var authorIndex: Int
|
|
12
|
+
@State private var postIndex = 0
|
|
13
|
+
@State private var progress: Double = 0
|
|
14
|
+
@State private var dragOffset: CGFloat = 0
|
|
15
|
+
/// Bumped to replay the current photo from the start.
|
|
16
|
+
@State private var restarts = 0
|
|
17
|
+
|
|
18
|
+
private let duration: Double = 5
|
|
19
|
+
private let tick: Double = 0.05
|
|
20
|
+
|
|
21
|
+
init(authors: [Profile], startIndex: Int) {
|
|
22
|
+
self.authors = authors
|
|
23
|
+
_authorIndex = State(initialValue: startIndex)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private var author: Profile? { authors.indices.contains(authorIndex) ? authors[authorIndex] : nil }
|
|
27
|
+
private var posts: [Post] { author.map { social.recentPosts(by: $0.id) } ?? [] }
|
|
28
|
+
private var post: Post? { posts.indices.contains(postIndex) ? posts[postIndex] : nil }
|
|
29
|
+
|
|
30
|
+
var body: some View {
|
|
31
|
+
ZStack {
|
|
32
|
+
Color.black.ignoresSafeArea()
|
|
33
|
+
if let post {
|
|
34
|
+
let url = social.media.url(for: post.imageUrl)
|
|
35
|
+
RemoteImage(url: url)
|
|
36
|
+
.blur(radius: 40)
|
|
37
|
+
.opacity(0.6)
|
|
38
|
+
.ignoresSafeArea()
|
|
39
|
+
Color.clear
|
|
40
|
+
.aspectRatio(4 / 5, contentMode: .fit)
|
|
41
|
+
.overlay { RemoteImage(url: url) }
|
|
42
|
+
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
|
|
43
|
+
.id(post.id)
|
|
44
|
+
}
|
|
45
|
+
HStack(spacing: 0) {
|
|
46
|
+
Color.clear.contentShape(Rectangle()).onTapGesture(perform: previous)
|
|
47
|
+
Color.clear.contentShape(Rectangle()).onTapGesture(perform: next)
|
|
48
|
+
}
|
|
49
|
+
VStack(spacing: 10) {
|
|
50
|
+
progressBars
|
|
51
|
+
topBar
|
|
52
|
+
Spacer()
|
|
53
|
+
if let caption = post?.caption, !caption.isEmpty {
|
|
54
|
+
Text(caption)
|
|
55
|
+
.font(.subheadline)
|
|
56
|
+
.foregroundStyle(.white)
|
|
57
|
+
.multilineTextAlignment(.leading)
|
|
58
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
59
|
+
.padding(14)
|
|
60
|
+
.background(.black.opacity(0.35), in: RoundedRectangle(cornerRadius: 14, style: .continuous))
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
.padding(.horizontal, 12)
|
|
64
|
+
.padding(.top, 8)
|
|
65
|
+
}
|
|
66
|
+
.offset(y: dragOffset)
|
|
67
|
+
.gesture(
|
|
68
|
+
DragGesture()
|
|
69
|
+
.onChanged { value in dragOffset = max(0, value.translation.height) }
|
|
70
|
+
.onEnded { value in
|
|
71
|
+
if value.translation.height > 120 { dismiss() } else {
|
|
72
|
+
withAnimation(.spring) { dragOffset = 0 }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
.statusBarHidden()
|
|
77
|
+
.task(id: "\(authorIndex)-\(postIndex)-\(restarts)") { await play() }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private var progressBars: some View {
|
|
81
|
+
HStack(spacing: 4) {
|
|
82
|
+
ForEach(posts.indices, id: \.self) { index in
|
|
83
|
+
GeometryReader { geo in
|
|
84
|
+
Capsule().fill(.white.opacity(0.35))
|
|
85
|
+
.overlay(alignment: .leading) {
|
|
86
|
+
Capsule().fill(.white)
|
|
87
|
+
.frame(width: geo.size.width * fill(for: index))
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
.frame(height: 2.5)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private var topBar: some View {
|
|
96
|
+
HStack(spacing: 10) {
|
|
97
|
+
AvatarView(profile: author, size: 32)
|
|
98
|
+
Text(author?.handle ?? "")
|
|
99
|
+
.font(.subheadline.weight(.semibold))
|
|
100
|
+
if let post {
|
|
101
|
+
Text(Timestamp.short(post.date))
|
|
102
|
+
.font(.subheadline)
|
|
103
|
+
.opacity(0.7)
|
|
104
|
+
}
|
|
105
|
+
Spacer()
|
|
106
|
+
Button { dismiss() } label: {
|
|
107
|
+
Image(systemName: "xmark")
|
|
108
|
+
.font(.system(size: 20, weight: .semibold))
|
|
109
|
+
.frame(width: 40, height: 40)
|
|
110
|
+
.contentShape(Rectangle())
|
|
111
|
+
}
|
|
112
|
+
.accessibilityLabel("Close")
|
|
113
|
+
}
|
|
114
|
+
.foregroundStyle(.white)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private func fill(for index: Int) -> Double {
|
|
118
|
+
if index < postIndex { return 1 }
|
|
119
|
+
if index == postIndex { return progress }
|
|
120
|
+
return 0
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private func play() async {
|
|
124
|
+
progress = 0
|
|
125
|
+
let steps = Int(duration / tick)
|
|
126
|
+
for _ in 0..<steps {
|
|
127
|
+
try? await Task.sleep(for: .seconds(tick))
|
|
128
|
+
if Task.isCancelled { return }
|
|
129
|
+
progress += tick / duration
|
|
130
|
+
}
|
|
131
|
+
next()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private func next() {
|
|
135
|
+
if postIndex + 1 < posts.count {
|
|
136
|
+
postIndex += 1
|
|
137
|
+
} else if authorIndex + 1 < authors.count {
|
|
138
|
+
authorIndex += 1
|
|
139
|
+
postIndex = 0
|
|
140
|
+
} else {
|
|
141
|
+
dismiss()
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private func previous() {
|
|
146
|
+
if postIndex > 0 {
|
|
147
|
+
postIndex -= 1
|
|
148
|
+
} else if authorIndex > 0 {
|
|
149
|
+
authorIndex -= 1
|
|
150
|
+
postIndex = 0
|
|
151
|
+
} else {
|
|
152
|
+
restarts += 1
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
enum Theme {
|
|
4
|
+
/// Filled buttons, links, and badges. Fixed so the black tab bar tint
|
|
5
|
+
/// does not change it.
|
|
6
|
+
static let accent = Color(red: 0.0, green: 0.584, blue: 0.965)
|
|
7
|
+
/// The like color.
|
|
8
|
+
static let heart = Color(red: 1.0, green: 0.19, blue: 0.25)
|
|
9
|
+
/// The ring around an avatar with a post from the last 24 hours.
|
|
10
|
+
static let storyRing = AngularGradient(
|
|
11
|
+
colors: [
|
|
12
|
+
Color(red: 0.99, green: 0.73, blue: 0.27),
|
|
13
|
+
Color(red: 0.98, green: 0.38, blue: 0.20),
|
|
14
|
+
Color(red: 0.87, green: 0.15, blue: 0.47),
|
|
15
|
+
Color(red: 0.55, green: 0.22, blue: 0.85),
|
|
16
|
+
Color(red: 0.99, green: 0.73, blue: 0.27),
|
|
17
|
+
],
|
|
18
|
+
center: .center
|
|
19
|
+
)
|
|
20
|
+
static let hairline = Color(uiColor: .separator)
|
|
21
|
+
static let fieldFill = Color(uiColor: .secondarySystemBackground)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// The app name in the feed header and on the sign-in screen.
|
|
25
|
+
struct Wordmark: View {
|
|
26
|
+
var size: CGFloat = 30
|
|
27
|
+
|
|
28
|
+
var body: some View {
|
|
29
|
+
Text("__APP_NAME__")
|
|
30
|
+
.font(.system(size: size, weight: .semibold, design: .serif))
|
|
31
|
+
.italic()
|
|
32
|
+
.tracking(-0.5)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// A full-width filled button for the main action on a screen.
|
|
37
|
+
struct PrimaryButtonStyle: ButtonStyle {
|
|
38
|
+
var isLoading = false
|
|
39
|
+
|
|
40
|
+
func makeBody(configuration: Configuration) -> some View {
|
|
41
|
+
ZStack {
|
|
42
|
+
configuration.label.opacity(isLoading ? 0 : 1)
|
|
43
|
+
if isLoading { ProgressView().tint(.white) }
|
|
44
|
+
}
|
|
45
|
+
.font(.body.weight(.semibold))
|
|
46
|
+
.foregroundStyle(.white)
|
|
47
|
+
.frame(maxWidth: .infinity, minHeight: 50)
|
|
48
|
+
.background(Theme.accent, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
|
|
49
|
+
.opacity(configuration.isPressed ? 0.8 : 1)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// A compact button for profile actions: Edit profile, Follow, Following.
|
|
54
|
+
struct ProfileButtonStyle: ButtonStyle {
|
|
55
|
+
var prominent = false
|
|
56
|
+
|
|
57
|
+
func makeBody(configuration: Configuration) -> some View {
|
|
58
|
+
configuration.label
|
|
59
|
+
.font(.subheadline.weight(.semibold))
|
|
60
|
+
.foregroundStyle(prominent ? Color.white : Color.primary)
|
|
61
|
+
.frame(maxWidth: .infinity, minHeight: 34)
|
|
62
|
+
.background(
|
|
63
|
+
prominent ? Theme.accent : Theme.fieldFill,
|
|
64
|
+
in: RoundedRectangle(cornerRadius: 9, style: .continuous)
|
|
65
|
+
)
|
|
66
|
+
.opacity(configuration.isPressed ? 0.7 : 1)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// A rounded text field on the sign-in and profile screens.
|
|
71
|
+
struct FieldStyle: ViewModifier {
|
|
72
|
+
func body(content: Content) -> some View {
|
|
73
|
+
content
|
|
74
|
+
.padding(.horizontal, 14)
|
|
75
|
+
.frame(minHeight: 50)
|
|
76
|
+
.background(Theme.fieldFill, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
extension View {
|
|
81
|
+
func fieldStyle() -> some View { modifier(FieldStyle()) }
|
|
82
|
+
}
|
package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift
CHANGED
|
@@ -1,57 +1,14 @@
|
|
|
1
1
|
import SwiftUI
|
|
2
|
-
import PylonClient
|
|
3
|
-
import PylonSync
|
|
4
2
|
|
|
5
3
|
@main
|
|
6
4
|
struct __APP_NAME_PASCAL__App: App {
|
|
7
|
-
@StateObject private var
|
|
5
|
+
@StateObject private var app = AppModel()
|
|
8
6
|
|
|
9
7
|
var body: some Scene {
|
|
10
8
|
WindowGroup {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
.environmentObject(session)
|
|
15
|
-
} else {
|
|
16
|
-
ProgressView()
|
|
17
|
-
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
.task { await session.bootIfNeeded() }
|
|
9
|
+
RootView()
|
|
10
|
+
.environmentObject(app)
|
|
11
|
+
.task { await app.boot() }
|
|
21
12
|
}
|
|
22
13
|
}
|
|
23
14
|
}
|
|
24
|
-
|
|
25
|
-
@MainActor
|
|
26
|
-
final class AppSession: ObservableObject {
|
|
27
|
-
let client: PylonClient
|
|
28
|
-
@Published private(set) var engine: SyncEngine?
|
|
29
|
-
@Published var myProfileId: String?
|
|
30
|
-
|
|
31
|
-
init() {
|
|
32
|
-
let baseURLString = ProcessInfo.processInfo.environment["PYLON_BASE_URL"]
|
|
33
|
-
?? "http://localhost:4321"
|
|
34
|
-
guard let url = URL(string: baseURLString) else {
|
|
35
|
-
fatalError("Invalid PYLON_BASE_URL: \(baseURLString)")
|
|
36
|
-
}
|
|
37
|
-
self.client = PylonClient(baseURL: url, appName: "__APP_NAME_SNAKE__")
|
|
38
|
-
self.myProfileId = UserDefaults.standard.string(forKey: "myProfileId")
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
func bootIfNeeded() async {
|
|
42
|
-
guard engine == nil else { return }
|
|
43
|
-
let baseURLString = ProcessInfo.processInfo.environment["PYLON_BASE_URL"]
|
|
44
|
-
?? "http://localhost:4321"
|
|
45
|
-
guard let url = URL(string: baseURLString) else { return }
|
|
46
|
-
let config = SyncEngineConfig(baseURL: url, appName: "__APP_NAME_SNAKE__")
|
|
47
|
-
let engine = await SyncEngine(config: config, client: client)
|
|
48
|
-
await engine.start()
|
|
49
|
-
self.engine = engine
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
func setMyProfileId(_ id: String?) {
|
|
53
|
-
myProfileId = id
|
|
54
|
-
if let id { UserDefaults.standard.set(id, forKey: "myProfileId") }
|
|
55
|
-
else { UserDefaults.standard.removeObject(forKey: "myProfileId") }
|
|
56
|
-
}
|
|
57
|
-
}
|
|
@@ -3,18 +3,24 @@
|
|
|
3
3
|
# Materialize the Xcode project once with:
|
|
4
4
|
# brew install xcodegen
|
|
5
5
|
# xcodegen generate
|
|
6
|
-
# Then open __APP_NAME_PASCAL__.xcodeproj.
|
|
6
|
+
# Then open __APP_NAME_PASCAL__.xcodeproj. Run xcodegen again after you add
|
|
7
|
+
# or remove Swift files.
|
|
8
|
+
#
|
|
9
|
+
# The app talks to the Pylon backend in apps/api. Start it with `pylon dev`
|
|
10
|
+
# there. The simulator reaches http://localhost:4321; on a device, set
|
|
11
|
+
# PYLON_BASE_URL in the scheme's environment to your Mac's LAN address or
|
|
12
|
+
# your deployed API.
|
|
7
13
|
|
|
8
14
|
name: __APP_NAME_PASCAL__
|
|
9
15
|
options:
|
|
10
16
|
bundleIdPrefix: com.example
|
|
11
17
|
deploymentTarget:
|
|
12
|
-
iOS: "
|
|
18
|
+
iOS: "17.0"
|
|
13
19
|
|
|
14
20
|
packages:
|
|
15
21
|
pylon:
|
|
16
22
|
url: https://github.com/pylonsync/pylon.git
|
|
17
|
-
from: "
|
|
23
|
+
from: "__PYLON_VERSION__"
|
|
18
24
|
|
|
19
25
|
targets:
|
|
20
26
|
__APP_NAME_PASCAL__:
|
|
@@ -25,15 +31,13 @@ targets:
|
|
|
25
31
|
settings:
|
|
26
32
|
base:
|
|
27
33
|
GENERATE_INFOPLIST_FILE: YES
|
|
34
|
+
INFOPLIST_KEY_CFBundleDisplayName: "__APP_NAME__"
|
|
28
35
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation: YES
|
|
29
36
|
INFOPLIST_KEY_UILaunchScreen_Generation: YES
|
|
30
|
-
INFOPLIST_KEY_UISupportedInterfaceOrientations: "UIInterfaceOrientationPortrait
|
|
31
|
-
TARGETED_DEVICE_FAMILY: "1
|
|
32
|
-
SWIFT_VERSION: "5.
|
|
37
|
+
INFOPLIST_KEY_UISupportedInterfaceOrientations: "UIInterfaceOrientationPortrait"
|
|
38
|
+
TARGETED_DEVICE_FAMILY: "1"
|
|
39
|
+
SWIFT_VERSION: "5.0"
|
|
33
40
|
ENABLE_PREVIEWS: YES
|
|
34
|
-
# __APP_NAME__ talks to a Pylon backend on localhost during dev.
|
|
35
|
-
# The simulator can reach localhost directly; on a physical device
|
|
36
|
-
# set PYLON_BASE_URL to your machine's LAN IP.
|
|
37
41
|
INFOPLIST_KEY_NSAppTransportSecurity: "{NSAllowsLocalNetworking = YES;}"
|
|
38
42
|
dependencies:
|
|
39
43
|
- package: pylon
|
|
@@ -10,3 +10,10 @@ PYLON_OWNER_EMAIL=you@yourshop.com
|
|
|
10
10
|
# ── Site URL (optional) ──────────────────────────────────────────────────────
|
|
11
11
|
# Used by robots.txt + sitemap.xml. Point it at your real domain in production.
|
|
12
12
|
# SITE_URL=https://yourshop.com
|
|
13
|
+
|
|
14
|
+
# ── Demo data ────────────────────────────────────────────────────────────────
|
|
15
|
+
# `pylon dev` fills an empty booking book with fictional appointments. Seeding
|
|
16
|
+
# runs only under `pylon dev` (it checks PYLON_DEV_MODE and
|
|
17
|
+
# PYLON_DEV_WATCH_DIR), so `pylon start`, Docker, and Pylon Cloud deploys never
|
|
18
|
+
# seed. Set 0 to start empty in development. Never set 1 on a deploy.
|
|
19
|
+
# PYLON_DEMO_DATA=0
|
|
@@ -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`.
|