@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.
- package/bin/create-pylon.js +1 -1
- package/package.json +1 -1
- package/templates/_root/AGENTS.md +1 -1
- package/templates/agency/AGENTS.md +1 -1
- package/templates/agency/app/(marketing)/contact-form.tsx +22 -32
- package/templates/agency/app/(marketing)/layout.tsx +36 -64
- package/templates/agency/app/(marketing)/not-found.tsx +6 -8
- package/templates/agency/app/(marketing)/page.tsx +93 -144
- package/templates/agency/app/(marketing)/work/[slug]/page.tsx +69 -71
- package/templates/agency/app/(marketing)/work/page.tsx +20 -17
- package/templates/agency/app/globals.css +15 -6
- package/templates/agency/app.ts +10 -0
- package/templates/agency/components/marketing.tsx +40 -104
- package/templates/agency/lib/site.config.ts +34 -52
- package/templates/agency/public/images/hero.jpg +0 -0
- package/templates/agency/public/images/team/claire-donovan.jpg +0 -0
- package/templates/agency/public/images/team/dana-okafor.jpg +0 -0
- package/templates/agency/public/images/team/marcus-lee.jpg +0 -0
- package/templates/agency/public/images/work/atlas-health.jpg +0 -0
- package/templates/agency/public/images/work/cohort.jpg +0 -0
- package/templates/agency/public/images/work/ledger.jpg +0 -0
- package/templates/agency/public/images/work/vela.jpg +0 -0
- package/templates/ai-chat/AGENTS.md +1 -1
- package/templates/ai-studio/AGENTS.md +1 -1
- package/templates/barebones/AGENTS.md +1 -1
- package/templates/chat/AGENTS.md +1 -1
- package/templates/consumer/AGENTS.md +1 -1
- package/templates/consumer/README.md +39 -23
- package/templates/consumer/app/(app)/explore/page.tsx +13 -0
- package/templates/consumer/app/(app)/layout.tsx +17 -0
- package/templates/consumer/app/(app)/p/[id]/page.tsx +13 -0
- package/templates/consumer/app/(app)/page.tsx +15 -0
- package/templates/consumer/app/(app)/u/[username]/page.tsx +14 -0
- package/templates/consumer/app/error.tsx +1 -1
- package/templates/consumer/app/globals.css +25 -0
- package/templates/consumer/app/layout.tsx +3 -10
- package/templates/consumer/app/llms.ts +10 -10
- package/templates/consumer/app/not-found.tsx +9 -22
- package/templates/consumer/app/robots.ts +3 -4
- package/templates/consumer/app/sitemap.ts +7 -21
- package/templates/consumer/app.ts +152 -33
- package/templates/consumer/components/social/app-chrome.tsx +109 -0
- package/templates/consumer/components/social/avatar.tsx +50 -0
- package/templates/consumer/components/social/comment-form.tsx +51 -0
- package/templates/consumer/components/social/create-post.tsx +155 -0
- package/templates/consumer/components/social/edit-profile.tsx +103 -0
- package/templates/consumer/components/social/explore-screen.tsx +23 -0
- package/templates/consumer/components/social/home-screen.tsx +161 -0
- package/templates/consumer/components/social/post-card.tsx +77 -0
- package/templates/consumer/components/social/post-grid.tsx +46 -0
- package/templates/consumer/components/social/post-parts.tsx +146 -0
- package/templates/consumer/components/social/post-screen.tsx +173 -0
- package/templates/consumer/components/social/profile-screen.tsx +146 -0
- package/templates/consumer/components/social/rich-text.tsx +22 -0
- package/templates/consumer/components/social/session.tsx +43 -0
- package/templates/consumer/components/social/use-social.ts +86 -0
- package/templates/consumer/functions/addComment.ts +27 -0
- package/templates/consumer/functions/createPost.ts +39 -0
- package/templates/consumer/functions/ensureProfile.ts +39 -0
- package/templates/consumer/functions/seedFeed.ts +38 -0
- package/templates/consumer/functions/updateProfile.ts +61 -0
- package/templates/consumer/lib/seed.ts +192 -0
- package/templates/consumer/lib/site.ts +6 -0
- package/templates/consumer/lib/social.ts +245 -0
- package/templates/consumer/tests/social.test.ts +157 -0
- package/templates/creator/AGENTS.md +1 -1
- package/templates/creator/app/(marketing)/layout.tsx +35 -69
- package/templates/creator/app/(marketing)/newsletter-signup.tsx +15 -26
- package/templates/creator/app/(marketing)/not-found.tsx +8 -6
- package/templates/creator/app/(marketing)/page.tsx +90 -106
- package/templates/creator/app/globals.css +14 -0
- package/templates/creator/app/layout.tsx +1 -0
- package/templates/creator/app.ts +10 -0
- package/templates/creator/components/marketing.tsx +28 -89
- package/templates/creator/lib/site.config.ts +51 -29
- package/templates/creator/public/images/headshot.jpg +0 -0
- package/templates/crm/AGENTS.md +1 -1
- package/templates/crm/lib/seed.ts +153 -21
- package/templates/directory/AGENTS.md +1 -1
- package/templates/directory/app/(marketing)/directory-browse.tsx +99 -137
- package/templates/directory/app/(marketing)/layout.tsx +37 -88
- package/templates/directory/app/(marketing)/not-found.tsx +11 -10
- package/templates/directory/app/(marketing)/page.tsx +12 -33
- package/templates/directory/app/(marketing)/submit/page.tsx +9 -10
- package/templates/directory/app/(marketing)/submit-form.tsx +26 -26
- package/templates/directory/app/globals.css +15 -4
- package/templates/directory/app.ts +10 -0
- package/templates/directory/components/marketing.tsx +27 -91
- package/templates/directory/lib/site.config.ts +13 -24
- package/templates/helpdesk/AGENTS.md +1 -1
- package/templates/helpdesk/lib/seed.ts +188 -104
- package/templates/inventory/AGENTS.md +1 -1
- package/templates/inventory/lib/seed.ts +175 -44
- package/templates/invoices/AGENTS.md +1 -1
- package/templates/invoices/lib/seed.ts +74 -89
- package/templates/local-service/AGENTS.md +1 -1
- package/templates/local-service/app/(marketing)/booking-widget.tsx +31 -31
- package/templates/local-service/app/(marketing)/layout.tsx +25 -37
- package/templates/local-service/app/(marketing)/not-found.tsx +4 -4
- package/templates/local-service/app/(marketing)/page.tsx +145 -139
- package/templates/local-service/app/globals.css +11 -0
- package/templates/local-service/app/layout.tsx +2 -0
- package/templates/local-service/app.ts +9 -0
- package/templates/local-service/components/marketing.tsx +14 -39
- package/templates/local-service/lib/site.config.ts +16 -23
- package/templates/local-service/public/images/hero.jpg +0 -0
- package/templates/marketplace/AGENTS.md +1 -1
- package/templates/marketplace/client/SellForm.tsx +2 -0
- package/templates/projects/AGENTS.md +1 -1
- package/templates/projects/lib/seed.ts +103 -32
- package/templates/restaurant/AGENTS.md +1 -1
- package/templates/restaurant/app/(marketing)/layout.tsx +26 -48
- package/templates/restaurant/app/(marketing)/page.tsx +146 -123
- package/templates/restaurant/app/globals.css +11 -0
- package/templates/restaurant/app/layout.tsx +1 -0
- package/templates/restaurant/app.ts +10 -0
- package/templates/restaurant/components/marketing.tsx +10 -0
- package/templates/restaurant/lib/site.config.ts +16 -14
- package/templates/restaurant/public/images/bar.jpg +0 -0
- package/templates/restaurant/public/images/branzino.jpg +0 -0
- package/templates/restaurant/public/images/hearth.jpg +0 -0
- package/templates/restaurant/public/images/hero.jpg +0 -0
- package/templates/saas/AGENTS.md +1 -1
- package/templates/shop/AGENTS.md +1 -1
- package/templates/shop/public/images/products/cedar-smoke.jpg +0 -0
- package/templates/shop/public/images/products/linen.jpg +0 -0
- package/templates/todo/AGENTS.md +1 -1
- package/templates/waitlist/AGENTS.md +1 -1
- package/templates/waitlist/app/(marketing)/layout.tsx +39 -85
- package/templates/waitlist/app/(marketing)/not-found.tsx +8 -9
- package/templates/waitlist/app/(marketing)/page.tsx +51 -86
- package/templates/waitlist/app/(marketing)/waitlist-hero.tsx +43 -83
- package/templates/waitlist/app/globals.css +23 -0
- package/templates/waitlist/app/layout.tsx +1 -0
- package/templates/waitlist/app.ts +18 -0
- package/templates/waitlist/components/marketing.tsx +67 -81
- package/templates/waitlist/lib/site.config.ts +52 -59
- package/templates/consumer/app/feed-client.tsx +0 -159
- package/templates/consumer/app/page.tsx +0 -27
- package/templates/consumer/functions/_keep.ts +0 -13
- package/templates/consumer/functions/seedPosts.ts +0 -56
- package/templates/consumer/tests/example.test.ts +0 -12
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
import React, { useState } from "react";
|
|
4
4
|
import { callFn } from "@pylonsync/react";
|
|
5
5
|
import { siteConfig } from "@/lib/site.config";
|
|
6
|
+
import { LINK } from "@/components/marketing";
|
|
6
7
|
|
|
7
8
|
// The "submit a tool" form. submitListing is a public mutation, so it works for
|
|
8
9
|
// anonymous visitors with no session. PRIVACY: the submission (incl. the
|
|
9
|
-
// submitter's email) is written to the deny-all Submission table
|
|
10
|
+
// submitter's email) is written to the deny-all Submission table. It is NOT
|
|
10
11
|
// public and never syncs to a browser. It shows up in the directory only after
|
|
11
12
|
// the owner approves it from the dashboard.
|
|
12
13
|
export function SubmitForm() {
|
|
@@ -33,7 +34,7 @@ export function SubmitForm() {
|
|
|
33
34
|
e.preventDefault();
|
|
34
35
|
if (status === "sending") return;
|
|
35
36
|
if (!form.submitterName.trim() || !form.submitterEmail.trim() || !form.name.trim() || !form.url.trim()) {
|
|
36
|
-
setError("Your name
|
|
37
|
+
setError("Your name, your email, the tool name, and the URL are required.");
|
|
37
38
|
return;
|
|
38
39
|
}
|
|
39
40
|
setStatus("sending");
|
|
@@ -58,8 +59,8 @@ export function SubmitForm() {
|
|
|
58
59
|
: /valid URL/i.test(msg)
|
|
59
60
|
? "Enter a valid URL (https://…)."
|
|
60
61
|
: /INVALID_ARGS/i.test(msg)
|
|
61
|
-
? "
|
|
62
|
-
: "
|
|
62
|
+
? "Fill in the required fields."
|
|
63
|
+
: "The submission failed. Try again in a moment.",
|
|
63
64
|
);
|
|
64
65
|
setStatus("idle");
|
|
65
66
|
}
|
|
@@ -67,10 +68,9 @@ export function SubmitForm() {
|
|
|
67
68
|
|
|
68
69
|
if (status === "done") {
|
|
69
70
|
return (
|
|
70
|
-
<div className="
|
|
71
|
-
<
|
|
72
|
-
<
|
|
73
|
-
<a href="/" className="mt-5 inline-flex h-10 items-center rounded-full border border-zinc-300 px-5 text-sm font-medium text-zinc-700 hover:bg-zinc-50">
|
|
71
|
+
<div className="border-t border-zinc-200 pt-4">
|
|
72
|
+
<p className="text-[14px] font-medium text-zinc-900">{submit.confirmationMessage}</p>
|
|
73
|
+
<a href="/" className={`${LINK} mt-2 inline-block text-[14px]`}>
|
|
74
74
|
Back to the directory
|
|
75
75
|
</a>
|
|
76
76
|
</div>
|
|
@@ -78,8 +78,8 @@ export function SubmitForm() {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
return (
|
|
81
|
-
<form onSubmit={onSubmit} className="
|
|
82
|
-
<div className="grid gap-
|
|
81
|
+
<form onSubmit={onSubmit} className="border-t border-zinc-200 pt-5">
|
|
82
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
83
83
|
<Field label="Your name" required>
|
|
84
84
|
<input value={form.submitterName} onChange={set("submitterName")} autoComplete="name" className={inputCls} />
|
|
85
85
|
</Field>
|
|
@@ -88,8 +88,8 @@ export function SubmitForm() {
|
|
|
88
88
|
</Field>
|
|
89
89
|
</div>
|
|
90
90
|
|
|
91
|
-
<div className="mt-
|
|
92
|
-
<div className="grid gap-
|
|
91
|
+
<div className="mt-5 border-t border-zinc-200 pt-5">
|
|
92
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
93
93
|
<Field label="Tool name" required>
|
|
94
94
|
<input value={form.name} onChange={set("name")} className={inputCls} />
|
|
95
95
|
</Field>
|
|
@@ -101,33 +101,33 @@ export function SubmitForm() {
|
|
|
101
101
|
</select>
|
|
102
102
|
</Field>
|
|
103
103
|
</div>
|
|
104
|
-
<div className="mt-
|
|
104
|
+
<div className="mt-4">
|
|
105
105
|
<Field label="URL" required>
|
|
106
|
-
<input value={form.url} onChange={set("url")} placeholder="https
|
|
106
|
+
<input value={form.url} onChange={set("url")} placeholder="https://" inputMode="url" className={`${inputCls} font-mono text-[13px]`} />
|
|
107
107
|
</Field>
|
|
108
108
|
</div>
|
|
109
|
-
<div className="mt-
|
|
110
|
-
<Field label="One-line
|
|
111
|
-
<input value={form.tagline} onChange={set("tagline")} placeholder="What it does, in
|
|
109
|
+
<div className="mt-4">
|
|
110
|
+
<Field label="One-line description">
|
|
111
|
+
<input value={form.tagline} onChange={set("tagline")} placeholder="What it does, in one sentence." className={inputCls} />
|
|
112
112
|
</Field>
|
|
113
113
|
</div>
|
|
114
|
-
<div className="mt-
|
|
114
|
+
<div className="mt-4">
|
|
115
115
|
<Field label="Tags">
|
|
116
|
-
<input value={form.tags} onChange={set("tags")} placeholder="comma, separated
|
|
116
|
+
<input value={form.tags} onChange={set("tags")} placeholder="comma, separated" className={`${inputCls} font-mono text-[13px]`} />
|
|
117
117
|
</Field>
|
|
118
118
|
</div>
|
|
119
|
-
<div className="mt-
|
|
120
|
-
<Field label="
|
|
121
|
-
<textarea value={form.description} onChange={set("description")} rows={3} className={inputCls
|
|
119
|
+
<div className="mt-4">
|
|
120
|
+
<Field label="Notes for the reviewer">
|
|
121
|
+
<textarea value={form.description} onChange={set("description")} rows={3} className={`${inputCls} h-auto resize-none py-2`} />
|
|
122
122
|
</Field>
|
|
123
123
|
</div>
|
|
124
124
|
</div>
|
|
125
125
|
|
|
126
|
-
{error ? <p className="mt-
|
|
126
|
+
{error ? <p className="mt-4 text-[13px] text-red-700">{error}</p> : null}
|
|
127
127
|
<button
|
|
128
128
|
type="submit"
|
|
129
129
|
disabled={status === "sending"}
|
|
130
|
-
className="mt-
|
|
130
|
+
className="mt-6 h-10 border border-brand bg-brand px-5 text-[14px] font-medium text-white hover:opacity-90 disabled:opacity-60"
|
|
131
131
|
>
|
|
132
132
|
{status === "sending" ? "Submitting…" : "Submit for review"}
|
|
133
133
|
</button>
|
|
@@ -138,7 +138,7 @@ export function SubmitForm() {
|
|
|
138
138
|
function Field({ label, required, children }: { label: string; required?: boolean; children: React.ReactNode }) {
|
|
139
139
|
return (
|
|
140
140
|
<label className="block">
|
|
141
|
-
<span className="mb-1
|
|
141
|
+
<span className="mb-1 block text-[12px] font-medium uppercase tracking-wide text-zinc-500">
|
|
142
142
|
{label}
|
|
143
143
|
{required ? <span className="text-brand"> *</span> : null}
|
|
144
144
|
</span>
|
|
@@ -148,4 +148,4 @@ function Field({ label, required, children }: { label: string; required?: boolea
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
const inputCls =
|
|
151
|
-
"h-10 w-full
|
|
151
|
+
"h-10 w-full border border-zinc-300 bg-white px-3 text-[14px] text-zinc-900 outline-none placeholder:text-zinc-400 focus:border-brand";
|
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
three brand vars are defaults — app/layout.tsx overrides them from
|
|
16
16
|
lib/site.config.ts on <html>, so re-theming the whole page is one edit there. */
|
|
17
17
|
:root {
|
|
18
|
-
--radius: 0.
|
|
19
|
-
--brand: #
|
|
20
|
-
--brand-soft: #
|
|
21
|
-
--paper: #
|
|
18
|
+
--radius: 0.125rem;
|
|
19
|
+
--brand: #1d4ed8;
|
|
20
|
+
--brand-soft: #eff6ff;
|
|
21
|
+
--paper: #ffffff;
|
|
22
22
|
--background: oklch(1 0 0);
|
|
23
23
|
--foreground: oklch(0.141 0.005 285.823);
|
|
24
24
|
--card: oklch(1 0 0);
|
|
@@ -125,6 +125,8 @@
|
|
|
125
125
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
126
126
|
--color-sidebar-border: var(--sidebar-border);
|
|
127
127
|
--color-sidebar-ring: var(--sidebar-ring);
|
|
128
|
+
/* `font-mono` utility. IBM Plex Mono is declared in app.ts as --font-code. */
|
|
129
|
+
--font-mono: var(--font-code, "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
128
130
|
}
|
|
129
131
|
|
|
130
132
|
@layer base {
|
|
@@ -153,3 +155,12 @@
|
|
|
153
155
|
cursor: pointer;
|
|
154
156
|
}
|
|
155
157
|
}
|
|
158
|
+
|
|
159
|
+
/* Headings. The page uses Inter semibold at modest sizes; no separate display
|
|
160
|
+
face. Kept as a class so a template fork can swap the heading font in one
|
|
161
|
+
place (declare it in app.ts as --font-display). */
|
|
162
|
+
.font-display {
|
|
163
|
+
font-family: var(--font-display, var(--font-sans, Inter, ui-sans-serif, system-ui, sans-serif));
|
|
164
|
+
font-weight: 600;
|
|
165
|
+
letter-spacing: -0.01em;
|
|
166
|
+
}
|
|
@@ -158,6 +158,16 @@ const manifest = buildManifest({
|
|
|
158
158
|
display: "swap",
|
|
159
159
|
preload: true,
|
|
160
160
|
}),
|
|
161
|
+
// The mono face for tags, counts, and the search hint. globals.css maps it
|
|
162
|
+
// onto Tailwind's `font-mono` utility.
|
|
163
|
+
font({
|
|
164
|
+
family: "IBM Plex Mono",
|
|
165
|
+
variable: "--font-code",
|
|
166
|
+
weights: ["400", "500"],
|
|
167
|
+
subsets: ["latin"],
|
|
168
|
+
display: "swap",
|
|
169
|
+
preload: true,
|
|
170
|
+
}),
|
|
161
171
|
],
|
|
162
172
|
routes: await discoverAppRoutes(),
|
|
163
173
|
});
|
|
@@ -1,91 +1,33 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
|
-
// Reusable presentational pieces for the
|
|
4
|
-
// no client JS.
|
|
5
|
-
//
|
|
6
|
-
// app/layout.tsx, which read lib/site.config.ts — so re-theming is one edit.
|
|
3
|
+
// Reusable presentational pieces for the marketing pages. All server-rendered,
|
|
4
|
+
// no client JS. The brand accent (`text-brand`) comes from CSS vars set on
|
|
5
|
+
// <html> in app/layout.tsx, which read lib/site.config.ts.
|
|
7
6
|
|
|
8
7
|
// Shared container: a contained, centered column.
|
|
9
|
-
export const WRAP = "mx-auto w-full max-w-5xl px-
|
|
8
|
+
export const WRAP = "mx-auto w-full max-w-5xl px-5 sm:px-8";
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
<p className="font-mono text-[11px] font-semibold uppercase tracking-[0.14em] text-brand">
|
|
14
|
-
{children}
|
|
15
|
-
</p>
|
|
16
|
-
);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// "New / Coming soon"-style pill for the hero.
|
|
20
|
-
export function Badge({ children }: { children: React.ReactNode }) {
|
|
21
|
-
return (
|
|
22
|
-
<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">
|
|
23
|
-
<span className="inline-block size-1.5 rounded-full bg-brand" />
|
|
24
|
-
{children}
|
|
25
|
-
</span>
|
|
26
|
-
);
|
|
27
|
-
}
|
|
10
|
+
// Plain text link in the accent color. Used for nav, rail, and inline links.
|
|
11
|
+
export const LINK = "text-brand underline-offset-2 hover:underline";
|
|
28
12
|
|
|
29
13
|
export function Divider() {
|
|
30
14
|
return (
|
|
31
15
|
<div className={WRAP}>
|
|
32
|
-
<div className="border-t border-zinc-200
|
|
16
|
+
<div className="border-t border-zinc-200" />
|
|
33
17
|
</div>
|
|
34
18
|
);
|
|
35
19
|
}
|
|
36
20
|
|
|
37
|
-
export function SectionHead({
|
|
38
|
-
eyebrow,
|
|
39
|
-
title,
|
|
40
|
-
body,
|
|
41
|
-
}: {
|
|
42
|
-
eyebrow: string;
|
|
43
|
-
title: string;
|
|
44
|
-
body?: string;
|
|
45
|
-
}) {
|
|
21
|
+
export function SectionHead({ title, body }: { title: string; body?: string }) {
|
|
46
22
|
return (
|
|
47
23
|
<div>
|
|
48
|
-
<
|
|
49
|
-
<
|
|
50
|
-
{title}
|
|
51
|
-
</h2>
|
|
52
|
-
{body ? (
|
|
53
|
-
<p className="mt-4 max-w-xl text-[15px] leading-relaxed text-zinc-500">
|
|
54
|
-
{body}
|
|
55
|
-
</p>
|
|
56
|
-
) : null}
|
|
24
|
+
<h2 className="font-display text-[20px] leading-tight text-zinc-900">{title}</h2>
|
|
25
|
+
{body ? <p className="mt-2 max-w-xl text-[14px] leading-relaxed text-zinc-600">{body}</p> : null}
|
|
57
26
|
</div>
|
|
58
27
|
);
|
|
59
28
|
}
|
|
60
29
|
|
|
61
|
-
//
|
|
62
|
-
export function FeatureGrid({
|
|
63
|
-
items,
|
|
64
|
-
}: {
|
|
65
|
-
items: { title: string; body: string; icon?: string }[];
|
|
66
|
-
}) {
|
|
67
|
-
return (
|
|
68
|
-
<div className="grid gap-6 sm:grid-cols-3">
|
|
69
|
-
{items.map((f) => (
|
|
70
|
-
<div key={f.title}>
|
|
71
|
-
{f.icon ? (
|
|
72
|
-
<span className="flex size-9 items-center justify-center rounded-lg bg-brand-soft text-brand">
|
|
73
|
-
{f.icon}
|
|
74
|
-
</span>
|
|
75
|
-
) : null}
|
|
76
|
-
<h3 className="mt-4 text-[15px] font-semibold text-zinc-900">
|
|
77
|
-
{f.title}
|
|
78
|
-
</h3>
|
|
79
|
-
<p className="mt-2 text-[14px] leading-relaxed text-zinc-500">
|
|
80
|
-
{f.body}
|
|
81
|
-
</p>
|
|
82
|
-
</div>
|
|
83
|
-
))}
|
|
84
|
-
</div>
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Initials for testimonial avatars, so the cards look finished without a photo.
|
|
30
|
+
// Initials for avatars, so a row looks finished without a photo.
|
|
89
31
|
export function initials(name: string) {
|
|
90
32
|
return name
|
|
91
33
|
.split(/\s+/)
|
|
@@ -95,23 +37,24 @@ export function initials(name: string) {
|
|
|
95
37
|
.toUpperCase();
|
|
96
38
|
}
|
|
97
39
|
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
// "swap this" instruction telling you exactly what to replace and where. Looks
|
|
101
|
-
// tidy enough to demo, but no one will mistake it for a finished design.
|
|
40
|
+
// An image slot. With `src` it renders the image; without, a dashed box with a
|
|
41
|
+
// title naming the image that belongs there and a hint on where to replace it.
|
|
102
42
|
//
|
|
103
43
|
// shape — "landscape" | "portrait" | "square" | "circle"
|
|
104
|
-
// title — what
|
|
44
|
+
// title — what image belongs here
|
|
105
45
|
// hint — how to replace it ("Replace in app/page.tsx")
|
|
106
46
|
export function ImagePlaceholder({
|
|
107
47
|
shape = "landscape",
|
|
108
48
|
title,
|
|
109
49
|
hint,
|
|
50
|
+
src,
|
|
110
51
|
className = "",
|
|
111
52
|
}: {
|
|
112
53
|
shape?: "landscape" | "portrait" | "square" | "circle";
|
|
113
54
|
title: string;
|
|
114
55
|
hint?: string;
|
|
56
|
+
/** A real image under public/images. */
|
|
57
|
+
src?: string;
|
|
115
58
|
className?: string;
|
|
116
59
|
}) {
|
|
117
60
|
const aspect =
|
|
@@ -120,27 +63,20 @@ export function ImagePlaceholder({
|
|
|
120
63
|
: shape === "square" || shape === "circle"
|
|
121
64
|
? "aspect-square"
|
|
122
65
|
: "aspect-[4/3]";
|
|
123
|
-
const radius = shape === "circle" ? "rounded-full" : "
|
|
66
|
+
const radius = shape === "circle" ? "rounded-full" : "";
|
|
67
|
+
if (src) {
|
|
68
|
+
return (
|
|
69
|
+
<div className={`relative overflow-hidden bg-zinc-100 ${aspect} ${radius} ${className}`}>
|
|
70
|
+
<img src={src} alt={title} className="absolute inset-0 size-full object-cover" loading="lazy" />
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
124
74
|
return (
|
|
125
75
|
<div
|
|
126
|
-
className={`relative grid place-items-center overflow-hidden border
|
|
76
|
+
className={`relative grid place-items-center overflow-hidden border border-dashed border-zinc-300 bg-zinc-50 ${aspect} ${radius} ${className}`}
|
|
127
77
|
>
|
|
128
78
|
<div className="px-4 text-center">
|
|
129
|
-
<
|
|
130
|
-
className="mx-auto size-7 text-zinc-300"
|
|
131
|
-
viewBox="0 0 24 24"
|
|
132
|
-
fill="none"
|
|
133
|
-
stroke="currentColor"
|
|
134
|
-
strokeWidth="1.5"
|
|
135
|
-
strokeLinecap="round"
|
|
136
|
-
strokeLinejoin="round"
|
|
137
|
-
aria-hidden
|
|
138
|
-
>
|
|
139
|
-
<rect x="3" y="3" width="18" height="18" rx="2" />
|
|
140
|
-
<circle cx="9" cy="9" r="1.6" />
|
|
141
|
-
<path d="m21 15-4.5-4.5L7 20" />
|
|
142
|
-
</svg>
|
|
143
|
-
<p className="mt-2 text-[12.5px] font-medium text-zinc-500">{title}</p>
|
|
79
|
+
<p className="font-mono text-[12px] text-zinc-500">{title}</p>
|
|
144
80
|
{hint ? <p className="mt-1 text-[11px] leading-snug text-zinc-400">{hint}</p> : null}
|
|
145
81
|
</div>
|
|
146
82
|
</div>
|
|
@@ -36,21 +36,18 @@ export type SeedListing = {
|
|
|
36
36
|
};
|
|
37
37
|
|
|
38
38
|
export type DirectoryConfig = BaseConfig & {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
subcopy: string;
|
|
39
|
+
// The page opens with the brand name, this one line, and the search box.
|
|
40
|
+
intro: {
|
|
41
|
+
description: string;
|
|
43
42
|
ctaLabel: string;
|
|
44
43
|
searchPlaceholder: string;
|
|
45
44
|
};
|
|
46
|
-
browse: { eyebrow: string; headline: string };
|
|
47
45
|
// The facet values offered in the submit form's category select. The browse
|
|
48
46
|
// facet sidebar is built live from the data, so this only needs to cover what
|
|
49
47
|
// submitters can pick.
|
|
50
48
|
categories: string[];
|
|
51
49
|
seedListings: SeedListing[];
|
|
52
50
|
submit: {
|
|
53
|
-
eyebrow: string;
|
|
54
51
|
headline: string;
|
|
55
52
|
subcopy: string;
|
|
56
53
|
confirmationMessage: string;
|
|
@@ -77,26 +74,19 @@ export const siteConfig: DirectoryConfig = {
|
|
|
77
74
|
],
|
|
78
75
|
},
|
|
79
76
|
|
|
80
|
-
colors: { brand: "#
|
|
77
|
+
colors: { brand: "#1d4ed8", brandSoft: "#eff6ff", paper: "#ffffff" },
|
|
81
78
|
|
|
82
79
|
seo: {
|
|
83
|
-
title: "Stacked
|
|
80
|
+
title: "Stacked, a developer tools directory",
|
|
84
81
|
description:
|
|
85
|
-
"A searchable, community-voted directory of developer tools. Full-text search,
|
|
82
|
+
"A searchable, community-voted directory of developer tools. Full-text search, category filters, upvotes, and a submit form.",
|
|
86
83
|
},
|
|
87
84
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
subcopy:
|
|
92
|
-
"Search a hand-checked directory, filter by category, and upvote useful tools. Results and vote counts update live, and browsing requires no account.",
|
|
85
|
+
intro: {
|
|
86
|
+
description:
|
|
87
|
+
"A hand-checked directory of developer tools. Search, filter by category, and upvote. No account needed.",
|
|
93
88
|
ctaLabel: "Submit a tool",
|
|
94
|
-
searchPlaceholder: "
|
|
95
|
-
},
|
|
96
|
-
|
|
97
|
-
browse: {
|
|
98
|
-
eyebrow: "Browse",
|
|
99
|
-
headline: "Find your next tool.",
|
|
89
|
+
searchPlaceholder: "search: database, deploy, auth",
|
|
100
90
|
},
|
|
101
91
|
|
|
102
92
|
categories: ["Database", "Hosting", "Auth", "AI", "Analytics", "Design", "Productivity", "DevOps"],
|
|
@@ -119,10 +109,9 @@ export const siteConfig: DirectoryConfig = {
|
|
|
119
109
|
],
|
|
120
110
|
|
|
121
111
|
submit: {
|
|
122
|
-
|
|
123
|
-
headline: "Know one we're missing?",
|
|
112
|
+
headline: "Submit a tool",
|
|
124
113
|
subcopy:
|
|
125
|
-
"
|
|
126
|
-
confirmationMessage: "
|
|
114
|
+
"Submissions go to the review queue. Approved tools appear in the directory.",
|
|
115
|
+
confirmationMessage: "Your submission is in the queue. We review new tools a few times a week.",
|
|
127
116
|
},
|
|
128
117
|
};
|
|
@@ -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
|
|