@pylonsync/create-pylon 0.11.1 → 0.11.3

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.
Files changed (142) hide show
  1. package/bin/create-pylon.js +1 -1
  2. package/package.json +1 -1
  3. package/templates/_root/AGENTS.md +1 -1
  4. package/templates/agency/AGENTS.md +1 -1
  5. package/templates/agency/app/(marketing)/contact-form.tsx +22 -32
  6. package/templates/agency/app/(marketing)/layout.tsx +36 -64
  7. package/templates/agency/app/(marketing)/not-found.tsx +6 -8
  8. package/templates/agency/app/(marketing)/page.tsx +93 -144
  9. package/templates/agency/app/(marketing)/work/[slug]/page.tsx +69 -71
  10. package/templates/agency/app/(marketing)/work/page.tsx +20 -17
  11. package/templates/agency/app/globals.css +15 -6
  12. package/templates/agency/app.ts +10 -0
  13. package/templates/agency/components/marketing.tsx +40 -104
  14. package/templates/agency/lib/site.config.ts +34 -52
  15. package/templates/agency/public/images/hero.jpg +0 -0
  16. package/templates/agency/public/images/team/claire-donovan.jpg +0 -0
  17. package/templates/agency/public/images/team/dana-okafor.jpg +0 -0
  18. package/templates/agency/public/images/team/marcus-lee.jpg +0 -0
  19. package/templates/agency/public/images/work/atlas-health.jpg +0 -0
  20. package/templates/agency/public/images/work/cohort.jpg +0 -0
  21. package/templates/agency/public/images/work/ledger.jpg +0 -0
  22. package/templates/agency/public/images/work/vela.jpg +0 -0
  23. package/templates/ai-chat/AGENTS.md +1 -1
  24. package/templates/ai-studio/AGENTS.md +1 -1
  25. package/templates/barebones/AGENTS.md +1 -1
  26. package/templates/chat/AGENTS.md +1 -1
  27. package/templates/consumer/AGENTS.md +1 -1
  28. package/templates/consumer/README.md +39 -23
  29. package/templates/consumer/app/(app)/explore/page.tsx +13 -0
  30. package/templates/consumer/app/(app)/layout.tsx +17 -0
  31. package/templates/consumer/app/(app)/p/[id]/page.tsx +13 -0
  32. package/templates/consumer/app/(app)/page.tsx +15 -0
  33. package/templates/consumer/app/(app)/u/[username]/page.tsx +14 -0
  34. package/templates/consumer/app/error.tsx +1 -1
  35. package/templates/consumer/app/globals.css +25 -0
  36. package/templates/consumer/app/layout.tsx +3 -10
  37. package/templates/consumer/app/llms.ts +10 -10
  38. package/templates/consumer/app/not-found.tsx +9 -22
  39. package/templates/consumer/app/robots.ts +3 -4
  40. package/templates/consumer/app/sitemap.ts +7 -21
  41. package/templates/consumer/app.ts +152 -33
  42. package/templates/consumer/components/social/app-chrome.tsx +109 -0
  43. package/templates/consumer/components/social/avatar.tsx +50 -0
  44. package/templates/consumer/components/social/comment-form.tsx +51 -0
  45. package/templates/consumer/components/social/create-post.tsx +155 -0
  46. package/templates/consumer/components/social/edit-profile.tsx +103 -0
  47. package/templates/consumer/components/social/explore-screen.tsx +23 -0
  48. package/templates/consumer/components/social/home-screen.tsx +161 -0
  49. package/templates/consumer/components/social/post-card.tsx +77 -0
  50. package/templates/consumer/components/social/post-grid.tsx +46 -0
  51. package/templates/consumer/components/social/post-parts.tsx +146 -0
  52. package/templates/consumer/components/social/post-screen.tsx +173 -0
  53. package/templates/consumer/components/social/profile-screen.tsx +146 -0
  54. package/templates/consumer/components/social/rich-text.tsx +22 -0
  55. package/templates/consumer/components/social/session.tsx +43 -0
  56. package/templates/consumer/components/social/use-social.ts +86 -0
  57. package/templates/consumer/functions/addComment.ts +27 -0
  58. package/templates/consumer/functions/createPost.ts +39 -0
  59. package/templates/consumer/functions/ensureProfile.ts +39 -0
  60. package/templates/consumer/functions/seedFeed.ts +38 -0
  61. package/templates/consumer/functions/updateProfile.ts +61 -0
  62. package/templates/consumer/lib/seed.ts +192 -0
  63. package/templates/consumer/lib/site.ts +6 -0
  64. package/templates/consumer/lib/social.ts +245 -0
  65. package/templates/consumer/tests/social.test.ts +157 -0
  66. package/templates/creator/AGENTS.md +1 -1
  67. package/templates/creator/app/(marketing)/layout.tsx +35 -69
  68. package/templates/creator/app/(marketing)/newsletter-signup.tsx +15 -26
  69. package/templates/creator/app/(marketing)/not-found.tsx +8 -6
  70. package/templates/creator/app/(marketing)/page.tsx +90 -106
  71. package/templates/creator/app/globals.css +14 -0
  72. package/templates/creator/app/layout.tsx +1 -0
  73. package/templates/creator/app.ts +10 -0
  74. package/templates/creator/components/marketing.tsx +28 -89
  75. package/templates/creator/lib/site.config.ts +51 -29
  76. package/templates/creator/public/images/headshot.jpg +0 -0
  77. package/templates/crm/AGENTS.md +1 -1
  78. package/templates/crm/lib/seed.ts +153 -21
  79. package/templates/directory/AGENTS.md +1 -1
  80. package/templates/directory/app/(marketing)/directory-browse.tsx +99 -137
  81. package/templates/directory/app/(marketing)/layout.tsx +37 -88
  82. package/templates/directory/app/(marketing)/not-found.tsx +11 -10
  83. package/templates/directory/app/(marketing)/page.tsx +12 -33
  84. package/templates/directory/app/(marketing)/submit/page.tsx +9 -10
  85. package/templates/directory/app/(marketing)/submit-form.tsx +26 -26
  86. package/templates/directory/app/globals.css +15 -4
  87. package/templates/directory/app.ts +10 -0
  88. package/templates/directory/components/marketing.tsx +27 -91
  89. package/templates/directory/lib/site.config.ts +13 -24
  90. package/templates/helpdesk/AGENTS.md +1 -1
  91. package/templates/helpdesk/lib/seed.ts +188 -104
  92. package/templates/inventory/AGENTS.md +1 -1
  93. package/templates/inventory/lib/seed.ts +175 -44
  94. package/templates/invoices/AGENTS.md +1 -1
  95. package/templates/invoices/lib/seed.ts +74 -89
  96. package/templates/local-service/AGENTS.md +1 -1
  97. package/templates/local-service/app/(marketing)/booking-widget.tsx +31 -31
  98. package/templates/local-service/app/(marketing)/layout.tsx +25 -37
  99. package/templates/local-service/app/(marketing)/not-found.tsx +4 -4
  100. package/templates/local-service/app/(marketing)/page.tsx +145 -139
  101. package/templates/local-service/app/globals.css +11 -0
  102. package/templates/local-service/app/layout.tsx +2 -0
  103. package/templates/local-service/app.ts +9 -0
  104. package/templates/local-service/components/marketing.tsx +14 -39
  105. package/templates/local-service/lib/site.config.ts +16 -23
  106. package/templates/local-service/public/images/hero.jpg +0 -0
  107. package/templates/marketplace/AGENTS.md +1 -1
  108. package/templates/marketplace/client/SellForm.tsx +2 -0
  109. package/templates/projects/AGENTS.md +1 -1
  110. package/templates/projects/lib/seed.ts +103 -32
  111. package/templates/restaurant/AGENTS.md +1 -1
  112. package/templates/restaurant/app/(marketing)/layout.tsx +26 -48
  113. package/templates/restaurant/app/(marketing)/page.tsx +146 -123
  114. package/templates/restaurant/app/globals.css +11 -0
  115. package/templates/restaurant/app/layout.tsx +1 -0
  116. package/templates/restaurant/app.ts +10 -0
  117. package/templates/restaurant/components/marketing.tsx +10 -0
  118. package/templates/restaurant/lib/site.config.ts +16 -14
  119. package/templates/restaurant/public/images/bar.jpg +0 -0
  120. package/templates/restaurant/public/images/branzino.jpg +0 -0
  121. package/templates/restaurant/public/images/hearth.jpg +0 -0
  122. package/templates/restaurant/public/images/hero.jpg +0 -0
  123. package/templates/saas/AGENTS.md +1 -1
  124. package/templates/shop/AGENTS.md +1 -1
  125. package/templates/shop/public/images/products/cedar-smoke.jpg +0 -0
  126. package/templates/shop/public/images/products/linen.jpg +0 -0
  127. package/templates/todo/AGENTS.md +1 -1
  128. package/templates/waitlist/AGENTS.md +1 -1
  129. package/templates/waitlist/app/(marketing)/layout.tsx +39 -85
  130. package/templates/waitlist/app/(marketing)/not-found.tsx +8 -9
  131. package/templates/waitlist/app/(marketing)/page.tsx +51 -86
  132. package/templates/waitlist/app/(marketing)/waitlist-hero.tsx +43 -83
  133. package/templates/waitlist/app/globals.css +23 -0
  134. package/templates/waitlist/app/layout.tsx +1 -0
  135. package/templates/waitlist/app.ts +18 -0
  136. package/templates/waitlist/components/marketing.tsx +67 -81
  137. package/templates/waitlist/lib/site.config.ts +52 -59
  138. package/templates/consumer/app/feed-client.tsx +0 -159
  139. package/templates/consumer/app/page.tsx +0 -27
  140. package/templates/consumer/functions/_keep.ts +0 -13
  141. package/templates/consumer/functions/seedPosts.ts +0 -56
  142. package/templates/consumer/tests/example.test.ts +0 -12
