@pylonsync/create-pylon 0.3.274 → 0.3.276
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 +80 -0
- package/package.json +1 -1
- package/templates/ARCHETYPES.md +339 -0
- package/templates/agency/.env.example +12 -0
- package/templates/agency/AGENTS.md +61 -0
- package/templates/agency/README.md +90 -0
- package/templates/agency/app/auth-form.tsx +129 -0
- package/templates/agency/app/contact-form.tsx +258 -0
- package/templates/agency/app/dashboard/dashboard-client.tsx +1440 -0
- package/templates/agency/app/dashboard/page.tsx +70 -0
- package/templates/agency/app/error.tsx +26 -0
- package/templates/agency/app/globals.css +148 -0
- package/templates/agency/app/layout.tsx +174 -0
- package/templates/agency/app/login/page.tsx +39 -0
- package/templates/agency/app/not-found.tsx +19 -0
- package/templates/agency/app/page.tsx +249 -0
- package/templates/agency/app/robots.ts +12 -0
- package/templates/agency/app/seeder.tsx +26 -0
- package/templates/agency/app/sitemap.ts +9 -0
- package/templates/agency/app/work/[slug]/page.tsx +182 -0
- package/templates/agency/app/work/page.tsx +83 -0
- package/templates/agency/app.ts +284 -0
- package/templates/agency/components/marketing.tsx +187 -0
- package/templates/agency/components/section-scroller.tsx +35 -0
- package/templates/agency/components/ui/button.tsx +56 -0
- package/templates/agency/components/ui/card.tsx +90 -0
- package/templates/agency/components.json +20 -0
- package/templates/agency/functions/bookInquiry.ts +42 -0
- package/templates/agency/functions/clientsForOwner.ts +27 -0
- package/templates/agency/functions/declineInquiry.ts +41 -0
- package/templates/agency/functions/deleteClient.ts +27 -0
- package/templates/agency/functions/deleteInvoice.ts +19 -0
- package/templates/agency/functions/deleteProject.ts +20 -0
- package/templates/agency/functions/inquiriesForOwner.ts +31 -0
- package/templates/agency/functions/invoicesForOwner.ts +27 -0
- package/templates/agency/functions/seedCapacity.ts +26 -0
- package/templates/agency/functions/seedProjects.ts +41 -0
- package/templates/agency/functions/seedStudioBackoffice.ts +74 -0
- package/templates/agency/functions/setCapacity.ts +32 -0
- package/templates/agency/functions/setInvoiceStatus.ts +27 -0
- package/templates/agency/functions/setProjectFlags.ts +35 -0
- package/templates/agency/functions/submitInquiry.ts +55 -0
- package/templates/agency/functions/upsertClient.ts +73 -0
- package/templates/agency/functions/upsertInvoice.ts +113 -0
- package/templates/agency/functions/upsertProject.ts +97 -0
- package/templates/agency/gitignore +10 -0
- package/templates/agency/lib/agency.ts +189 -0
- package/templates/agency/lib/invoice-pdf.tsx +174 -0
- package/templates/agency/lib/owner.ts +26 -0
- package/templates/agency/lib/site.config.ts +418 -0
- package/templates/agency/lib/utils.ts +10 -0
- package/templates/agency/package.json +35 -0
- package/templates/agency/tsconfig.json +18 -0
- package/templates/ai-chat/.env.example +33 -0
- package/templates/ai-chat/AGENTS.md +61 -0
- package/templates/ai-chat/README.md +99 -0
- package/templates/ai-chat/app/auth-form.tsx +124 -0
- package/templates/ai-chat/app/chat-client.tsx +727 -0
- package/templates/ai-chat/app/error.tsx +26 -0
- package/templates/ai-chat/app/globals.css +148 -0
- package/templates/ai-chat/app/layout.tsx +75 -0
- package/templates/ai-chat/app/login/page.tsx +39 -0
- package/templates/ai-chat/app/not-found.tsx +19 -0
- package/templates/ai-chat/app/page.tsx +23 -0
- package/templates/ai-chat/app.ts +121 -0
- package/templates/ai-chat/components.json +20 -0
- package/templates/ai-chat/functions/deleteConversation.ts +33 -0
- package/templates/ai-chat/gitignore +10 -0
- package/templates/ai-chat/lib/site.config.ts +103 -0
- package/templates/ai-chat/lib/utils.ts +10 -0
- package/templates/ai-chat/package.json +34 -0
- package/templates/ai-chat/tsconfig.json +18 -0
- package/templates/ai-studio/.env.example +19 -0
- package/templates/ai-studio/AGENTS.md +61 -0
- package/templates/ai-studio/README.md +83 -0
- package/templates/ai-studio/app/auth-form.tsx +124 -0
- package/templates/ai-studio/app/error.tsx +26 -0
- package/templates/ai-studio/app/globals.css +148 -0
- package/templates/ai-studio/app/layout.tsx +75 -0
- package/templates/ai-studio/app/login/page.tsx +39 -0
- package/templates/ai-studio/app/not-found.tsx +19 -0
- package/templates/ai-studio/app/page.tsx +34 -0
- package/templates/ai-studio/app/studio-client.tsx +357 -0
- package/templates/ai-studio/app.ts +108 -0
- package/templates/ai-studio/components.json +20 -0
- package/templates/ai-studio/functions/_getGeneration.ts +25 -0
- package/templates/ai-studio/functions/_updateGeneration.ts +37 -0
- package/templates/ai-studio/functions/generate.ts +42 -0
- package/templates/ai-studio/functions/pollGeneration.ts +134 -0
- package/templates/ai-studio/gitignore +10 -0
- package/templates/ai-studio/lib/site.config.ts +80 -0
- package/templates/ai-studio/lib/studio.ts +52 -0
- package/templates/ai-studio/lib/utils.ts +10 -0
- package/templates/ai-studio/package.json +34 -0
- package/templates/ai-studio/tsconfig.json +18 -0
- package/templates/creator/.env.example +12 -0
- package/templates/creator/AGENTS.md +61 -0
- package/templates/creator/README.md +67 -0
- package/templates/creator/app/auth-form.tsx +129 -0
- package/templates/creator/app/dashboard/dashboard-client.tsx +297 -0
- package/templates/creator/app/dashboard/page.tsx +70 -0
- package/templates/creator/app/error.tsx +26 -0
- package/templates/creator/app/globals.css +148 -0
- package/templates/creator/app/layout.tsx +160 -0
- package/templates/creator/app/login/page.tsx +39 -0
- package/templates/creator/app/newsletter-signup.tsx +162 -0
- package/templates/creator/app/not-found.tsx +19 -0
- package/templates/creator/app/page.tsx +160 -0
- package/templates/creator/app/robots.ts +12 -0
- package/templates/creator/app/sitemap.ts +9 -0
- package/templates/creator/app.ts +134 -0
- package/templates/creator/components/marketing.tsx +148 -0
- package/templates/creator/components/section-scroller.tsx +35 -0
- package/templates/creator/components/ui/button.tsx +56 -0
- package/templates/creator/components/ui/card.tsx +90 -0
- package/templates/creator/components.json +20 -0
- package/templates/creator/functions/subscribe.ts +82 -0
- package/templates/creator/functions/subscriberStats.ts +75 -0
- package/templates/creator/gitignore +10 -0
- package/templates/creator/lib/owner.ts +26 -0
- package/templates/creator/lib/site.config.ts +173 -0
- package/templates/creator/lib/stats.ts +30 -0
- package/templates/creator/lib/utils.ts +10 -0
- package/templates/creator/package.json +34 -0
- package/templates/creator/tsconfig.json +18 -0
- package/templates/default/app/layout.tsx +26 -27
- package/templates/default/app/page.tsx +90 -274
- package/templates/default/lib/products.ts +9 -122
- package/templates/default/lib/site.config.ts +739 -0
- package/templates/default/lib/site.ts +14 -261
- package/templates/directory/.env.example +12 -0
- package/templates/directory/AGENTS.md +61 -0
- package/templates/directory/README.md +80 -0
- package/templates/directory/app/auth-form.tsx +129 -0
- package/templates/directory/app/dashboard/dashboard-client.tsx +205 -0
- package/templates/directory/app/dashboard/page.tsx +70 -0
- package/templates/directory/app/directory-browse.tsx +328 -0
- package/templates/directory/app/error.tsx +26 -0
- package/templates/directory/app/globals.css +148 -0
- package/templates/directory/app/layout.tsx +171 -0
- package/templates/directory/app/login/page.tsx +39 -0
- package/templates/directory/app/not-found.tsx +19 -0
- package/templates/directory/app/page.tsx +50 -0
- package/templates/directory/app/robots.ts +12 -0
- package/templates/directory/app/sitemap.ts +9 -0
- package/templates/directory/app/submit/page.tsx +30 -0
- package/templates/directory/app/submit-form.tsx +151 -0
- package/templates/directory/app.ts +146 -0
- package/templates/directory/components/marketing.tsx +148 -0
- package/templates/directory/components/section-scroller.tsx +35 -0
- package/templates/directory/components/ui/button.tsx +56 -0
- package/templates/directory/components/ui/card.tsx +90 -0
- package/templates/directory/components.json +20 -0
- package/templates/directory/functions/approveSubmission.ts +45 -0
- package/templates/directory/functions/rejectSubmission.ts +20 -0
- package/templates/directory/functions/seedListings.ts +33 -0
- package/templates/directory/functions/submissionsForOwner.ts +29 -0
- package/templates/directory/functions/submitListing.ts +63 -0
- package/templates/directory/functions/upvote.ts +24 -0
- package/templates/directory/gitignore +10 -0
- package/templates/directory/lib/directory.ts +45 -0
- package/templates/directory/lib/owner.ts +26 -0
- package/templates/directory/lib/site.config.ts +130 -0
- package/templates/directory/lib/utils.ts +10 -0
- package/templates/directory/package.json +34 -0
- package/templates/directory/tsconfig.json +18 -0
- package/templates/local-service/.env.example +12 -0
- package/templates/local-service/AGENTS.md +61 -0
- package/templates/local-service/README.md +82 -0
- package/templates/local-service/app/auth-form.tsx +129 -0
- package/templates/local-service/app/booking-widget.tsx +399 -0
- package/templates/local-service/app/dashboard/dashboard-client.tsx +304 -0
- package/templates/local-service/app/dashboard/page.tsx +63 -0
- package/templates/local-service/app/error.tsx +26 -0
- package/templates/local-service/app/globals.css +148 -0
- package/templates/local-service/app/layout.tsx +151 -0
- package/templates/local-service/app/login/page.tsx +39 -0
- package/templates/local-service/app/not-found.tsx +19 -0
- package/templates/local-service/app/page.tsx +233 -0
- package/templates/local-service/app/robots.ts +12 -0
- package/templates/local-service/app/sitemap.ts +9 -0
- package/templates/local-service/app.ts +131 -0
- package/templates/local-service/components/marketing.tsx +162 -0
- package/templates/local-service/components/section-scroller.tsx +35 -0
- package/templates/local-service/components/ui/button.tsx +56 -0
- package/templates/local-service/components/ui/card.tsx +90 -0
- package/templates/local-service/components.json +20 -0
- package/templates/local-service/functions/bookingsForOwner.ts +30 -0
- package/templates/local-service/functions/cancelBooking.ts +27 -0
- package/templates/local-service/functions/confirmBooking.ts +18 -0
- package/templates/local-service/functions/createBooking.ts +98 -0
- package/templates/local-service/gitignore +10 -0
- package/templates/local-service/lib/booking.ts +24 -0
- package/templates/local-service/lib/owner.ts +26 -0
- package/templates/local-service/lib/site.config.ts +232 -0
- package/templates/local-service/lib/slots.ts +97 -0
- package/templates/local-service/lib/utils.ts +10 -0
- package/templates/local-service/package.json +34 -0
- package/templates/local-service/tsconfig.json +18 -0
- package/templates/marketplace/.env.example +9 -0
- package/templates/marketplace/AGENTS.md +61 -0
- package/templates/marketplace/README.md +78 -0
- package/templates/marketplace/app/_components/CategoryIcon.tsx +40 -0
- package/templates/marketplace/app/error.tsx +26 -0
- package/templates/marketplace/app/globals.css +64 -0
- package/templates/marketplace/app/layout.tsx +60 -0
- package/templates/marketplace/app/listing/[id]/page.tsx +163 -0
- package/templates/marketplace/app/me/page.tsx +15 -0
- package/templates/marketplace/app/not-found.tsx +20 -0
- package/templates/marketplace/app/page.tsx +159 -0
- package/templates/marketplace/app/robots.ts +12 -0
- package/templates/marketplace/app/sell/page.tsx +26 -0
- package/templates/marketplace/app/sitemap.ts +14 -0
- package/templates/marketplace/app.ts +190 -0
- package/templates/marketplace/client/AuthNav.tsx +46 -0
- package/templates/marketplace/client/LiveTicker.tsx +104 -0
- package/templates/marketplace/client/LoginCard.tsx +130 -0
- package/templates/marketplace/client/MarketProvider.tsx +148 -0
- package/templates/marketplace/client/MyMarket.tsx +180 -0
- package/templates/marketplace/client/OfferPanel.tsx +355 -0
- package/templates/marketplace/client/SeedOnEmpty.tsx +26 -0
- package/templates/marketplace/client/SellForm.tsx +160 -0
- package/templates/marketplace/client/WatchButton.tsx +88 -0
- package/templates/marketplace/client/market.ts +341 -0
- package/templates/marketplace/functions/buyNow.ts +78 -0
- package/templates/marketplace/functions/makeOffer.ts +65 -0
- package/templates/marketplace/functions/respondToOffer.ts +62 -0
- package/templates/marketplace/functions/seedMarket.ts +90 -0
- package/templates/marketplace/gitignore +10 -0
- package/templates/marketplace/package.json +35 -0
- package/templates/marketplace/tsconfig.json +14 -0
- package/templates/marketplace/ui/badge.tsx +30 -0
- package/templates/marketplace/ui/button.tsx +49 -0
- package/templates/marketplace/ui/card.tsx +48 -0
- package/templates/marketplace/ui/input.tsx +17 -0
- package/templates/marketplace/ui/label.tsx +18 -0
- package/templates/marketplace/ui/textarea.tsx +17 -0
- package/templates/marketplace/ui/tokens.css +32 -0
- package/templates/marketplace/ui/utils.ts +6 -0
- package/templates/restaurant/.env.example +12 -0
- package/templates/restaurant/AGENTS.md +61 -0
- package/templates/restaurant/README.md +77 -0
- package/templates/restaurant/app/auth-form.tsx +129 -0
- package/templates/restaurant/app/dashboard/dashboard-client.tsx +263 -0
- package/templates/restaurant/app/dashboard/page.tsx +59 -0
- package/templates/restaurant/app/error.tsx +26 -0
- package/templates/restaurant/app/globals.css +148 -0
- package/templates/restaurant/app/layout.tsx +151 -0
- package/templates/restaurant/app/login/page.tsx +39 -0
- package/templates/restaurant/app/not-found.tsx +19 -0
- package/templates/restaurant/app/page.tsx +194 -0
- package/templates/restaurant/app/reservation-widget.tsx +359 -0
- package/templates/restaurant/app/robots.ts +12 -0
- package/templates/restaurant/app/sitemap.ts +9 -0
- package/templates/restaurant/app.ts +115 -0
- package/templates/restaurant/components/marketing.tsx +162 -0
- package/templates/restaurant/components/section-scroller.tsx +35 -0
- package/templates/restaurant/components/ui/button.tsx +56 -0
- package/templates/restaurant/components/ui/card.tsx +90 -0
- package/templates/restaurant/components.json +20 -0
- package/templates/restaurant/functions/cancelReservation.ts +26 -0
- package/templates/restaurant/functions/confirmReservation.ts +17 -0
- package/templates/restaurant/functions/createReservation.ts +92 -0
- package/templates/restaurant/functions/reservationsForOwner.ts +28 -0
- package/templates/restaurant/gitignore +10 -0
- package/templates/restaurant/lib/owner.ts +26 -0
- package/templates/restaurant/lib/reservation.ts +22 -0
- package/templates/restaurant/lib/site.config.ts +218 -0
- package/templates/restaurant/lib/slots.ts +55 -0
- package/templates/restaurant/lib/utils.ts +10 -0
- package/templates/restaurant/package.json +34 -0
- package/templates/restaurant/tsconfig.json +18 -0
- package/templates/shop/.env.example +32 -0
- package/templates/shop/AGENTS.md +61 -0
- package/templates/shop/README.md +102 -0
- package/templates/shop/app/auth-form.tsx +129 -0
- package/templates/shop/app/dashboard/dashboard-client.tsx +264 -0
- package/templates/shop/app/dashboard/page.tsx +59 -0
- package/templates/shop/app/error.tsx +26 -0
- package/templates/shop/app/globals.css +148 -0
- package/templates/shop/app/layout.tsx +160 -0
- package/templates/shop/app/login/page.tsx +39 -0
- package/templates/shop/app/not-found.tsx +19 -0
- package/templates/shop/app/page.tsx +95 -0
- package/templates/shop/app/robots.ts +12 -0
- package/templates/shop/app/shop-client.tsx +436 -0
- package/templates/shop/app/sitemap.ts +9 -0
- package/templates/shop/app/success/page.tsx +33 -0
- package/templates/shop/app.ts +134 -0
- package/templates/shop/components/marketing.tsx +96 -0
- package/templates/shop/components/section-scroller.tsx +35 -0
- package/templates/shop/components/ui/button.tsx +56 -0
- package/templates/shop/components/ui/card.tsx +90 -0
- package/templates/shop/components.json +20 -0
- package/templates/shop/functions/cancelOrder.ts +33 -0
- package/templates/shop/functions/checkout.ts +130 -0
- package/templates/shop/functions/fulfillOrder.ts +17 -0
- package/templates/shop/functions/markGroupPaid.ts +26 -0
- package/templates/shop/functions/ordersForOwner.ts +28 -0
- package/templates/shop/functions/releaseGroup.ts +36 -0
- package/templates/shop/functions/reserveCart.ts +87 -0
- package/templates/shop/functions/restockProduct.ts +23 -0
- package/templates/shop/functions/seedProducts.ts +30 -0
- package/templates/shop/functions/stripeWebhook.ts +72 -0
- package/templates/shop/gitignore +10 -0
- package/templates/shop/lib/owner.ts +26 -0
- package/templates/shop/lib/shop.ts +45 -0
- package/templates/shop/lib/site.config.ts +198 -0
- package/templates/shop/lib/utils.ts +10 -0
- package/templates/shop/package.json +35 -0
- package/templates/shop/tsconfig.json +18 -0
- package/templates/waitlist/.env.example +12 -0
- package/templates/waitlist/AGENTS.md +61 -0
- package/templates/waitlist/README.md +81 -0
- package/templates/waitlist/app/auth-form.tsx +129 -0
- package/templates/waitlist/app/dashboard/dashboard-client.tsx +297 -0
- package/templates/waitlist/app/dashboard/page.tsx +70 -0
- package/templates/waitlist/app/error.tsx +26 -0
- package/templates/waitlist/app/globals.css +148 -0
- package/templates/waitlist/app/layout.tsx +158 -0
- package/templates/waitlist/app/login/page.tsx +39 -0
- package/templates/waitlist/app/not-found.tsx +19 -0
- package/templates/waitlist/app/page.tsx +119 -0
- package/templates/waitlist/app/robots.ts +12 -0
- package/templates/waitlist/app/sitemap.ts +9 -0
- package/templates/waitlist/app/waitlist-hero.tsx +219 -0
- package/templates/waitlist/app.ts +134 -0
- package/templates/waitlist/components/marketing.tsx +96 -0
- package/templates/waitlist/components/ui/button.tsx +56 -0
- package/templates/waitlist/components/ui/card.tsx +90 -0
- package/templates/waitlist/components.json +20 -0
- package/templates/waitlist/functions/joinWaitlist.ts +82 -0
- package/templates/waitlist/functions/waitlistStats.ts +75 -0
- package/templates/waitlist/gitignore +10 -0
- package/templates/waitlist/lib/owner.ts +26 -0
- package/templates/waitlist/lib/site.config.ts +178 -0
- package/templates/waitlist/lib/stats.ts +30 -0
- package/templates/waitlist/lib/utils.ts +10 -0
- package/templates/waitlist/package.json +34 -0
- package/templates/waitlist/tsconfig.json +18 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
// THE single source of truth for everything business-specific on this agency
|
|
2
|
+
// site. Rebrand the whole studio by editing this ONE file — the landing page,
|
|
3
|
+
// layout, and the seedCapacity function all read from here. The create-pylon
|
|
4
|
+
// scaffolder and Mast target this file: a whole studio site is themed from one
|
|
5
|
+
// typed object.
|
|
6
|
+
//
|
|
7
|
+
// Colors live here (applied as CSS variables on <html> in app/layout.tsx).
|
|
8
|
+
// Fictional demo copy — replace the values, keep the shape. Anywhere a real
|
|
9
|
+
// photo belongs (case-study shots, team headshots) the page renders a clearly
|
|
10
|
+
// marked placeholder — swap those for <img>s when you have the assets.
|
|
11
|
+
|
|
12
|
+
/* ----------------------------- types ----------------------------- */
|
|
13
|
+
|
|
14
|
+
export type Social = { label: string; href: string; path: string };
|
|
15
|
+
|
|
16
|
+
export type BaseConfig = {
|
|
17
|
+
brand: {
|
|
18
|
+
name: string;
|
|
19
|
+
letter: string; // monogram
|
|
20
|
+
domain: string;
|
|
21
|
+
email: string;
|
|
22
|
+
footerBlurb: string;
|
|
23
|
+
copyrightName: string;
|
|
24
|
+
socials: Social[];
|
|
25
|
+
};
|
|
26
|
+
colors: { brand: string; brandSoft: string; paper: string };
|
|
27
|
+
seo: { title: string; description: string };
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type Service = { title: string; body: string; icon?: string };
|
|
31
|
+
|
|
32
|
+
// A portfolio piece + case study. `seedProjects` writes these into the public
|
|
33
|
+
// Project entity on first visit; after that the owner curates them from the
|
|
34
|
+
// dashboard. `selected` features it on the homepage; the challenge/approach/
|
|
35
|
+
// outcome render on the /work/[slug] case-study page. `slug` is the URL segment
|
|
36
|
+
// (auto-derived from the title if omitted).
|
|
37
|
+
export type CaseStudy = {
|
|
38
|
+
title: string;
|
|
39
|
+
slug?: string;
|
|
40
|
+
client: string; // display label, e.g. "Fintech · 0→1"
|
|
41
|
+
summary: string;
|
|
42
|
+
year?: string;
|
|
43
|
+
tags: string[];
|
|
44
|
+
selected?: boolean; // featured on the homepage "Selected work" grid
|
|
45
|
+
challenge?: string;
|
|
46
|
+
approach?: string;
|
|
47
|
+
outcome?: string;
|
|
48
|
+
liveUrl?: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type ProcessStep = { title: string; body: string };
|
|
52
|
+
export type TeamMember = { name: string; role: string };
|
|
53
|
+
export type Testimonial = { quote: string; name: string; role: string };
|
|
54
|
+
|
|
55
|
+
// Demo back-office rows. `seedStudioBackoffice` (owner-gated) writes these into
|
|
56
|
+
// the private Client + Invoice entities on the owner's first dashboard visit, so
|
|
57
|
+
// the dashboard isn't an empty shell. `invoices[].client` matches a `clients[]`
|
|
58
|
+
// name; `invoices[].projectSlug` (optional) ties a bill to a case study.
|
|
59
|
+
export type ClientSeed = {
|
|
60
|
+
name: string;
|
|
61
|
+
company?: string;
|
|
62
|
+
email?: string;
|
|
63
|
+
phone?: string;
|
|
64
|
+
status?: "prospect" | "active" | "past";
|
|
65
|
+
notes?: string;
|
|
66
|
+
};
|
|
67
|
+
export type InvoiceSeed = {
|
|
68
|
+
number: string;
|
|
69
|
+
client: string; // matches a clients[].name
|
|
70
|
+
projectSlug?: string; // matches a work.items[].slug
|
|
71
|
+
// Line items drive the total — `amountCents` is computed from them on seed.
|
|
72
|
+
lineItems: { description: string; quantity: number; unitCents: number }[];
|
|
73
|
+
status?: "draft" | "sent" | "paid" | "overdue";
|
|
74
|
+
issuedAt?: string; // "2026-04-01"
|
|
75
|
+
dueAt?: string;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// The studio's billing identity — the "from" block + terms on every invoice and
|
|
79
|
+
// its PDF. Edit these to make the invoices yours.
|
|
80
|
+
export type Billing = {
|
|
81
|
+
addressLines: string[]; // shown under the studio name on the invoice
|
|
82
|
+
paymentTerms: string; // e.g. "Net 30"
|
|
83
|
+
footerNote: string; // a thank-you / payment note in the invoice footer
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export type AgencyConfig = BaseConfig & {
|
|
87
|
+
hero: {
|
|
88
|
+
tagline: string;
|
|
89
|
+
headline: string;
|
|
90
|
+
subcopy: string;
|
|
91
|
+
ctaLabel: string;
|
|
92
|
+
secondaryCtaLabel: string;
|
|
93
|
+
};
|
|
94
|
+
// Seeds the public Capacity row on first visit; after that it lives in the DB
|
|
95
|
+
// and the owner manages it from the dashboard. `openSlots` is the number the
|
|
96
|
+
// hero shows live; `label` is the booking window it refers to.
|
|
97
|
+
capacity: { label: string; openSlots: number };
|
|
98
|
+
logos: { eyebrow: string; names: string[] };
|
|
99
|
+
services: { eyebrow: string; headline: string; items: Service[] };
|
|
100
|
+
work: { eyebrow: string; headline: string; items: CaseStudy[] };
|
|
101
|
+
// The studio's invoice "from" identity + terms.
|
|
102
|
+
billing: Billing;
|
|
103
|
+
// Demo CRM + billing rows seeded into the owner dashboard on first visit.
|
|
104
|
+
backoffice: { clients: ClientSeed[]; invoices: InvoiceSeed[] };
|
|
105
|
+
process: { eyebrow: string; headline: string; steps: ProcessStep[] };
|
|
106
|
+
team: { eyebrow: string; headline: string; members: TeamMember[] };
|
|
107
|
+
testimonials?: { eyebrow: string; headline: string; items: Testimonial[] };
|
|
108
|
+
contact: {
|
|
109
|
+
eyebrow: string;
|
|
110
|
+
headline: string;
|
|
111
|
+
subcopy: string;
|
|
112
|
+
projectTypes: string[];
|
|
113
|
+
budgets: string[];
|
|
114
|
+
confirmationMessage: string;
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/* ----------------------------- config ---------------------------- */
|
|
119
|
+
|
|
120
|
+
export const siteConfig: AgencyConfig = {
|
|
121
|
+
brand: {
|
|
122
|
+
name: "Halyard",
|
|
123
|
+
letter: "H",
|
|
124
|
+
domain: "halyard.studio",
|
|
125
|
+
email: "hello@halyard.example",
|
|
126
|
+
footerBlurb:
|
|
127
|
+
"A product studio in Dallas. We design and build the software ambitious teams bet on — and we only take on a few projects at a time, so the work stays sharp.",
|
|
128
|
+
copyrightName: "Halyard Studio",
|
|
129
|
+
socials: [
|
|
130
|
+
{
|
|
131
|
+
label: "X",
|
|
132
|
+
href: "https://x.com",
|
|
133
|
+
path: "M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z",
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
label: "LinkedIn",
|
|
137
|
+
href: "https://linkedin.com",
|
|
138
|
+
path: "M20.45 20.45h-3.56v-5.57c0-1.33-.02-3.04-1.85-3.04-1.85 0-2.13 1.45-2.13 2.94v5.67H9.35V9h3.42v1.56h.05c.48-.9 1.64-1.85 3.37-1.85 3.6 0 4.27 2.37 4.27 5.46v6.28zM5.34 7.43a2.06 2.06 0 1 1 0-4.13 2.06 2.06 0 0 1 0 4.13zM7.12 20.45H3.55V9h3.57v11.45zM22.22 0H1.77C.79 0 0 .77 0 1.73v20.54C0 23.22.79 24 1.77 24h20.45c.98 0 1.78-.78 1.78-1.73V1.73C24 .77 23.2 0 22.22 0z",
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
colors: { brand: "#4f46e5", brandSoft: "#e0e7ff", paper: "#fafafa" },
|
|
144
|
+
|
|
145
|
+
seo: {
|
|
146
|
+
title: "Halyard — a product studio for ambitious teams.",
|
|
147
|
+
description:
|
|
148
|
+
"Halyard is a Dallas product studio. We design and build web and mobile software end-to-end. We take on a few projects at a time — see how many slots are open this quarter.",
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
hero: {
|
|
152
|
+
tagline: "Product studio · Dallas",
|
|
153
|
+
headline: "We build the products teams bet on.",
|
|
154
|
+
subcopy:
|
|
155
|
+
"Halyard is a small, senior team that designs and ships web and mobile software end-to-end. We take on a handful of projects at a time, so the work — and your launch — stays sharp.",
|
|
156
|
+
ctaLabel: "Start a project",
|
|
157
|
+
secondaryCtaLabel: "See our work",
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
capacity: { label: "Q3 2026", openSlots: 3 },
|
|
161
|
+
|
|
162
|
+
logos: {
|
|
163
|
+
eyebrow: "Trusted by teams at",
|
|
164
|
+
names: ["Northwind", "Lumen", "Foundry", "Atlas", "Cohort", "Vela"],
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
services: {
|
|
168
|
+
eyebrow: "What we do",
|
|
169
|
+
headline: "One team, the whole build.",
|
|
170
|
+
items: [
|
|
171
|
+
{
|
|
172
|
+
icon: "◆",
|
|
173
|
+
title: "Product design",
|
|
174
|
+
body: "Research, flows, and interface design that turns a fuzzy idea into something people understand in seconds.",
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
icon: "◇",
|
|
178
|
+
title: "Web & mobile",
|
|
179
|
+
body: "Production engineering across web, iOS, and Android — typed, tested, and built to scale past launch day.",
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
icon: "◈",
|
|
183
|
+
title: "Brand & identity",
|
|
184
|
+
body: "Naming, logo, and a system that makes a young product feel like it's been around — and worth paying for.",
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
icon: "✦",
|
|
188
|
+
title: "Fractional team",
|
|
189
|
+
body: "Embed with your team for a quarter. We plan, build, and hand off — leaving you faster than we found you.",
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
work: {
|
|
195
|
+
eyebrow: "Selected work",
|
|
196
|
+
headline: "A few things we've shipped.",
|
|
197
|
+
items: [
|
|
198
|
+
{
|
|
199
|
+
title: "Ledger",
|
|
200
|
+
slug: "ledger",
|
|
201
|
+
client: "Fintech · 0→1",
|
|
202
|
+
year: "2026",
|
|
203
|
+
summary: "A consumer banking app from first sketch to App Store launch in one quarter.",
|
|
204
|
+
tags: ["Product design", "iOS", "Brand"],
|
|
205
|
+
selected: true,
|
|
206
|
+
challenge:
|
|
207
|
+
"A two-founder fintech had funding and a thesis, but no product, no brand, and a hard 12-week runway to a launchable app the App Store would approve.",
|
|
208
|
+
approach:
|
|
209
|
+
"We ran a one-week scope sprint, then designed and built in parallel — a clickable prototype on real data by week three, weekly TestFlight builds after that. Brand and UI moved together so nothing felt bolted on.",
|
|
210
|
+
outcome:
|
|
211
|
+
"Shipped to the App Store in the 11th week with a 4.8★ launch rating. The founders closed their seed round two weeks later using the live app as the demo.",
|
|
212
|
+
liveUrl: "https://example.com",
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
title: "Atlas Health",
|
|
216
|
+
slug: "atlas-health",
|
|
217
|
+
client: "Healthcare · Platform",
|
|
218
|
+
year: "2025",
|
|
219
|
+
summary: "Rebuilt a clinical scheduling tool used by 4,000 providers, with zero downtime.",
|
|
220
|
+
tags: ["Web", "Design system"],
|
|
221
|
+
selected: true,
|
|
222
|
+
challenge:
|
|
223
|
+
"A scheduling platform 4,000 clinicians depended on daily had become impossible to change — every release risked an outage no hospital could tolerate.",
|
|
224
|
+
approach:
|
|
225
|
+
"We introduced a typed design system and migrated screen by screen behind feature flags, shipping to a few clinics at a time and watching the metrics before widening the rollout.",
|
|
226
|
+
outcome:
|
|
227
|
+
"Replaced the entire front end over a quarter with zero scheduled downtime, and cut the time to ship a new screen from two weeks to two days.",
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
title: "Cohort",
|
|
231
|
+
slug: "cohort",
|
|
232
|
+
client: "B2B SaaS · Rebrand",
|
|
233
|
+
year: "2025",
|
|
234
|
+
summary: "New identity and marketing site that lifted demo requests 40% in six weeks.",
|
|
235
|
+
tags: ["Brand", "Web"],
|
|
236
|
+
selected: true,
|
|
237
|
+
challenge:
|
|
238
|
+
"A profitable B2B SaaS had outgrown the brand it launched with — the site read like a side project and was quietly losing enterprise deals at the first click.",
|
|
239
|
+
approach:
|
|
240
|
+
"A focused rebrand: new name treatment, a confident visual system, and a marketing site rebuilt around the two proof points buyers actually asked about.",
|
|
241
|
+
outcome:
|
|
242
|
+
"Demo requests rose 40% in the first six weeks, and the sales team stopped apologizing for the website on calls.",
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
title: "Vela",
|
|
246
|
+
slug: "vela",
|
|
247
|
+
client: "Logistics · Mobile",
|
|
248
|
+
year: "2024",
|
|
249
|
+
summary: "A driver app with live routing that cut dispatch calls in half.",
|
|
250
|
+
tags: ["Product design", "Android"],
|
|
251
|
+
selected: false,
|
|
252
|
+
challenge:
|
|
253
|
+
"Dispatchers spent their day on the phone because drivers had no live view of their own routes — every change meant a call.",
|
|
254
|
+
approach:
|
|
255
|
+
"We designed an offline-first driver app with live routing and a single, glanceable 'what's next' screen, built for one-handed use in a moving vehicle.",
|
|
256
|
+
outcome:
|
|
257
|
+
"Dispatch calls dropped by half within a month, and on-time delivery climbed eight points.",
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
backoffice: {
|
|
263
|
+
clients: [
|
|
264
|
+
{
|
|
265
|
+
name: "Erin Caldwell",
|
|
266
|
+
company: "Ledger",
|
|
267
|
+
email: "erin@ledger.example",
|
|
268
|
+
phone: "+1 (214) 555-0142",
|
|
269
|
+
status: "active",
|
|
270
|
+
notes: "Founder. Seed round closed; discussing a phase-2 retainer for Q4.",
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
name: "Tom Reyes",
|
|
274
|
+
company: "Atlas Health",
|
|
275
|
+
email: "tom@atlashealth.example",
|
|
276
|
+
status: "active",
|
|
277
|
+
notes: "VP Product. Rollout complete; on a monthly maintenance retainer.",
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
name: "Sofia Marin",
|
|
281
|
+
company: "Cohort",
|
|
282
|
+
email: "sofia@cohort.example",
|
|
283
|
+
status: "past",
|
|
284
|
+
notes: "Rebrand shipped. Happy to be a reference.",
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
name: "Grant Whitaker",
|
|
288
|
+
company: "Northwind",
|
|
289
|
+
email: "grant@northwind.example",
|
|
290
|
+
status: "prospect",
|
|
291
|
+
notes: "Inbound about a 0→1 mobile app. Sent a proposal; awaiting reply.",
|
|
292
|
+
},
|
|
293
|
+
],
|
|
294
|
+
invoices: [
|
|
295
|
+
{
|
|
296
|
+
number: "INV-001",
|
|
297
|
+
client: "Erin Caldwell",
|
|
298
|
+
projectSlug: "ledger",
|
|
299
|
+
lineItems: [
|
|
300
|
+
{ description: "Product design & iOS build — Ledger (12-week engagement)", quantity: 1, unitCents: 4800000 },
|
|
301
|
+
],
|
|
302
|
+
status: "paid",
|
|
303
|
+
issuedAt: "2026-01-15",
|
|
304
|
+
dueAt: "2026-02-14",
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
number: "INV-002",
|
|
308
|
+
client: "Tom Reyes",
|
|
309
|
+
projectSlug: "atlas-health",
|
|
310
|
+
lineItems: [
|
|
311
|
+
{ description: "Platform rebuild & design system", quantity: 1, unitCents: 5500000 },
|
|
312
|
+
{ description: "Discovery & scoping sprint", quantity: 1, unitCents: 700000 },
|
|
313
|
+
],
|
|
314
|
+
status: "paid",
|
|
315
|
+
issuedAt: "2026-03-01",
|
|
316
|
+
dueAt: "2026-03-31",
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
number: "INV-003",
|
|
320
|
+
client: "Tom Reyes",
|
|
321
|
+
projectSlug: "atlas-health",
|
|
322
|
+
lineItems: [
|
|
323
|
+
{ description: "Maintenance retainer — June", quantity: 1, unitCents: 850000 },
|
|
324
|
+
],
|
|
325
|
+
status: "sent",
|
|
326
|
+
issuedAt: "2026-06-01",
|
|
327
|
+
dueAt: "2026-07-01",
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
number: "INV-004",
|
|
331
|
+
client: "Sofia Marin",
|
|
332
|
+
projectSlug: "cohort",
|
|
333
|
+
lineItems: [
|
|
334
|
+
{ description: "Brand identity & system", quantity: 1, unitCents: 1400000 },
|
|
335
|
+
{ description: "Marketing site design & build", quantity: 1, unitCents: 1000000 },
|
|
336
|
+
],
|
|
337
|
+
status: "overdue",
|
|
338
|
+
issuedAt: "2026-04-10",
|
|
339
|
+
dueAt: "2026-05-10",
|
|
340
|
+
},
|
|
341
|
+
],
|
|
342
|
+
},
|
|
343
|
+
|
|
344
|
+
billing: {
|
|
345
|
+
addressLines: ["Halyard Studio", "Dallas, TX", "hello@halyard.example"],
|
|
346
|
+
paymentTerms: "Net 30",
|
|
347
|
+
footerNote: "Thank you. Please reference the invoice number with payment.",
|
|
348
|
+
},
|
|
349
|
+
|
|
350
|
+
process: {
|
|
351
|
+
eyebrow: "How we work",
|
|
352
|
+
headline: "Senior, hands-on, and fast.",
|
|
353
|
+
steps: [
|
|
354
|
+
{
|
|
355
|
+
title: "Scope",
|
|
356
|
+
body: "A focused kickoff week. We pin down the real problem, the riskiest unknowns, and what a great launch looks like.",
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
title: "Design",
|
|
360
|
+
body: "Clickable, real-data prototypes — not slide decks. You react to something you can use within two weeks.",
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
title: "Build",
|
|
364
|
+
body: "Weekly shipping you can watch. Typed, reviewed code in your repo from day one, no black box.",
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
title: "Launch & hand off",
|
|
368
|
+
body: "We ship it, document it, and leave your team able to run and extend it without us.",
|
|
369
|
+
},
|
|
370
|
+
],
|
|
371
|
+
},
|
|
372
|
+
|
|
373
|
+
team: {
|
|
374
|
+
eyebrow: "Who you'll work with",
|
|
375
|
+
headline: "A small, senior team.",
|
|
376
|
+
members: [
|
|
377
|
+
{ name: "Claire Donovan", role: "Principal, Design" },
|
|
378
|
+
{ name: "Marcus Lee", role: "Principal, Engineering" },
|
|
379
|
+
{ name: "Dana Okafor", role: "Brand & Strategy" },
|
|
380
|
+
],
|
|
381
|
+
},
|
|
382
|
+
|
|
383
|
+
testimonials: {
|
|
384
|
+
eyebrow: "Kind words",
|
|
385
|
+
headline: "Teams we've worked with.",
|
|
386
|
+
items: [
|
|
387
|
+
{
|
|
388
|
+
quote:
|
|
389
|
+
"Halyard shipped in a quarter what our last agency couldn't in a year. Senior people, no hand-offs, no drama.",
|
|
390
|
+
name: "Erin Caldwell",
|
|
391
|
+
role: "CEO, Ledger",
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
quote:
|
|
395
|
+
"They felt like our team, not a vendor. The work was sharp and the launch was the smoothest we've had.",
|
|
396
|
+
name: "Tom Reyes",
|
|
397
|
+
role: "VP Product, Atlas Health",
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
quote:
|
|
401
|
+
"The rebrand paid for itself in two months. We still use the system they built every single day.",
|
|
402
|
+
name: "Sofia Marin",
|
|
403
|
+
role: "Founder, Cohort",
|
|
404
|
+
},
|
|
405
|
+
],
|
|
406
|
+
},
|
|
407
|
+
|
|
408
|
+
contact: {
|
|
409
|
+
eyebrow: "Start a project",
|
|
410
|
+
headline: "Tell us what you're building.",
|
|
411
|
+
subcopy:
|
|
412
|
+
"We take on a few projects at a time. Send a note and we'll reply within two business days — if we're full, we'll tell you straight and point you somewhere good.",
|
|
413
|
+
projectTypes: ["New product (0→1)", "Existing product", "Rebrand", "Not sure yet"],
|
|
414
|
+
budgets: ["$25–50k", "$50–100k", "$100k+", "Let's talk"],
|
|
415
|
+
confirmationMessage:
|
|
416
|
+
"Thanks — your note's in. We'll get back to you within two business days.",
|
|
417
|
+
},
|
|
418
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ClassValue, clsx } from "clsx";
|
|
2
|
+
import { twMerge } from "tailwind-merge";
|
|
3
|
+
|
|
4
|
+
// `cn` — the shadcn class merger. clsx resolves conditional/array class
|
|
5
|
+
// inputs; tailwind-merge then dedupes conflicting Tailwind utilities so
|
|
6
|
+
// the last one wins (e.g. `cn("px-2", "px-4")` → "px-4"). Every shadcn
|
|
7
|
+
// component routes its className through this.
|
|
8
|
+
export function cn(...inputs: ClassValue[]) {
|
|
9
|
+
return twMerge(clsx(inputs));
|
|
10
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__APP_NAME_KEBAB__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "pylon dev",
|
|
8
|
+
"deploy": "pylon deploy",
|
|
9
|
+
"check": "tsc --noEmit"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@pylonsync/react": "^__PYLON_VERSION__",
|
|
13
|
+
"@pylonsync/sdk": "^__PYLON_VERSION__",
|
|
14
|
+
"@pylonsync/functions": "^__PYLON_VERSION__",
|
|
15
|
+
"@pylonsync/client": "^__PYLON_VERSION__",
|
|
16
|
+
"react": "^19.0.0",
|
|
17
|
+
"react-dom": "^19.0.0",
|
|
18
|
+
"tailwindcss": "^4.3.0",
|
|
19
|
+
"@tailwindcss/cli": "^4.3.0",
|
|
20
|
+
"tw-animate-css": "^1.2.0",
|
|
21
|
+
"class-variance-authority": "^0.7.1",
|
|
22
|
+
"clsx": "^2.1.1",
|
|
23
|
+
"tailwind-merge": "^2.5.0",
|
|
24
|
+
"lucide-react": "^0.460.0",
|
|
25
|
+
"@radix-ui/react-slot": "^1.1.0",
|
|
26
|
+
"@react-pdf/renderer": "^4.5.1"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@pylonsync/cli": "^__PYLON_VERSION__",
|
|
30
|
+
"@types/node": "^22.0.0",
|
|
31
|
+
"@types/react": "^19.0.0",
|
|
32
|
+
"@types/react-dom": "^19.0.0",
|
|
33
|
+
"typescript": "^5.6.0"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"jsx": "react",
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"lib": ["ES2022", "DOM"],
|
|
11
|
+
"types": ["react", "react-dom", "node"],
|
|
12
|
+
"baseUrl": ".",
|
|
13
|
+
"paths": {
|
|
14
|
+
"@/*": ["./*"]
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"include": ["app.ts", "app/**/*", "components/**/*", "lib/**/*", "functions/**/*"]
|
|
18
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Copy to `.env` and fill in. `pylon dev` loads `.env` automatically.
|
|
2
|
+
#
|
|
3
|
+
# The app boots with no config — you can sign in and create chats — but the
|
|
4
|
+
# assistant can't reply until you point it at an LLM provider. Streaming runs
|
|
5
|
+
# through the built-in POST /api/ai/stream endpoint, so your API key stays on
|
|
6
|
+
# the server and never reaches the browser.
|
|
7
|
+
|
|
8
|
+
# ── AI provider (required for the assistant to reply) ────────────────────────
|
|
9
|
+
# PYLON_AI_PROVIDER "anthropic" | "openai" | "custom"
|
|
10
|
+
# PYLON_AI_API_KEY your provider key (sk-… / sk-ant-…)
|
|
11
|
+
# PYLON_AI_MODEL the default model used when none is selected
|
|
12
|
+
# PYLON_AI_PROVIDER=anthropic
|
|
13
|
+
# PYLON_AI_API_KEY=sk-ant-...
|
|
14
|
+
# PYLON_AI_MODEL=claude-sonnet-4-6
|
|
15
|
+
|
|
16
|
+
# ── Model switching ──────────────────────────────────────────────────────────
|
|
17
|
+
# The chat's model picker (lib/site.config.ts → chat.models) sends a `model` per
|
|
18
|
+
# request. For security, client-chosen models must be allow-listed here
|
|
19
|
+
# (comma-separated) — otherwise the override is refused and the chat shows a
|
|
20
|
+
# notice. List exactly the model ids you expose in the picker:
|
|
21
|
+
# PYLON_AI_MODELS_ALLOWED=claude-sonnet-4-6,claude-opus-4-8,claude-haiku-4-5
|
|
22
|
+
|
|
23
|
+
# ── Multiple PROVIDERS in one picker (optional) ──────────────────────────────
|
|
24
|
+
# /api/ai/stream talks to ONE provider. To offer Anthropic + OpenAI + others in
|
|
25
|
+
# the same picker, route through an OpenAI-compatible gateway like OpenRouter:
|
|
26
|
+
# PYLON_AI_PROVIDER=custom
|
|
27
|
+
# PYLON_AI_BASE_URL=https://openrouter.ai/api/v1
|
|
28
|
+
# PYLON_AI_API_KEY=<openrouter key>
|
|
29
|
+
# then set chat.models (config) + PYLON_AI_MODELS_ALLOWED to that gateway's slugs
|
|
30
|
+
# (e.g. anthropic/claude-sonnet-4, openai/gpt-4o, google/gemini-2.5-pro).
|
|
31
|
+
|
|
32
|
+
# ── Site URL (optional) ──────────────────────────────────────────────────────
|
|
33
|
+
# SITE_URL=https://yourchat.com
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# AGENTS.md — working in a Pylon project
|
|
2
|
+
|
|
3
|
+
Operating rules for a coding agent in this Pylon app. Pylon is a Rails-like framework for realtime apps: you declare entities, policies, and server functions in TypeScript, and a single Rust binary (`pylon`) serves the API, auth, sync, WebSocket, SSE, and native React 19 SSR — one process, one port. The full API reference is at **/llms-full.txt** (served at `/llms-full.txt`; in the repo at `apps/web/public/llms-full.txt`). Read it before guessing an API name.
|
|
4
|
+
|
|
5
|
+
## Directory conventions
|
|
6
|
+
|
|
7
|
+
**Unified SSR app:**
|
|
8
|
+
- `app.ts` — data model + manifest (`entity()` + `field.*`, queries/actions/policies, `routes: await discoverAppRoutes()`). Ends with `console.log(JSON.stringify(manifest))`.
|
|
9
|
+
- `app/` — file-based SSR routes. `app/page.tsx` → `/`, `app/about/page.tsx` → `/about`, `app/blog/[slug]/page.tsx` → `/blog/:slug`. `app/layout.tsx` is the shell; `app/error.tsx` / `app/not-found.tsx` are boundaries.
|
|
10
|
+
- `app/globals.css` — Tailwind v4 entrypoint (auto-compiled and injected).
|
|
11
|
+
- `functions/` — server functions, one per file, `default`-exported.
|
|
12
|
+
- `.pylon/` — local dev state (sqlite, jobs, sessions, uploads). Created by `pylon dev`. Do not commit.
|
|
13
|
+
|
|
14
|
+
**Monorepo app:** backend is `apps/api/` (entry `apps/api/schema.ts`, handlers in `apps/api/functions/`); frontend in `apps/web/`. `pylon.manifest.json` / `pylon.client.ts` are generated — do not hand-edit.
|
|
15
|
+
|
|
16
|
+
## The core authoring loop
|
|
17
|
+
|
|
18
|
+
1. **Define an entity** — `entity("Thing", { name: field.string(), done: field.boolean().default(false) })`. Modifiers: `.optional()`, `.unique()`, `.readonly()` (settable on insert, rejected on client update — use for `authorId`/`orgId`), `.serverOnly()` (never in HTTP responses), `.encrypted()` (AEAD at rest, needs `PYLON_ENCRYPTION_KEY`), `.crdt("text")` (collaborative).
|
|
19
|
+
2. **Write a policy** — `policy({ entity: "Thing", allowRead, allowInsert, allowUpdate, allowDelete })` with CEL-like expressions over `auth.*` / `data.*` (e.g. `"auth.userId == data.authorId"`). **Omitted actions DENY by default.** Wide-open dev policies (`allow*: "true"`) are flagged by `pylon lint` — tighten before shipping.
|
|
20
|
+
3. **Author a function** in `functions/<name>.ts` — `query` (read-only), `mutation` (transactional read+write), or `action` (external I/O, no direct `ctx.db`). Import `{ query, mutation, action, v }` from `@pylonsync/functions`. `auth` defaults to `"user"` (secure-by-default); set `"public"` explicitly for unauthenticated access. Use `ctx.db.*`, `ctx.auth.userId`, `ctx.error(code, msg)`.
|
|
21
|
+
4. **Read it on the client** — `db.useQuery("Thing")` (live, re-renders on any write) or `db.useQueryOne("Thing", id)`. Call functions with `db.fn(name, args)` / `callFn`. On SSR pages, read via `use(serverData.list("Thing"))` inside `<Suspense>`.
|
|
22
|
+
|
|
23
|
+
## Key gotchas
|
|
24
|
+
|
|
25
|
+
- **Policies deny by default; server functions BYPASS them.** Direct client CRUD (`/api/entities/*`) and sync are policy-checked. Functions run with full DB access — enforce trust with `ctx.auth` checks inside the handler, not policies.
|
|
26
|
+
- **Type page props from the SDK, don't hand-roll them.** `import type { PageProps, Metadata } from "@pylonsync/react"`. Every page/layout gets `{ url, params, searchParams, auth, response, serverData }`; `PageProps<{ slug: string }>` types a `[slug]` route's params. Request headers/cookies are intentionally NOT on `PageProps` — they're server-only and stripped from hydration, so reading them in the render would mismatch.
|
|
27
|
+
- **Anonymous output caching is opt-in + earned.** `export const revalidate = 60` (seconds) on a page makes it CDN-cacheable (`public, s-maxage=60`) — but ONLY if the render is auth-INDEPENDENT: it must NOT read `props.auth` (reading it at all opts out, even for anonymous), set no cookie, and the app must not run strict per-caller policies (`PYLON_STRICT_FN_POLICIES`). `export const dynamic = "force-static"` caches until the next deploy; `"force-dynamic"` never caches. Fail-closed: without the opt-in (or if any condition fails) the page is `no-cache`. A page that reads `auth` or sets a cookie is never shared. The SAME earned render is also kept in an **origin disk cache** (`.pylon/.cache/ssr`): a cookie-less GET with no query string is served straight off disk for the TTL — skipping the render entirely — then re-rendered live when stale. The disk cache is namespaced per deploy (wiped on each new build) and OFF in `pylon dev` (so an edit is never masked by a stale entry); invalidation is by the `revalidate` TTL or the next deploy.
|
|
28
|
+
- **No-JS forms use `route.ts` + `<Form>`.** Drop `app/.../route.ts` exporting `export const POST: RouteHandler = async ({ form, db, response, auth }) => { await db.insert("X", {...}); response.redirect("/x?ok=1"); }` (303 POST-redirect-GET by default). Render `<Form action="/x">` (from @pylonsync/react) with plain `<input name=...>` — works with JS off (native POST→handler→redirect) and is enhanced to no-reload when JS is on. The handler's `db` is read+write (mutation trust model — gate on `auth`); CSRF is automatic (Origin gate + SameSite=Lax). Multipart/file uploads aren't supported yet — use urlencoded forms + `/api/files`.
|
|
29
|
+
- **`loading.tsx` streams a skeleton while the page's data resolves.** Drop `app/.../loading.tsx` (default export, page props) and the nearest one becomes a route-level Suspense fallback: Pylon flushes the shell + skeleton immediately, then reveals the real page when its top-level `use(serverData…)` resolves (no blank page). It only shows when the PAGE suspends — a page that wraps its own `<Suspense>` around a child (like `/dashboard` in this template) handles that itself. The skeleton is SERVER-ONLY: don't read `serverData` in it. A page with no `loading.tsx` is buffered (unchanged).
|
|
30
|
+
- **`export const streaming = true` streams a page's OWN inner `<Suspense>` boundaries.** Without it (and without a `loading.tsx`), a page is BUFFERED — the whole document, including suspended children, resolves before the first byte. Opt in and the shell + each inner `<Suspense>` fallback flush immediately, then each boundary's real content streams in as its data resolves (multi-boundary progressive streaming). It's opt-in because it changes the response timing contract: a streaming render commits its HTTP head BEFORE suspended subtrees finish, so (a) it's never CDN/disk cacheable — don't combine with `export const revalidate`; (b) `response.setStatus/setCookie/redirect/notFound` only take effect from the SYNCHRONOUS shell render — a call from inside a suspended subtree is dropped (the runtime logs a loud warning naming what was lost); (c) a `throw` from a deep `<Suspense>` child resolves via its nearest `error.tsx` at HTTP 200, not a 5xx. Hydration is clean for any number of boundaries (the data blob ships before hydration runs). Type the config with `import type { RouteSegmentConfig } from "@pylonsync/react"`.
|
|
31
|
+
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`) — `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
|
|
32
|
+
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive — during SSR they return defaults (empty params / "/"); for server-side URL values read the `url` / `searchParams` page props.
|
|
33
|
+
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"` — `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
34
|
+
- **`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.
|
|
35
|
+
- **`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.
|
|
36
|
+
- **`ctx.llm` and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db` — use `ctx.runQuery` / `ctx.runMutation`.
|
|
37
|
+
- **It's `db.useQueryOne`, not `useOne`.** Validators and field types have aliases: `v.bool`/`v.boolean`, `v.float`/`v.number`.
|
|
38
|
+
- **There is no `ctx.files` or `defineWorkflow`/`defineJob`.** Files go through `<FileUpload>` + `/api/files/*`; deferred execution is `ctx.scheduler.runAfter/runAt/cancel`.
|
|
39
|
+
|
|
40
|
+
## Use the CLI — don't guess
|
|
41
|
+
|
|
42
|
+
| Need | Command |
|
|
43
|
+
|---|---|
|
|
44
|
+
| Run the app (SSR + API, hot reload, one port `:4321`) | `pylon dev` (or `npm run dev`) |
|
|
45
|
+
| Regenerate manifest + typed client | `pylon codegen` (Swift client: `pylon codegen client --target swift`) |
|
|
46
|
+
| Validate / diff / push schema | `pylon schema check` \| `diff` \| `push` |
|
|
47
|
+
| Migrations | `pylon migrate create <name>` \| `plan` \| `apply` |
|
|
48
|
+
| Lint policies (PYL001–PYL004) | `pylon lint --strict` |
|
|
49
|
+
| Tests | `pylon test` |
|
|
50
|
+
| Adversarial security probe | `pylon test:security` |
|
|
51
|
+
| Inspect cloud request logs (agent-safe) | `pylon logs --json --limit 50` |
|
|
52
|
+
| Inspect data / entities | `pylon data entities` \| `pylon data list <Entity>` |
|
|
53
|
+
| Call a function | `pylon fn <name> key=value` |
|
|
54
|
+
| Health snapshot | `pylon status` |
|
|
55
|
+
| Build for prod | `pylon build` |
|
|
56
|
+
| Deploy (Pylon Cloud by default) | `pylon deploy` |
|
|
57
|
+
| Look up an error code | `pylon explain <CODE>` |
|
|
58
|
+
|
|
59
|
+
`--json` works on every command for machine-readable output. Prefer one-shot/agent-safe flags (`pylon logs --limit N`, not a blocking `--follow`).
|
|
60
|
+
|
|
61
|
+
For full signatures, env vars, the complete CLI, and SSR/client/server-primitive details: **/llms-full.txt**.
|