@pylonsync/create-pylon 0.3.373 → 0.3.375
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/package.json +1 -1
- package/templates/_root/AGENTS.md +1 -1
- package/templates/agency/AGENTS.md +1 -1
- package/templates/agency/app/(marketing)/layout.tsx +136 -0
- package/templates/{creator/app → agency/app/(marketing)}/not-found.tsx +6 -2
- package/templates/agency/app/error.tsx +6 -3
- package/templates/agency/app/layout.tsx +8 -134
- package/templates/agency/app/login/page.tsx +3 -2
- package/templates/ai-chat/AGENTS.md +1 -1
- package/templates/ai-chat/app/(chat)/layout.tsx +46 -0
- package/templates/ai-chat/app/{not-found.tsx → (chat)/not-found.tsx} +5 -2
- package/templates/ai-chat/app/error.tsx +6 -3
- package/templates/ai-chat/app/layout.tsx +6 -39
- package/templates/ai-chat/app/login/page.tsx +2 -1
- package/templates/ai-studio/AGENTS.md +1 -1
- package/templates/ai-studio/app/(studio)/layout.tsx +46 -0
- package/templates/{agency/app → ai-studio/app/(studio)}/not-found.tsx +5 -2
- package/templates/ai-studio/app/error.tsx +6 -3
- package/templates/ai-studio/app/layout.tsx +6 -39
- package/templates/ai-studio/app/login/page.tsx +2 -1
- package/templates/barebones/AGENTS.md +1 -1
- package/templates/chat/AGENTS.md +1 -1
- package/templates/consumer/AGENTS.md +1 -1
- package/templates/creator/AGENTS.md +1 -1
- package/templates/creator/app/(marketing)/layout.tsx +122 -0
- package/templates/{ai-studio/app → creator/app/(marketing)}/not-found.tsx +6 -2
- package/templates/creator/app/error.tsx +6 -3
- package/templates/creator/app/layout.tsx +8 -120
- package/templates/creator/app/login/page.tsx +3 -2
- package/templates/crm/AGENTS.md +1 -1
- package/templates/directory/AGENTS.md +1 -1
- package/templates/directory/app/(marketing)/layout.tsx +133 -0
- package/templates/directory/app/(marketing)/not-found.tsx +23 -0
- package/templates/directory/app/error.tsx +6 -3
- package/templates/directory/app/layout.tsx +8 -131
- package/templates/directory/app/login/page.tsx +3 -2
- package/templates/helpdesk/AGENTS.md +1 -1
- package/templates/inventory/AGENTS.md +1 -1
- package/templates/invoices/AGENTS.md +1 -1
- package/templates/local-service/AGENTS.md +1 -1
- package/templates/local-service/app/(marketing)/layout.tsx +119 -0
- package/templates/local-service/app/(marketing)/not-found.tsx +23 -0
- package/templates/local-service/app/{page.tsx → (marketing)/page.tsx} +2 -2
- package/templates/local-service/app/error.tsx +3 -1
- package/templates/local-service/app/layout.tsx +8 -116
- package/templates/local-service/app/login/page.tsx +3 -2
- package/templates/local-service/components/marketing.tsx +1 -1
- package/templates/marketplace/AGENTS.md +1 -1
- package/templates/projects/AGENTS.md +1 -1
- package/templates/restaurant/AGENTS.md +1 -1
- package/templates/restaurant/app/(marketing)/layout.tsx +119 -0
- package/templates/restaurant/app/(marketing)/not-found.tsx +23 -0
- package/templates/restaurant/app/{page.tsx → (marketing)/page.tsx} +2 -2
- package/templates/restaurant/app/error.tsx +3 -1
- package/templates/restaurant/app/layout.tsx +8 -116
- package/templates/restaurant/app/login/page.tsx +3 -2
- package/templates/restaurant/components/marketing.tsx +1 -1
- package/templates/saas/AGENTS.md +1 -1
- package/templates/saas/app/{auth-shell.tsx → (auth)/layout.tsx} +21 -29
- package/templates/saas/app/(auth)/login/page.tsx +33 -0
- package/templates/saas/app/(auth)/signup/page.tsx +32 -0
- package/templates/saas/app/(marketing)/layout.tsx +411 -0
- package/templates/saas/app/{not-found.tsx → (marketing)/not-found.tsx} +9 -5
- package/templates/saas/app/dashboard/billing/page.tsx +7 -20
- package/templates/saas/app/dashboard/layout.tsx +58 -0
- package/templates/saas/app/dashboard/members/page.tsx +9 -22
- package/templates/saas/app/dashboard/page.tsx +15 -31
- package/templates/saas/app/dashboard/projects/page.tsx +4 -23
- package/templates/saas/app/dashboard/settings/page.tsx +7 -19
- package/templates/saas/app/error.tsx +7 -5
- package/templates/saas/app/layout.tsx +7 -408
- package/templates/saas/components/dashboard-shell.tsx +10 -6
- package/templates/shop/AGENTS.md +1 -1
- package/templates/shop/app/(marketing)/layout.tsx +122 -0
- package/templates/shop/app/(marketing)/not-found.tsx +23 -0
- package/templates/shop/app/{page.tsx → (marketing)/page.tsx} +1 -1
- package/templates/shop/app/error.tsx +3 -1
- package/templates/shop/app/layout.tsx +8 -120
- package/templates/shop/app/login/page.tsx +3 -2
- package/templates/todo/AGENTS.md +1 -1
- package/templates/waitlist/AGENTS.md +1 -1
- package/templates/waitlist/app/(marketing)/layout.tsx +122 -0
- package/templates/waitlist/app/(marketing)/not-found.tsx +23 -0
- package/templates/waitlist/app/{page.tsx → (marketing)/page.tsx} +1 -1
- package/templates/waitlist/app/error.tsx +3 -1
- package/templates/waitlist/app/layout.tsx +8 -120
- package/templates/waitlist/app/login/page.tsx +3 -2
- package/templates/directory/app/not-found.tsx +0 -19
- package/templates/local-service/app/not-found.tsx +0 -19
- package/templates/restaurant/app/not-found.tsx +0 -19
- package/templates/saas/app/login/page.tsx +0 -27
- package/templates/saas/app/signup/page.tsx +0 -24
- package/templates/shop/app/not-found.tsx +0 -19
- package/templates/waitlist/app/not-found.tsx +0 -19
- /package/templates/agency/app/{contact-form.tsx → (marketing)/contact-form.tsx} +0 -0
- /package/templates/agency/app/{page.tsx → (marketing)/page.tsx} +0 -0
- /package/templates/agency/app/{seeder.tsx → (marketing)/seeder.tsx} +0 -0
- /package/templates/agency/app/{work → (marketing)/work}/[slug]/page.tsx +0 -0
- /package/templates/agency/app/{work → (marketing)/work}/page.tsx +0 -0
- /package/templates/ai-chat/app/{chat-client.tsx → (chat)/chat-client.tsx} +0 -0
- /package/templates/ai-chat/app/{page.tsx → (chat)/page.tsx} +0 -0
- /package/templates/ai-studio/app/{page.tsx → (studio)/page.tsx} +0 -0
- /package/templates/ai-studio/app/{studio-client.tsx → (studio)/studio-client.tsx} +0 -0
- /package/templates/creator/app/{newsletter-signup.tsx → (marketing)/newsletter-signup.tsx} +0 -0
- /package/templates/creator/app/{page.tsx → (marketing)/page.tsx} +0 -0
- /package/templates/directory/app/{directory-browse.tsx → (marketing)/directory-browse.tsx} +0 -0
- /package/templates/directory/app/{page.tsx → (marketing)/page.tsx} +0 -0
- /package/templates/directory/app/{submit → (marketing)/submit}/page.tsx +0 -0
- /package/templates/directory/app/{submit-form.tsx → (marketing)/submit-form.tsx} +0 -0
- /package/templates/local-service/app/{booking-widget.tsx → (marketing)/booking-widget.tsx} +0 -0
- /package/templates/restaurant/app/{reservation-widget.tsx → (marketing)/reservation-widget.tsx} +0 -0
- /package/templates/saas/app/{auth-form.tsx → (auth)/auth-form.tsx} +0 -0
- /package/templates/saas/app/{company → (marketing)/company}/[slug]/page.tsx +0 -0
- /package/templates/saas/app/{compare → (marketing)/compare}/[slug]/page.tsx +0 -0
- /package/templates/saas/app/{page.tsx → (marketing)/page.tsx} +0 -0
- /package/templates/saas/app/{products → (marketing)/products}/[slug]/page.tsx +0 -0
- /package/templates/saas/app/{resources → (marketing)/resources}/[slug]/page.tsx +0 -0
- /package/templates/saas/app/{solutions → (marketing)/solutions}/[slug]/page.tsx +0 -0
- /package/templates/shop/app/{shop-client.tsx → (marketing)/shop-client.tsx} +0 -0
- /package/templates/shop/app/{success → (marketing)/success}/page.tsx +0 -0
- /package/templates/waitlist/app/{waitlist-hero.tsx → (marketing)/waitlist-hero.tsx} +0 -0
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import React, { use } from "react";
|
|
2
2
|
import { type Metadata, type PageProps } from "@pylonsync/react";
|
|
3
|
-
import { DashboardShell } from "@/components/dashboard-shell";
|
|
4
3
|
import { Projects, type Project } from "../dashboard-client";
|
|
5
4
|
|
|
6
5
|
export const metadata: Metadata = {
|
|
@@ -9,31 +8,13 @@ export const metadata: Metadata = {
|
|
|
9
8
|
};
|
|
10
9
|
|
|
11
10
|
// `/dashboard/projects` — this org's projects, server-resolved via `use()` then
|
|
12
|
-
// kept live + optimistic by `db` on the client.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
response,
|
|
16
|
-
serverData,
|
|
17
|
-
}: PageProps) {
|
|
18
|
-
if (!auth.user_id) {
|
|
19
|
-
response.redirect("/login");
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
11
|
+
// kept live + optimistic by `db` on the client. Auth gate + shell chrome come
|
|
12
|
+
// from the dashboard layout.
|
|
13
|
+
export default function ProjectsPage({ auth, response, serverData }: PageProps) {
|
|
22
14
|
if (!auth.tenant_id) {
|
|
23
15
|
response.redirect("/dashboard");
|
|
24
16
|
return null;
|
|
25
17
|
}
|
|
26
|
-
const me = use(serverData.get<{ email?: string }>("User", auth.user_id));
|
|
27
|
-
const org = use(serverData.get<{ name?: string }>("Org", auth.tenant_id));
|
|
28
18
|
const projects = use(serverData.list<Project>("Project"));
|
|
29
|
-
return
|
|
30
|
-
<DashboardShell
|
|
31
|
-
active="projects"
|
|
32
|
-
title="Projects"
|
|
33
|
-
userEmail={me?.email ?? ""}
|
|
34
|
-
orgName={org?.name}
|
|
35
|
-
>
|
|
36
|
-
<Projects tenantId={auth.tenant_id} initial={projects} />
|
|
37
|
-
</DashboardShell>
|
|
38
|
-
);
|
|
19
|
+
return <Projects tenantId={auth.tenant_id} initial={projects} />;
|
|
39
20
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import React, { use } from "react";
|
|
2
2
|
import { type Metadata, type PageProps } from "@pylonsync/react";
|
|
3
|
-
import { DashboardShell } from "@/components/dashboard-shell";
|
|
4
3
|
import {
|
|
5
4
|
Settings,
|
|
6
5
|
type OrgInfo,
|
|
@@ -14,34 +13,23 @@ export const metadata: Metadata = {
|
|
|
14
13
|
|
|
15
14
|
// `/dashboard/settings` — workspace settings: rename (owners/admins) and delete
|
|
16
15
|
// (owners). The active org + member count are resolved server-side so the page
|
|
17
|
-
// paints with real values on the first byte.
|
|
16
|
+
// paints with real values on the first byte. Auth gate + shell chrome come from
|
|
17
|
+
// the dashboard layout.
|
|
18
18
|
export default function SettingsPage({ auth, response, serverData }: PageProps) {
|
|
19
|
-
if (!auth.user_id) {
|
|
20
|
-
response.redirect("/login");
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
19
|
if (!auth.tenant_id) {
|
|
24
20
|
response.redirect("/dashboard");
|
|
25
21
|
return null;
|
|
26
22
|
}
|
|
27
|
-
const me = use(serverData.get<{ email?: string }>("User", auth.user_id));
|
|
28
23
|
const org = use(serverData.get<OrgInfo>("Org", auth.tenant_id));
|
|
29
24
|
const members = use(serverData.list<OrgMemberRow>("OrgMember"));
|
|
30
25
|
const memberCount = members.filter(
|
|
31
26
|
(m) => m.orgId === auth.tenant_id,
|
|
32
27
|
).length;
|
|
33
28
|
return (
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
>
|
|
40
|
-
<Settings
|
|
41
|
-
org={org}
|
|
42
|
-
role={auth.roles?.[0] ?? ""}
|
|
43
|
-
memberCount={memberCount}
|
|
44
|
-
/>
|
|
45
|
-
</DashboardShell>
|
|
29
|
+
<Settings
|
|
30
|
+
org={org}
|
|
31
|
+
role={auth.roles?.[0] ?? ""}
|
|
32
|
+
memberCount={memberCount}
|
|
33
|
+
/>
|
|
46
34
|
);
|
|
47
35
|
}
|
|
@@ -2,16 +2,18 @@ import React from "react";
|
|
|
2
2
|
import { type ErrorBoundaryProps } from "@pylonsync/react";
|
|
3
3
|
import { Button } from "@/components/ui/button";
|
|
4
4
|
|
|
5
|
-
// `app/error.tsx` → the error boundary
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
5
|
+
// `app/error.tsx` → the GLOBAL error boundary. Sitting at the app root
|
|
6
|
+
// (outside the `(marketing)` group), it catches a throw from any section —
|
|
7
|
+
// marketing, auth, or dashboard — and renders at HTTP 500 in the bare root
|
|
8
|
+
// shell, so it brings its own centered container. It's HYDRATED, so this is
|
|
9
|
+
// a real interactive client component: `reset()` re-attempts the route, and
|
|
10
|
+
// useState/onClick work. The thrown error reaches the client as
|
|
9
11
|
// `{ message, digest }` only — the stack stays in the dev overlay
|
|
10
12
|
// (PYLON_DEV_MODE) and the server logs, never in the page.
|
|
11
13
|
export default function Error({ error, reset }: ErrorBoundaryProps) {
|
|
12
14
|
const [tries, setTries] = React.useState(0);
|
|
13
15
|
return (
|
|
14
|
-
<div className="space-y-6">
|
|
16
|
+
<div className="mx-auto w-full max-w-xl space-y-6 px-6 py-24">
|
|
15
17
|
<section>
|
|
16
18
|
<h1 className="text-2xl font-semibold tracking-tight">
|
|
17
19
|
Something went wrong
|
|
@@ -1,189 +1,16 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { Link, type PageAuth } from "@pylonsync/react";
|
|
3
|
-
import { PRODUCTS } from "@/lib/products";
|
|
4
|
-
import { SOLUTIONS, RESOURCES, COMPANY, COMPARISONS } from "@/lib/site";
|
|
5
2
|
import { siteConfig } from "@/lib/site.config";
|
|
6
3
|
|
|
7
|
-
// A layout receives the page props plus `children`. `auth.user_id` is null for
|
|
8
|
-
// anonymous visitors and the signed-in user's id otherwise — resolved
|
|
9
|
-
// server-side from the session cookie before any HTML is sent, so the nav
|
|
10
|
-
// renders the right links on the first byte (no flash, no client fetch).
|
|
11
4
|
interface LayoutProps {
|
|
12
5
|
children: React.ReactNode;
|
|
13
|
-
url: string;
|
|
14
|
-
auth: PageAuth;
|
|
15
6
|
}
|
|
16
7
|
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
const PRODUCT_MENU: MenuItem[] = PRODUCTS.map((p) => ({
|
|
24
|
-
icon: p.icon,
|
|
25
|
-
title: p.title,
|
|
26
|
-
desc: p.tagline,
|
|
27
|
-
href: `/products/${p.slug}`,
|
|
28
|
-
}));
|
|
29
|
-
|
|
30
|
-
const RESOURCES_MENU: MenuItem[] = [
|
|
31
|
-
{ icon: "▢", title: "Docs", desc: `Set up and use ${siteConfig.brand.name}.`, href: "/resources/docs" },
|
|
32
|
-
{ icon: "✎", title: "Guides", desc: "Playbooks and walkthroughs.", href: "/resources/guides" },
|
|
33
|
-
{ icon: "✦", title: "Changelog", desc: `What's new in ${siteConfig.brand.name}.`, href: "/resources/changelog" },
|
|
34
|
-
{ icon: "⌘", title: "API reference", desc: `Build on the ${siteConfig.brand.name} API.`, href: "/resources/api" },
|
|
35
|
-
{ icon: "◈", title: "Compare", desc: `See how ${siteConfig.brand.name} stacks up.`, href: `/compare/${COMPARISONS[0].slug}` },
|
|
36
|
-
];
|
|
37
|
-
|
|
38
|
-
// A hover/focus dropdown — pure CSS via `group`, so the nav stays a server
|
|
39
|
-
// component (no client JS). The panel opens on hover and on keyboard focus.
|
|
40
|
-
function NavDropdown({ label, items }: { label: string; items: MenuItem[] }) {
|
|
41
|
-
return (
|
|
42
|
-
<div className="group relative">
|
|
43
|
-
<button
|
|
44
|
-
type="button"
|
|
45
|
-
aria-haspopup="menu"
|
|
46
|
-
className="flex items-center gap-1 text-[13.5px] text-zinc-600 transition-colors hover:text-zinc-900 group-hover:text-zinc-900"
|
|
47
|
-
>
|
|
48
|
-
{label}
|
|
49
|
-
<Chevron />
|
|
50
|
-
</button>
|
|
51
|
-
{/* `pt-3` (padding, not margin) bridges the gap so hover doesn't drop. */}
|
|
52
|
-
<div className="invisible absolute left-0 top-full z-40 w-[340px] translate-y-1 pt-3 opacity-0 transition-all duration-150 group-hover:visible group-hover:translate-y-0 group-hover:opacity-100 group-focus-within:visible group-focus-within:translate-y-0 group-focus-within:opacity-100">
|
|
53
|
-
<div className="rounded-2xl border border-zinc-200 bg-white p-2 shadow-[0_24px_60px_-25px_rgba(0,0,0,0.25)]">
|
|
54
|
-
{items.map((it) => (
|
|
55
|
-
<a
|
|
56
|
-
key={it.title}
|
|
57
|
-
href={it.href}
|
|
58
|
-
className="flex items-start gap-3 rounded-xl p-3 transition-colors hover:bg-zinc-50"
|
|
59
|
-
>
|
|
60
|
-
<span className="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-lg bg-brand-soft text-[13px] text-brand">
|
|
61
|
-
{it.icon}
|
|
62
|
-
</span>
|
|
63
|
-
<span className="min-w-0">
|
|
64
|
-
<span className="block text-[13.5px] font-medium text-zinc-900">
|
|
65
|
-
{it.title}
|
|
66
|
-
</span>
|
|
67
|
-
<span className="block text-[12.5px] leading-snug text-zinc-500">
|
|
68
|
-
{it.desc}
|
|
69
|
-
</span>
|
|
70
|
-
</span>
|
|
71
|
-
</a>
|
|
72
|
-
))}
|
|
73
|
-
</div>
|
|
74
|
-
</div>
|
|
75
|
-
</div>
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function Chevron() {
|
|
80
|
-
return (
|
|
81
|
-
<svg
|
|
82
|
-
width="10"
|
|
83
|
-
height="10"
|
|
84
|
-
viewBox="0 0 10 10"
|
|
85
|
-
fill="none"
|
|
86
|
-
aria-hidden
|
|
87
|
-
className="text-zinc-400 transition-transform duration-200 group-hover:rotate-180"
|
|
88
|
-
>
|
|
89
|
-
<path
|
|
90
|
-
d="M2 3.5L5 6.5L8 3.5"
|
|
91
|
-
stroke="currentColor"
|
|
92
|
-
strokeWidth="1.5"
|
|
93
|
-
strokeLinecap="round"
|
|
94
|
-
strokeLinejoin="round"
|
|
95
|
-
/>
|
|
96
|
-
</svg>
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Mobile menu — the desktop nav is `hidden md:flex`, so on phones this is the
|
|
101
|
-
// only way to reach Product / Resources / Pricing. Pure-CSS via native
|
|
102
|
-
// `<details>` (no client JS, same pattern as the FAQ + user menu); plain `<a>`
|
|
103
|
-
// links do a full navigation, which closes the open panel. md:hidden so it
|
|
104
|
-
// never shows alongside the desktop nav.
|
|
105
|
-
function MobileNav({ signedIn }: { signedIn: boolean }) {
|
|
106
|
-
return (
|
|
107
|
-
<details className="md:hidden">
|
|
108
|
-
<summary
|
|
109
|
-
aria-label="Open menu"
|
|
110
|
-
className="flex size-9 cursor-pointer select-none list-none items-center justify-center rounded-md text-zinc-700 transition-colors marker:hidden hover:bg-zinc-100 [&::-webkit-details-marker]:hidden"
|
|
111
|
-
>
|
|
112
|
-
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden>
|
|
113
|
-
<path d="M3 6h18M3 12h18M3 18h18" />
|
|
114
|
-
</svg>
|
|
115
|
-
</summary>
|
|
116
|
-
<div className="fixed inset-x-0 top-14 z-40 max-h-[calc(100vh-3.5rem)] overflow-y-auto border-b border-zinc-200 bg-white shadow-[0_24px_48px_-24px_rgba(0,0,0,0.25)]">
|
|
117
|
-
<div className="mx-auto max-w-5xl space-y-6 px-6 py-6">
|
|
118
|
-
<MobileGroup title="Product" items={PRODUCT_MENU} />
|
|
119
|
-
<MobileGroup title="Resources" items={RESOURCES_MENU} />
|
|
120
|
-
<div className="flex flex-col">
|
|
121
|
-
<a href="/#pricing" className="rounded-lg px-2 py-2 text-[14px] font-medium text-zinc-700 transition-colors hover:bg-zinc-50">
|
|
122
|
-
Pricing
|
|
123
|
-
</a>
|
|
124
|
-
<a href="/#customers" className="rounded-lg px-2 py-2 text-[14px] font-medium text-zinc-700 transition-colors hover:bg-zinc-50">
|
|
125
|
-
Customers
|
|
126
|
-
</a>
|
|
127
|
-
</div>
|
|
128
|
-
<div className="flex flex-col gap-2 border-t border-zinc-100 pt-5">
|
|
129
|
-
{signedIn ? (
|
|
130
|
-
<a href="/dashboard" className="inline-flex h-10 items-center justify-center rounded-full bg-zinc-900 text-[14px] font-medium text-white transition-colors hover:bg-zinc-700">
|
|
131
|
-
Open dashboard
|
|
132
|
-
</a>
|
|
133
|
-
) : (
|
|
134
|
-
<>
|
|
135
|
-
<a href="/login" className="inline-flex h-10 items-center justify-center rounded-full border border-zinc-300 text-[14px] font-medium text-zinc-900 transition-colors hover:bg-zinc-50">
|
|
136
|
-
Log in
|
|
137
|
-
</a>
|
|
138
|
-
<a href="/signup" className="inline-flex h-10 items-center justify-center rounded-full bg-zinc-900 text-[14px] font-medium text-white transition-colors hover:bg-zinc-700">
|
|
139
|
-
Get started
|
|
140
|
-
</a>
|
|
141
|
-
</>
|
|
142
|
-
)}
|
|
143
|
-
</div>
|
|
144
|
-
</div>
|
|
145
|
-
</div>
|
|
146
|
-
</details>
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function MobileGroup({ title, items }: { title: string; items: MenuItem[] }) {
|
|
151
|
-
return (
|
|
152
|
-
<div>
|
|
153
|
-
<div className="mb-1.5 px-2 text-[11px] font-semibold uppercase tracking-wider text-zinc-400">
|
|
154
|
-
{title}
|
|
155
|
-
</div>
|
|
156
|
-
<div className="flex flex-col">
|
|
157
|
-
{items.map((it) => (
|
|
158
|
-
<a
|
|
159
|
-
key={it.href}
|
|
160
|
-
href={it.href}
|
|
161
|
-
className="flex items-center gap-3 rounded-lg px-2 py-2 text-[14px] text-zinc-700 transition-colors hover:bg-zinc-50"
|
|
162
|
-
>
|
|
163
|
-
<span className="flex size-7 shrink-0 items-center justify-center rounded-md bg-brand-soft text-[12px] text-brand">
|
|
164
|
-
{it.icon}
|
|
165
|
-
</span>
|
|
166
|
-
{it.title}
|
|
167
|
-
</a>
|
|
168
|
-
))}
|
|
169
|
-
</div>
|
|
170
|
-
</div>
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// The root layout wraps every page: a marketing nav up top, a fat footer below.
|
|
175
|
-
// `<main>` is full-bleed — each page owns its own container so the landing page
|
|
176
|
-
// can run edge-to-edge while the app pages stay centered. Rebrand "Acme" to
|
|
177
|
-
// your product.
|
|
178
|
-
export default function RootLayout({ children, url, auth }: LayoutProps) {
|
|
179
|
-
const signedIn = Boolean(auth?.user_id);
|
|
180
|
-
// These render bare — no marketing nav/footer: the auth screens and the
|
|
181
|
-
// dashboard (which brings its own sidebar shell). Match on the path PREFIX
|
|
182
|
-
// (not a substring) so a future marketing slug that happens to contain one
|
|
183
|
-
// of these words — e.g. /products/dashboard-tools — keeps its chrome.
|
|
184
|
-
const path = (url ?? "").split("?")[0];
|
|
185
|
-
const BARE_PREFIXES = ["/login", "/signup", "/dashboard"];
|
|
186
|
-
const isBare = BARE_PREFIXES.some((p) => path === p || path.startsWith(p + "/"));
|
|
8
|
+
// The root layout is only the document shell: <html>, <head>, <body>, and the
|
|
9
|
+
// site-wide theme variables. Each section owns its chrome in its own layout:
|
|
10
|
+
// `(marketing)/layout.tsx` adds the nav + footer, `(auth)/layout.tsx` the
|
|
11
|
+
// split-screen frame for /login + /signup, and `dashboard/layout.tsx` the
|
|
12
|
+
// sidebar shell.
|
|
13
|
+
export default function RootLayout({ children }: LayoutProps) {
|
|
187
14
|
return (
|
|
188
15
|
<html
|
|
189
16
|
lang="en"
|
|
@@ -213,236 +40,8 @@ export default function RootLayout({ children, url, auth }: LayoutProps) {
|
|
|
213
40
|
here too. */}
|
|
214
41
|
</head>
|
|
215
42
|
<body className="flex min-h-screen flex-col bg-background text-foreground antialiased">
|
|
216
|
-
{
|
|
217
|
-
children
|
|
218
|
-
) : (
|
|
219
|
-
<>
|
|
220
|
-
<header className="sticky top-0 z-30 bg-white/80 backdrop-blur">
|
|
221
|
-
<div className="mx-auto flex h-14 max-w-5xl items-center justify-between px-6">
|
|
222
|
-
<div className="flex items-center gap-8">
|
|
223
|
-
<Link href="/" className="flex items-center gap-2">
|
|
224
|
-
<span className="flex size-6 items-center justify-center rounded-[7px] bg-zinc-900 text-[13px] font-bold text-white">
|
|
225
|
-
{siteConfig.brand.letter}
|
|
226
|
-
</span>
|
|
227
|
-
<span className="text-[15px] font-semibold tracking-tight text-zinc-900">
|
|
228
|
-
{siteConfig.brand.name}
|
|
229
|
-
</span>
|
|
230
|
-
</Link>
|
|
231
|
-
<nav className="hidden items-center gap-6 md:flex">
|
|
232
|
-
<NavDropdown label="Product" items={PRODUCT_MENU} />
|
|
233
|
-
<NavDropdown label="Resources" items={RESOURCES_MENU} />
|
|
234
|
-
<Link
|
|
235
|
-
href="/#pricing"
|
|
236
|
-
className="text-[13.5px] text-zinc-600 transition-colors hover:text-zinc-900"
|
|
237
|
-
>
|
|
238
|
-
Pricing
|
|
239
|
-
</Link>
|
|
240
|
-
<Link
|
|
241
|
-
href="/#customers"
|
|
242
|
-
className="text-[13.5px] text-zinc-600 transition-colors hover:text-zinc-900"
|
|
243
|
-
>
|
|
244
|
-
Customers
|
|
245
|
-
</Link>
|
|
246
|
-
</nav>
|
|
247
|
-
</div>
|
|
248
|
-
<nav className="flex items-center gap-2">
|
|
249
|
-
<MobileNav signedIn={signedIn} />
|
|
250
|
-
{signedIn ? (
|
|
251
|
-
<Link
|
|
252
|
-
href="/dashboard"
|
|
253
|
-
className="inline-flex items-center rounded-full bg-zinc-900 px-3.5 py-1.5 text-[13px] font-medium text-white transition-colors hover:bg-zinc-700"
|
|
254
|
-
>
|
|
255
|
-
Dashboard
|
|
256
|
-
</Link>
|
|
257
|
-
) : (
|
|
258
|
-
<>
|
|
259
|
-
<Link
|
|
260
|
-
href="/login"
|
|
261
|
-
className="hidden rounded-full px-3 py-1.5 text-[13px] font-medium text-zinc-600 transition-colors hover:text-zinc-900 sm:inline-flex"
|
|
262
|
-
>
|
|
263
|
-
Log in
|
|
264
|
-
</Link>
|
|
265
|
-
<Link
|
|
266
|
-
href="/signup"
|
|
267
|
-
className="inline-flex items-center rounded-full bg-zinc-900 px-3.5 py-1.5 text-[13px] font-medium text-white transition-colors hover:bg-zinc-700"
|
|
268
|
-
>
|
|
269
|
-
Get started
|
|
270
|
-
</Link>
|
|
271
|
-
</>
|
|
272
|
-
)}
|
|
273
|
-
</nav>
|
|
274
|
-
</div>
|
|
275
|
-
</header>
|
|
276
|
-
|
|
277
|
-
<main className="flex-1">{children}</main>
|
|
278
|
-
|
|
279
|
-
<SiteFooter />
|
|
280
|
-
</>
|
|
281
|
-
)}
|
|
43
|
+
{children}
|
|
282
44
|
</body>
|
|
283
45
|
</html>
|
|
284
46
|
);
|
|
285
47
|
}
|
|
286
|
-
|
|
287
|
-
// Footer link groups, derived from the same data the pages use so every link
|
|
288
|
-
// resolves to a real route. Each link carries a small icon (except Compare).
|
|
289
|
-
type FooterLink = { label: string; href: string; icon?: string };
|
|
290
|
-
type FooterGroupData = { title: string; links: FooterLink[] };
|
|
291
|
-
|
|
292
|
-
const SOLUTION_ICONS = ["◆", "◈", "▣", "◎"];
|
|
293
|
-
const RESOURCE_ICONS: Record<string, string> = {
|
|
294
|
-
docs: "▢",
|
|
295
|
-
guides: "✎",
|
|
296
|
-
changelog: "✦",
|
|
297
|
-
api: "⌘",
|
|
298
|
-
status: "◉",
|
|
299
|
-
};
|
|
300
|
-
const COMPANY_ICONS = ["◍", "≣", "◆", "✉", "▢"];
|
|
301
|
-
|
|
302
|
-
const PRODUCT_GROUP: FooterGroupData = {
|
|
303
|
-
title: "Product",
|
|
304
|
-
links: [
|
|
305
|
-
...PRODUCTS.map((p) => ({
|
|
306
|
-
label: p.title,
|
|
307
|
-
href: `/products/${p.slug}`,
|
|
308
|
-
icon: p.icon,
|
|
309
|
-
})),
|
|
310
|
-
{ label: "Pricing", href: "/#pricing", icon: "◷" },
|
|
311
|
-
],
|
|
312
|
-
};
|
|
313
|
-
const SOLUTIONS_GROUP: FooterGroupData = {
|
|
314
|
-
title: "Solutions",
|
|
315
|
-
links: SOLUTIONS.map((s, i) => ({
|
|
316
|
-
label: s.navLabel,
|
|
317
|
-
href: `/solutions/${s.slug}`,
|
|
318
|
-
icon: SOLUTION_ICONS[i] ?? "◆",
|
|
319
|
-
})),
|
|
320
|
-
};
|
|
321
|
-
const RESOURCES_GROUP: FooterGroupData = {
|
|
322
|
-
title: "Resources",
|
|
323
|
-
links: RESOURCES.map((r) => ({
|
|
324
|
-
label: r.navLabel,
|
|
325
|
-
href: `/resources/${r.slug}`,
|
|
326
|
-
icon: RESOURCE_ICONS[r.slug] ?? "▢",
|
|
327
|
-
})),
|
|
328
|
-
};
|
|
329
|
-
const COMPANY_GROUP: FooterGroupData = {
|
|
330
|
-
title: "Company",
|
|
331
|
-
links: COMPANY.map((c, i) => ({
|
|
332
|
-
label: c.navLabel,
|
|
333
|
-
href: `/company/${c.slug}`,
|
|
334
|
-
icon: COMPANY_ICONS[i] ?? "◍",
|
|
335
|
-
})),
|
|
336
|
-
};
|
|
337
|
-
const COMPARE_GROUP: FooterGroupData = {
|
|
338
|
-
title: "Compare",
|
|
339
|
-
links: COMPARISONS.map((c) => ({
|
|
340
|
-
label: c.navLabel,
|
|
341
|
-
href: `/compare/${c.slug}`,
|
|
342
|
-
})),
|
|
343
|
-
};
|
|
344
|
-
|
|
345
|
-
function FooterGroup({
|
|
346
|
-
group,
|
|
347
|
-
className = "",
|
|
348
|
-
}: {
|
|
349
|
-
group: FooterGroupData;
|
|
350
|
-
className?: string;
|
|
351
|
-
}) {
|
|
352
|
-
return (
|
|
353
|
-
<div className={className}>
|
|
354
|
-
<div className="text-[11px] font-semibold uppercase tracking-wider text-zinc-900">
|
|
355
|
-
{group.title}
|
|
356
|
-
</div>
|
|
357
|
-
<div className="mt-4 flex flex-col gap-2.5">
|
|
358
|
-
{group.links.map((l) => (
|
|
359
|
-
<Link
|
|
360
|
-
key={l.href}
|
|
361
|
-
href={l.href}
|
|
362
|
-
className="group flex items-center gap-2 text-[13px] text-zinc-500 transition-colors hover:text-zinc-900"
|
|
363
|
-
>
|
|
364
|
-
{l.icon ? (
|
|
365
|
-
<span className="flex w-3.5 justify-center text-[11px] text-zinc-400 transition-colors group-hover:text-zinc-600">
|
|
366
|
-
{l.icon}
|
|
367
|
-
</span>
|
|
368
|
-
) : null}
|
|
369
|
-
{l.label}
|
|
370
|
-
</Link>
|
|
371
|
-
))}
|
|
372
|
-
</div>
|
|
373
|
-
</div>
|
|
374
|
-
);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function SiteFooter() {
|
|
378
|
-
return (
|
|
379
|
-
<footer className="border-t border-zinc-200/70 bg-white">
|
|
380
|
-
<div className="mx-auto max-w-5xl px-6 py-16">
|
|
381
|
-
{/* Brand block */}
|
|
382
|
-
<div className="max-w-xs">
|
|
383
|
-
<Link href="/" className="inline-flex items-center gap-2">
|
|
384
|
-
<span className="flex size-7 items-center justify-center rounded-lg bg-zinc-900 text-sm font-bold text-white">
|
|
385
|
-
{siteConfig.brand.letter}
|
|
386
|
-
</span>
|
|
387
|
-
<span className="text-[15px] font-semibold tracking-tight text-zinc-900">
|
|
388
|
-
{siteConfig.brand.name}
|
|
389
|
-
</span>
|
|
390
|
-
</Link>
|
|
391
|
-
<p className="mt-4 text-[13px] leading-relaxed text-zinc-500">
|
|
392
|
-
{siteConfig.brand.footerBlurb}
|
|
393
|
-
</p>
|
|
394
|
-
<div className="mt-5 flex items-center gap-4">
|
|
395
|
-
{siteConfig.brand.socials.map((s) => (
|
|
396
|
-
<a
|
|
397
|
-
key={s.label}
|
|
398
|
-
href={s.href}
|
|
399
|
-
aria-label={s.label}
|
|
400
|
-
className="text-zinc-400 transition-colors hover:text-zinc-900"
|
|
401
|
-
>
|
|
402
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
|
403
|
-
<path d={s.path} />
|
|
404
|
-
</svg>
|
|
405
|
-
</a>
|
|
406
|
-
))}
|
|
407
|
-
<a
|
|
408
|
-
href={`mailto:${siteConfig.brand.email}`}
|
|
409
|
-
aria-label="Email"
|
|
410
|
-
className="text-zinc-400 transition-colors hover:text-zinc-900"
|
|
411
|
-
>
|
|
412
|
-
<svg
|
|
413
|
-
width="16"
|
|
414
|
-
height="16"
|
|
415
|
-
viewBox="0 0 24 24"
|
|
416
|
-
fill="none"
|
|
417
|
-
stroke="currentColor"
|
|
418
|
-
strokeWidth="2"
|
|
419
|
-
>
|
|
420
|
-
<rect x="3" y="5" width="18" height="14" rx="2" />
|
|
421
|
-
<path d="m3 7 9 6 9-6" />
|
|
422
|
-
</svg>
|
|
423
|
-
</a>
|
|
424
|
-
</div>
|
|
425
|
-
</div>
|
|
426
|
-
|
|
427
|
-
{/* Link columns */}
|
|
428
|
-
<div className="mt-12 grid gap-x-8 gap-y-12 sm:grid-cols-2 lg:grid-cols-3">
|
|
429
|
-
<div>
|
|
430
|
-
<FooterGroup group={PRODUCT_GROUP} />
|
|
431
|
-
<FooterGroup group={SOLUTIONS_GROUP} className="mt-10" />
|
|
432
|
-
</div>
|
|
433
|
-
<div>
|
|
434
|
-
<FooterGroup group={RESOURCES_GROUP} />
|
|
435
|
-
<FooterGroup group={COMPANY_GROUP} className="mt-10" />
|
|
436
|
-
</div>
|
|
437
|
-
<div>
|
|
438
|
-
<FooterGroup group={COMPARE_GROUP} />
|
|
439
|
-
</div>
|
|
440
|
-
</div>
|
|
441
|
-
|
|
442
|
-
<div className="mt-14 border-t border-zinc-200/70 pt-6 text-[12px] text-zinc-400">
|
|
443
|
-
<span>© {new Date().getFullYear()} {siteConfig.brand.copyrightName}</span>
|
|
444
|
-
</div>
|
|
445
|
-
</div>
|
|
446
|
-
</footer>
|
|
447
|
-
);
|
|
448
|
-
}
|
|
@@ -14,9 +14,12 @@ import {
|
|
|
14
14
|
type LucideIcon,
|
|
15
15
|
} from "lucide-react";
|
|
16
16
|
|
|
17
|
-
type NavKey = "overview" | "projects" | "members" | "billing" | "settings";
|
|
17
|
+
export type NavKey = "overview" | "projects" | "members" | "billing" | "settings";
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
// Exported so `app/dashboard/layout.tsx` can derive the active item + page
|
|
20
|
+
// title from the request URL — one table drives both the sidebar and the
|
|
21
|
+
// layout's routing awareness.
|
|
22
|
+
export const NAV: { key: NavKey; label: string; href: string; Icon: LucideIcon }[] = [
|
|
20
23
|
{ key: "overview", label: "Overview", href: "/dashboard", Icon: LayoutDashboard },
|
|
21
24
|
{ key: "projects", label: "Projects", href: "/dashboard/projects", Icon: FolderKanban },
|
|
22
25
|
{ key: "members", label: "Members", href: "/dashboard/members", Icon: Users },
|
|
@@ -25,10 +28,11 @@ const NAV: { key: NavKey; label: string; href: string; Icon: LucideIcon }[] = [
|
|
|
25
28
|
];
|
|
26
29
|
|
|
27
30
|
// Dashboard chrome: a fixed sidebar (logo, workspace switcher, nav) plus a top
|
|
28
|
-
// bar with a user menu.
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
31
|
+
// bar with a user menu. Rendered by `app/dashboard/layout.tsx`, which wraps
|
|
32
|
+
// every /dashboard page — /dashboard sits outside the `(marketing)` route
|
|
33
|
+
// group, so this shell is the only chrome here. `userEmail` is resolved on the
|
|
34
|
+
// server (serverData.get("User", …)) and passed in, so the menu shows a real
|
|
35
|
+
// email instead of a raw id.
|
|
32
36
|
export function DashboardShell({
|
|
33
37
|
active,
|
|
34
38
|
title,
|
package/templates/shop/AGENTS.md
CHANGED
|
@@ -6,7 +6,7 @@ Pylon serves the API, auth, sync, WebSocket, SSE, and native React 19 SSR from o
|
|
|
6
6
|
|
|
7
7
|
**Unified SSR app:**
|
|
8
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.
|
|
9
|
+
- `app/`: file-based SSR routes. `app/page.tsx` → `/`, `app/about/page.tsx` → `/about`, `app/blog/[slug]/page.tsx` → `/blog/:slug`. A `(group)` directory is stripped from the URL — `app/(marketing)/about/page.tsx` still serves `/about` — so a group's `layout.tsx` gives one section its own chrome (nav, footer) without changing any path; routes outside the group render without it. `app/layout.tsx` is the document shell; `app/error.tsx` / `app/not-found.tsx` are boundaries.
|
|
10
10
|
- `app/globals.css`: Tailwind v4 entrypoint (auto-compiled and injected).
|
|
11
11
|
- `functions/`: server functions, one per file, `default`-exported.
|
|
12
12
|
- `.pylon/`: local dev state (SQLite, jobs, sessions, uploads). Created by `pylon dev`. Do not commit.
|