@@ -11,14 +11,16 @@
11
11
 
12
12
  @custom-variant dark (&:where(.dark, .dark *));
13
13
 
14
- /* shadcn/ui design tokens (new-york / zinc) + the marketing brand accent. The
15
- three brand vars are defaults — app/layout.tsx overrides them from
16
- lib/site.config.ts on <html>, so re-theming the whole page is one edit there. */
14
+ /* shadcn/ui design tokens (new-york / zinc) + the marketing site colors. The
15
+ marketing page is white with black type; `--brand` is used for links only,
16
+ `--paper` is the light grey band behind the contact form. app/layout.tsx
17
+ overrides these three from lib/site.config.ts on <html>. */
17
18
  :root {
18
19
  --radius: 0.625rem;
19
- --brand: #4f46e5;
20
- --brand-soft: #eef2ff;
21
- --paper: #fafafa;
20
+ --ink: #111111;
21
+ --brand: #1d4ed8;
22
+ --brand-soft: #dbeafe;
23
+ --paper: #f4f4f2;
22
24
  --background: oklch(1 0 0);
23
25
  --foreground: oklch(0.141 0.005 285.823);
24
26
  --card: oklch(1 0 0);
@@ -109,6 +111,7 @@
109
111
  --color-border: var(--border);
110
112
  --color-input: var(--input);
111
113
  --color-ring: var(--ring);
114
+ --color-ink: var(--ink);
112
115
  --color-brand: var(--brand);
113
116
  --color-brand-soft: var(--brand-soft);
114
117
  --color-paper: var(--paper);
@@ -153,3 +156,9 @@
153
156
  cursor: pointer;
154
157
  }
155
158
  }
159
+
160
+ /* Headlines on the marketing pages. Instrument Serif is declared in app.ts. */
161
+ .font-display {
162
+ font-family: var(--font-display, "Iowan Old Style", "Palatino Linotype", Georgia, serif);
163
+ font-weight: 400;
164
+ }
@@ -296,6 +296,16 @@ const manifest = buildManifest({
296
296
  display: "swap",
297
297
  preload: true,
298
298
  }),
299
+ // The display face for headlines. Normal weight, set large.
300
+ font({
301
+ family: "Instrument Serif",
302
+ variable: "--font-display",
303
+ weights: ["400"],
304
+ styles: ["normal", "italic"],
305
+ subsets: ["latin"],
306
+ display: "swap",
307
+ preload: true,
308
+ }),
299
309
  ],
300
310
  routes: await discoverAppRoutes(),
301
311
  });
@@ -2,139 +2,65 @@ import React from "react";
2
2
  import { Link } from "@pylonsync/react";
3
3
  import type { ProjectView } from "@/lib/agency";
4
4
 
5
- // Reusable presentational pieces for the landing page. All server-rendered —
6
- // no client JS. Restyle here and the whole page follows. The brand accent
7
- // (`text-brand`, `bg-brand-soft`) comes from CSS vars set on <html> in
8
- // app/layout.tsx, which read lib/site.config.ts — so re-theming is one edit.
5
+ // Reusable presentational pieces for the marketing pages. All server-rendered,
6
+ // no client JS. The site is white with black type; `--brand` (set on <html>
7
+ // from lib/site.config.ts) colors links only, and `--paper` is the grey band
8
+ // behind the contact form. Headlines use `.font-display` (Instrument Serif,
9
+ // declared in app.ts).
9
10
 
10
11
  // Shared container: a contained, centered column.
11
- export const WRAP = "mx-auto w-full max-w-5xl px-6";
12
+ export const WRAP = "mx-auto w-full max-w-6xl px-6";
12
13
 
13
- export function Eyebrow({ children }: { children: React.ReactNode }) {
14
- return (
15
- <p className="font-mono text-[11px] font-semibold uppercase tracking-[0.14em] text-brand">
16
- {children}
17
- </p>
18
- );
19
- }
14
+ // A narrow measure for running text (case studies, intros).
15
+ export const WRAP_NARROW = "mx-auto w-full max-w-2xl px-6";
20
16
 
21
- // "New / Coming soon"-style pill for the hero.
22
- export function Badge({ children }: { children: React.ReactNode }) {
23
- return (
24
- <span className="inline-flex items-center gap-2 rounded-full border border-zinc-200 bg-white py-1 pl-1.5 pr-3 text-[13px] text-zinc-600 shadow-sm">
25
- <span className="inline-block size-1.5 rounded-full bg-brand" />
26
- {children}
27
- </span>
28
- );
29
- }
17
+ // A text link: black, underlined, brand color on hover.
18
+ export const TEXT_LINK =
19
+ "underline decoration-zinc-300 underline-offset-[5px] transition-colors hover:text-brand hover:decoration-brand";
30
20
 
31
21
  export function Divider() {
32
22
  return (
33
23
  <div className={WRAP}>
34
- <div className="border-t border-zinc-200/70" />
24
+ <div className="border-t border-zinc-200" />
35
25
  </div>
36
26
  );
37
27
  }
38
28
 
39
- export function SectionHead({
40
- eyebrow,
41
- title,
42
- body,
43
- }: {
44
- eyebrow: string;
45
- title: string;
46
- body?: string;
47
- }) {
29
+ // A section heading: one large serif line, optionally a short paragraph.
30
+ export function SectionTitle({ title, body }: { title: string; body?: string }) {
48
31
  return (
49
32
  <div>
50
- <Eyebrow>{eyebrow}</Eyebrow>
51
- <h2 className="mt-4 text-balance text-2xl font-semibold leading-[1.15] tracking-[-0.02em] sm:text-3xl">
33
+ <h2 className="font-display text-balance text-[2.25rem] leading-[1.05] tracking-[-0.01em] text-ink sm:text-[2.75rem]">
52
34
  {title}
53
35
  </h2>
54
- {body ? (
55
- <p className="mt-4 max-w-xl text-[15px] leading-relaxed text-zinc-500">
56
- {body}
57
- </p>
58
- ) : null}
36
+ {body ? <p className="mt-4 max-w-xl text-[16px] leading-relaxed text-zinc-600">{body}</p> : null}
59
37
  </div>
60
38
  );
61
39
  }
62
40
 
63
- // A grid of value props — icon + title + body.
64
- export function FeatureGrid({
65
- items,
66
- }: {
67
- items: { title: string; body: string; icon?: string }[];
68
- }) {
69
- return (
70
- <div className="grid gap-6 sm:grid-cols-3">
71
- {items.map((f) => (
72
- <div key={f.title}>
73
- {f.icon ? (
74
- <span className="flex size-9 items-center justify-center rounded-lg bg-brand-soft text-brand">
75
- {f.icon}
76
- </span>
77
- ) : null}
78
- <h3 className="mt-4 text-[15px] font-semibold text-zinc-900">
79
- {f.title}
80
- </h3>
81
- <p className="mt-2 text-[14px] leading-relaxed text-zinc-500">
82
- {f.body}
83
- </p>
84
- </div>
85
- ))}
86
- </div>
87
- );
88
- }
89
-
90
- // Initials for testimonial avatars, so the cards look finished without a photo.
91
- export function initials(name: string) {
92
- return name
93
- .split(/\s+/)
94
- .map((w) => w[0])
95
- .join("")
96
- .slice(0, 2)
97
- .toUpperCase();
98
- }
99
-
100
- // A linked portfolio card — the project's cover, title, client label, summary,
101
- // and tag chips, linking to its /work/[slug] case study. Used on the homepage
102
- // "Selected work" grid and the full /work index, so both stay in lockstep.
41
+ // A linked portfolio entry: the 4:3 cover, then the title, client label, and
42
+ // a one-line summary in small type. Used on the homepage work grid and the
43
+ // full /work index, so both stay in lockstep.
103
44
  export function ProjectCard({ p }: { p: ProjectView }) {
104
45
  return (
105
46
  <Link href={`/work/${p.slug}`} className="group block">
106
- {/* Case-study cover — drop in a real project screenshot. */}
107
- <ImagePlaceholder
108
- shape="landscape"
109
- title={`${p.title} — project shot`}
110
- hint="Swap for an <img> per case study"
111
- />
112
- <div className="mt-4 flex items-baseline justify-between gap-3">
113
- <h3 className="text-[16px] font-semibold text-zinc-900 transition-colors group-hover:text-brand">
47
+ {/* Case-study cover: public/images/work/<slug>.jpg. */}
48
+ <ImagePlaceholder shape="landscape" title={`${p.title} product shot`} src={`/images/work/${p.slug}.jpg`} />
49
+ <div className="mt-4 flex items-baseline justify-between gap-4">
50
+ <h3 className="font-display text-[1.5rem] leading-none text-ink transition-colors group-hover:text-brand">
114
51
  {p.title}
115
52
  </h3>
116
- <span className="shrink-0 font-mono text-[11px] uppercase tracking-wide text-zinc-400">
53
+ <span className="shrink-0 text-[13px] text-zinc-500">
117
54
  {p.client}
55
+ {p.year ? `, ${p.year}` : ""}
118
56
  </span>
119
57
  </div>
120
- <p className="mt-1.5 text-[14px] leading-relaxed text-zinc-500">{p.summary}</p>
121
- {p.tags.length > 0 ? (
122
- <div className="mt-3 flex flex-wrap gap-1.5">
123
- {p.tags.map((t) => (
124
- <span key={t} className="rounded-full bg-zinc-100 px-2.5 py-0.5 text-[11px] font-medium text-zinc-600">
125
- {t}
126
- </span>
127
- ))}
128
- </div>
129
- ) : null}
130
- <span className="mt-3 inline-flex items-center gap-1 text-[13px] font-medium text-brand opacity-0 transition-opacity group-hover:opacity-100">
131
- Read case study →
132
- </span>
58
+ <p className="mt-2 text-[14px] leading-relaxed text-zinc-600">{p.summary}</p>
133
59
  </Link>
134
60
  );
135
61
  }
136
62
 
137
- // A deliberately-obvious image placeholder for spots where a real photo belongs:
63
+ // A clearly marked image placeholder for spots where a real photo belongs:
138
64
  // dashed border, a photo glyph, and a one-line "swap this" instruction.
139
65
  //
140
66
  // shape — "landscape" | "portrait" | "square" | "circle"
@@ -144,23 +70,33 @@ export function ImagePlaceholder({
144
70
  shape = "landscape",
145
71
  title,
146
72
  hint,
73
+ src,
147
74
  className = "",
148
75
  }: {
149
76
  shape?: "landscape" | "portrait" | "square" | "circle";
150
77
  title: string;
151
78
  hint?: string;
79
+ /** A real image. The template ships one under public/images; swap it for yours. */
80
+ src?: string;
152
81
  className?: string;
153
82
  }) {
154
83
  const aspect =
155
84
  shape === "portrait"
156
- ? "aspect-[4/5]"
85
+ ? "aspect-[3/4]"
157
86
  : shape === "square" || shape === "circle"
158
87
  ? "aspect-square"
159
88
  : "aspect-[4/3]";
160
- const radius = shape === "circle" ? "rounded-full" : "rounded-2xl";
89
+ const radius = shape === "circle" ? "rounded-full" : "rounded-sm";
90
+ if (src) {
91
+ return (
92
+ <div className={`relative overflow-hidden bg-zinc-100 ${aspect} ${radius} ${className}`}>
93
+ <img src={src} alt={title} className="absolute inset-0 size-full object-cover" loading="lazy" />
94
+ </div>
95
+ );
96
+ }
161
97
  return (
162
98
  <div
163
- className={`relative grid place-items-center overflow-hidden border-2 border-dashed border-zinc-300 bg-zinc-50 ${aspect} ${radius} ${className}`}
99
+ className={`relative grid place-items-center overflow-hidden border border-dashed border-zinc-300 bg-zinc-50 ${aspect} ${radius} ${className}`}
164
100
  >
165
101
  <div className="px-4 text-center">
166
102
  <svg
@@ -24,7 +24,7 @@ export type BaseConfig = {
24
24
  seo: { title: string; description: string };
25
25
  };
26
26
 
27
- export type Service = { title: string; body: string; icon?: string };
27
+ export type Service = { title: string; body: string };
28
28
 
29
29
  // A portfolio piece + case study. `seedProjects` writes these into the public
30
30
  // Project entity on first visit; after that the owner curates them from the
@@ -82,7 +82,6 @@ export type Billing = {
82
82
 
83
83
  export type AgencyConfig = BaseConfig & {
84
84
  hero: {
85
- tagline: string;
86
85
  headline: string;
87
86
  subcopy: string;
88
87
  ctaLabel: string;
@@ -92,18 +91,16 @@ export type AgencyConfig = BaseConfig & {
92
91
  // and the owner manages it from the dashboard. `openSlots` is the number the
93
92
  // hero shows live; `label` is the booking window it refers to.
94
93
  capacity: { label: string; openSlots: number };
95
- logos: { eyebrow: string; names: string[] };
96
- services: { eyebrow: string; headline: string; items: Service[] };
97
- work: { eyebrow: string; headline: string; items: CaseStudy[] };
94
+ services: { headline: string; items: Service[] };
95
+ work: { headline: string; items: CaseStudy[] };
98
96
  // The studio's invoice "from" identity + terms.
99
97
  billing: Billing;
100
98
  // Demo CRM + billing rows seeded into the owner dashboard on first visit.
101
99
  backoffice: { clients: ClientSeed[]; invoices: InvoiceSeed[] };
102
- process: { eyebrow: string; headline: string; steps: ProcessStep[] };
103
- team: { eyebrow: string; headline: string; members: TeamMember[] };
104
- testimonials?: { eyebrow: string; headline: string; items: Testimonial[] };
100
+ process: { headline: string; steps: ProcessStep[] };
101
+ team: { headline: string; members: TeamMember[] };
102
+ testimonials?: { headline: string; items: Testimonial[] };
105
103
  contact: {
106
- eyebrow: string;
107
104
  headline: string;
108
105
  subcopy: string;
109
106
  projectTypes: string[];
@@ -121,7 +118,7 @@ export const siteConfig: AgencyConfig = {
121
118
  domain: "halyard.studio",
122
119
  email: "hello@halyard.example",
123
120
  footerBlurb:
124
- "A Dallas product studio that designs and builds software for ambitious teams. We take on only a few projects at a time.",
121
+ "A Dallas product studio. We design and build web and mobile software and take on a few projects at a time.",
125
122
  copyrightName: "Halyard Studio",
126
123
  socials: [
127
124
  {
@@ -137,60 +134,49 @@ export const siteConfig: AgencyConfig = {
137
134
  ],
138
135
  },
139
136
 
140
- colors: { brand: "#4f46e5", brandSoft: "#e0e7ff", paper: "#fafafa" },
137
+ // `brand` colors links only. `paper` is the grey band behind the contact form.
138
+ colors: { brand: "#1d4ed8", brandSoft: "#dbeafe", paper: "#f4f4f2" },
141
139
 
142
140
  seo: {
143
- title: "Halyard — a product studio for ambitious teams.",
141
+ title: "Halyard, a product studio in Dallas",
144
142
  description:
145
- "Halyard is a Dallas product studio for web and mobile software. See how many project slots are open this quarter.",
143
+ "Halyard is a Dallas product studio that designs and builds web and mobile software. See how many project slots are open this quarter.",
146
144
  },
147
145
 
148
146
  hero: {
149
- tagline: "Product studio · Dallas",
150
- headline: "We build the products teams bet on.",
147
+ headline: "Halyard designs and builds web and mobile products for funded teams that need to ship this quarter.",
151
148
  subcopy:
152
- "Halyard is a small, senior team that designs and ships web and mobile software. We take on a handful of projects at a time so each launch gets the team's attention.",
149
+ "We are a small senior team in Dallas. We take on a few projects at a time and work in your repo from the first week.",
153
150
  ctaLabel: "Start a project",
154
151
  secondaryCtaLabel: "See our work",
155
152
  },
156
153
 
157
154
  capacity: { label: "Q3 2026", openSlots: 3 },
158
155
 
159
- logos: {
160
- eyebrow: "Trusted by teams at",
161
- names: ["Northwind", "Lumen", "Foundry", "Atlas", "Cohort", "Vela"],
162
- },
163
-
164
156
  services: {
165
- eyebrow: "What we do",
166
- headline: "One team, the whole build.",
157
+ headline: "What we do",
167
158
  items: [
168
159
  {
169
- icon: "◆",
170
160
  title: "Product design",
171
- body: "Research, flows, and interface design that turns a fuzzy idea into something people understand in seconds.",
161
+ body: "Research, flows, and interface design. We test a clickable prototype with real users before anything is built.",
172
162
  },
173
163
  {
174
- icon: "◇",
175
- title: "Web & mobile",
176
- body: "Production engineering across web, iOS, and Android — typed, tested, and built to scale past launch day.",
164
+ title: "Web and mobile engineering",
165
+ body: "Production code for web, iOS, and Android. Typed, tested, and committed to your repository from day one.",
177
166
  },
178
167
  {
179
- icon: "◈",
180
- title: "Brand & identity",
181
- body: "Naming, logo, and a system that makes a young product feel like it's been around — and worth paying for.",
168
+ title: "Brand and identity",
169
+ body: "Naming, logo, type, and a visual system with the files and rules your team needs to keep using it.",
182
170
  },
183
171
  {
184
- icon: "✦",
185
- title: "Fractional team",
186
- body: "Embed with your team for a quarter. We plan, build, and hand off — leaving you faster than we found you.",
172
+ title: "Embedded team",
173
+ body: "Two or three of us join your team for a quarter. We plan, build, and hand off with documentation.",
187
174
  },
188
175
  ],
189
176
  },
190
177
 
191
178
  work: {
192
- eyebrow: "Selected work",
193
- headline: "A few things we've shipped.",
179
+ headline: "Work",
194
180
  items: [
195
181
  {
196
182
  title: "Ledger",
@@ -345,12 +331,11 @@ export const siteConfig: AgencyConfig = {
345
331
  },
346
332
 
347
333
  process: {
348
- eyebrow: "How we work",
349
- headline: "Senior, hands-on, and fast.",
334
+ headline: "How a project runs",
350
335
  steps: [
351
336
  {
352
337
  title: "Scope",
353
- body: "A focused kickoff week. We pin down the real problem, the riskiest unknowns, and what a great launch looks like.",
338
+ body: "One week. We agree on the problem, the riskiest unknowns, and what launch has to include.",
354
339
  },
355
340
  {
356
341
  title: "Design",
@@ -358,18 +343,17 @@ export const siteConfig: AgencyConfig = {
358
343
  },
359
344
  {
360
345
  title: "Build",
361
- body: "Weekly shipping you can watch. Typed, reviewed code in your repo from day one, no black box.",
346
+ body: "We ship every week to a staging build you can open. All code lives in your repository.",
362
347
  },
363
348
  {
364
- title: "Launch & hand off",
365
- body: "We ship it, document it, and leave your team able to run and extend it without us.",
349
+ title: "Launch and hand off",
350
+ body: "We release, write the docs, and stay for two weeks after launch to fix what comes up.",
366
351
  },
367
352
  ],
368
353
  },
369
354
 
370
355
  team: {
371
- eyebrow: "Who you'll work with",
372
- headline: "A small, senior team.",
356
+ headline: "Who you will work with",
373
357
  members: [
374
358
  { name: "Claire Donovan", role: "Principal, Design" },
375
359
  { name: "Marcus Lee", role: "Principal, Engineering" },
@@ -378,18 +362,17 @@ export const siteConfig: AgencyConfig = {
378
362
  },
379
363
 
380
364
  testimonials: {
381
- eyebrow: "Kind words",
382
- headline: "Teams we've worked with.",
365
+ headline: "From clients",
383
366
  items: [
384
367
  {
385
368
  quote:
386
- "Halyard shipped in a quarter what our last agency couldn't in a year. Senior people, no hand-offs, no drama.",
369
+ "Halyard shipped in a quarter what our last agency could not in a year. Senior people, no hand-offs.",
387
370
  name: "Erin Caldwell",
388
371
  role: "CEO, Ledger",
389
372
  },
390
373
  {
391
374
  quote:
392
- "They felt like our team, not a vendor. The work was sharp and the launch was the smoothest we've had.",
375
+ "They worked like part of our team. The rollout had zero downtime, which we had never managed before.",
393
376
  name: "Tom Reyes",
394
377
  role: "VP Product, Atlas Health",
395
378
  },
@@ -403,13 +386,12 @@ export const siteConfig: AgencyConfig = {
403
386
  },
404
387
 
405
388
  contact: {
406
- eyebrow: "Start a project",
407
- headline: "Tell us what you're building.",
389
+ headline: "Tell us what you are building",
408
390
  subcopy:
409
- "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.",
391
+ "We take on a few projects at a time. Send a note and we reply within two business days. If we are full, we say so and suggest another studio.",
410
392
  projectTypes: ["New product (0→1)", "Existing product", "Rebrand", "Not sure yet"],
411
393
  budgets: ["$25–50k", "$50–100k", "$100k+", "Let's talk"],
412
394
  confirmationMessage:
413
- "Thanks — your note's in. We'll get back to you within two business days.",
395
+ "Thanks. Your note is in. We reply within two business days.",
414
396
  },
415
397
  };
@@ -84,7 +84,7 @@ export default action({
84
84
  ```
85
85
 
86
86
  - **It's `db.useQueryOne`, not `useOne`.** Validators and field types have aliases: `v.bool`/`v.boolean`, `v.float`/`v.number`.
87
- - **Use the supported file and scheduling APIs.** Files go through `<FileUpload>` and `/api/files/*`; 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`.
87
+ - **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`.
88
88
 
89
89
  ## Testing
90
90
 
@@ -84,7 +84,7 @@ export default action({
84
84
  ```
85
85
 
86
86
  - **It's `db.useQueryOne`, not `useOne`.** Validators and field types have aliases: `v.bool`/`v.boolean`, `v.float`/`v.number`.
87
- - **Use the supported file and scheduling APIs.** Files go through `<FileUpload>` and `/api/files/*`; 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`.
87
+ - **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`.
88
88
 
89
89
  ## Testing
90
90
 
@@ -65,7 +65,7 @@ this pass before the user has to ask.
65
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`. Streaming does NOT extend the call deadline (`PYLON_FN_CALL_TIMEOUT`, 30s default) — set `timeout: <secs>` on the def for a long run.
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
- - **Use the supported file and scheduling APIs.** Files go through `<FileUpload>` and `/api/files/*`; 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`.
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`.
69
69
 
70
70
  ## Testing
71
71
 
@@ -84,7 +84,7 @@ export default action({
84
84
  ```
85
85
 
86
86
  - **It's `db.useQueryOne`, not `useOne`.** Validators and field types have aliases: `v.bool`/`v.boolean`, `v.float`/`v.number`.
87
- - **Use the supported file and scheduling APIs.** Files go through `<FileUpload>` and `/api/files/*`; 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`.
87
+ - **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`.
88
88
 
89
89
  ## Testing
90
90
 
@@ -65,7 +65,7 @@ this pass before the user has to ask.
65
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`. Streaming does NOT extend the call deadline (`PYLON_FN_CALL_TIMEOUT`, 30s default) — set `timeout: <secs>` on the def for a long run.
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
- - **Use the supported file and scheduling APIs.** Files go through `<FileUpload>` and `/api/files/*`; 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`.
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`.
69
69
 
70
70
  ## Testing
71
71