@pylonsync/create-pylon 0.3.374 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pylonsync/create-pylon",
3
- "version": "0.3.374",
3
+ "version": "0.3.375",
4
4
  "description": "Scaffold a new Pylon app — realtime backend + web/mobile/expo frontends in one command. Run via `npm create @pylonsync/pylon@latest`.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -1,22 +1,26 @@
1
1
  import React from "react";
2
- import { Link } from "@pylonsync/react";
2
+ import { Link, type PageAuth, type SsrResponse } from "@pylonsync/react";
3
3
 
4
- // Server-rendered split-screen shell for /login and /signup: the form on the
5
- // left, a brand/testimonial panel on the right (hidden on small screens). The
6
- // form itself (the client island) is passed in as `children`.
7
- export function AuthShell({
8
- title,
9
- switchPrompt,
10
- switchLabel,
11
- switchHref,
12
- children,
13
- }: {
14
- title: string;
15
- switchPrompt: string;
16
- switchLabel: string;
17
- switchHref: string;
4
+ // `(auth)` route group → the shared split-screen frame for /login and /signup:
5
+ // the form card on the left, a brand/testimonial panel on the right (hidden on
6
+ // small screens). Each page supplies what differs heading, switch link, and
7
+ // the form — as `children`. The group segment adds no URL prefix, and because
8
+ // these routes sit outside `(marketing)`, none of the site nav/footer renders
9
+ // here.
10
+ interface LayoutProps {
18
11
  children: React.ReactNode;
19
- }) {
12
+ auth: PageAuth;
13
+ response: SsrResponse;
14
+ }
15
+
16
+ export default function AuthLayout({ children, auth, response }: LayoutProps) {
17
+ // Already signed in? Skip the auth screens entirely. Gating here covers
18
+ // every page in the group; `response.redirect` runs in the synchronous
19
+ // shell render, so it's a real 307 before any HTML is sent.
20
+ if (auth.user_id) {
21
+ response.redirect("/dashboard");
22
+ return null;
23
+ }
20
24
  return (
21
25
  <div className="grid min-h-screen bg-white lg:grid-cols-2">
22
26
  {/* Form side */}
@@ -27,19 +31,7 @@ export function AuthShell({
27
31
  A
28
32
  </span>
29
33
  </Link>
30
- <h1 className="mt-5 text-[22px] font-semibold tracking-tight text-zinc-900">
31
- {title}
32
- </h1>
33
- <p className="mt-1 text-[13px] text-zinc-500">
34
- {switchPrompt}{" "}
35
- <Link
36
- href={switchHref}
37
- className="font-medium text-zinc-900 underline underline-offset-2"
38
- >
39
- {switchLabel}
40
- </Link>
41
- </p>
42
- <div className="mt-6">{children}</div>
34
+ {children}
43
35
  </div>
44
36
  </div>
45
37
 
@@ -0,0 +1,33 @@
1
+ import React from "react";
2
+ import { Link, type Metadata } from "@pylonsync/react";
3
+ import { AuthForm } from "../auth-form";
4
+
5
+ export const metadata: Metadata = {
6
+ title: "Sign in — Acme",
7
+ robots: "noindex",
8
+ };
9
+
10
+ // `(auth)/login/page.tsx` → `/login`. The `(auth)` layout owns the
11
+ // split-screen frame and the already-signed-in redirect; this page is just
12
+ // the heading, the switch link, and the form.
13
+ export default function LoginPage() {
14
+ return (
15
+ <>
16
+ <h1 className="mt-5 text-[22px] font-semibold tracking-tight text-zinc-900">
17
+ Welcome back
18
+ </h1>
19
+ <p className="mt-1 text-[13px] text-zinc-500">
20
+ New to Acme?{" "}
21
+ <Link
22
+ href="/signup"
23
+ className="font-medium text-zinc-900 underline underline-offset-2"
24
+ >
25
+ Create an account
26
+ </Link>
27
+ </p>
28
+ <div className="mt-6">
29
+ <AuthForm mode="login" />
30
+ </div>
31
+ </>
32
+ );
33
+ }
@@ -0,0 +1,32 @@
1
+ import React from "react";
2
+ import { Link, type Metadata } from "@pylonsync/react";
3
+ import { AuthForm } from "../auth-form";
4
+
5
+ export const metadata: Metadata = {
6
+ title: "Create your account — Acme",
7
+ robots: "noindex",
8
+ };
9
+
10
+ // `(auth)/signup/page.tsx` → `/signup`. Register mode of the same form; the
11
+ // `(auth)` layout supplies the frame and the signed-in redirect.
12
+ export default function SignupPage() {
13
+ return (
14
+ <>
15
+ <h1 className="mt-5 text-[22px] font-semibold tracking-tight text-zinc-900">
16
+ Create an account
17
+ </h1>
18
+ <p className="mt-1 text-[13px] text-zinc-500">
19
+ Already have an account?{" "}
20
+ <Link
21
+ href="/login"
22
+ className="font-medium text-zinc-900 underline underline-offset-2"
23
+ >
24
+ Sign in
25
+ </Link>
26
+ </p>
27
+ <div className="mt-6">
28
+ <AuthForm mode="signup" />
29
+ </div>
30
+ </>
31
+ );
32
+ }
@@ -7,8 +7,8 @@ import { siteConfig } from "@/lib/site.config";
7
7
  // `(marketing)` is a ROUTE GROUP: the parens segment is stripped from every
8
8
  // URL (so `(marketing)/page.tsx` still serves `/`), and this layout wraps
9
9
  // only the pages inside the group — the marketing nav up top, a fat footer
10
- // below. Routes outside the group (/login, /signup, /dashboard) render bare
11
- // in the root shell; the dashboard brings its own sidebar shell.
10
+ // below. The other sections bring their own layouts: `(auth)` the split-screen
11
+ // frame, `dashboard/` the sidebar shell.
12
12
  //
13
13
  // A layout receives the page props plus `children`. `auth.user_id` is null for
14
14
  // anonymous visitors and the signed-in user's id otherwise — resolved
@@ -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 { Billing, type Subscription } from "../dashboard-client";
5
4
 
6
5
  export const metadata: Metadata = {
@@ -11,20 +10,15 @@ export const metadata: Metadata = {
11
10
  // `/dashboard/billing` — the active workspace's plan + Stripe checkout/portal.
12
11
  // The StripeSubscription row is resolved server-side (the @pylonsync/stripe
13
12
  // read policy scopes it to the active tenant), so the plan paints with no flash;
14
- // upgrade/manage open Stripe and the webhook keeps the row in sync.
13
+ // upgrade/manage open Stripe and the webhook keeps the row in sync. Auth gate +
14
+ // shell chrome come from the dashboard layout.
15
15
  const ACTIVE = ["active", "trialing", "past_due"];
16
16
 
17
17
  export default function BillingPage({ auth, response, serverData }: PageProps) {
18
- if (!auth.user_id) {
19
- response.redirect("/login");
20
- return null;
21
- }
22
18
  if (!auth.tenant_id) {
23
19
  response.redirect("/dashboard");
24
20
  return null;
25
21
  }
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
22
  const subs = use(serverData.list<Subscription>("StripeSubscription"));
29
23
  const subscription =
30
24
  subs.find(
@@ -33,17 +27,10 @@ export default function BillingPage({ auth, response, serverData }: PageProps) {
33
27
  subs[0] ??
34
28
  null;
35
29
  return (
36
- <DashboardShell
37
- active="billing"
38
- title="Billing"
39
- userEmail={me?.email ?? ""}
40
- orgName={org?.name}
41
- >
42
- <Billing
43
- tenantId={auth.tenant_id}
44
- role={auth.roles?.[0] ?? ""}
45
- subscription={subscription}
46
- />
47
- </DashboardShell>
30
+ <Billing
31
+ tenantId={auth.tenant_id}
32
+ role={auth.roles?.[0] ?? ""}
33
+ subscription={subscription}
34
+ />
48
35
  );
49
36
  }
@@ -0,0 +1,58 @@
1
+ import React, { use } from "react";
2
+ import type { PageAuth, ServerData, SsrResponse } from "@pylonsync/react";
3
+ import { DashboardShell, NAV } from "@/components/dashboard-shell";
4
+
5
+ // `app/dashboard/layout.tsx` wraps every /dashboard page in the sidebar +
6
+ // top-bar shell, so no page repeats the chrome. This is the thin-server-
7
+ // wrapper pattern: the layout gates auth, resolves the chrome's data
8
+ // server-side (user email, org name), derives the active nav item + title
9
+ // from the request URL, and hands the interactive shell (a `"use client"`
10
+ // component) its props.
11
+ interface LayoutProps {
12
+ children: React.ReactNode;
13
+ url: string;
14
+ auth: PageAuth;
15
+ response: SsrResponse;
16
+ serverData: ServerData;
17
+ }
18
+
19
+ export default function DashboardLayout({
20
+ children,
21
+ url,
22
+ auth,
23
+ response,
24
+ serverData,
25
+ }: LayoutProps) {
26
+ // The layout renders before any page below it, so gating here protects the
27
+ // whole /dashboard section: signed-out visitors get a real 307 to /login
28
+ // and no page code runs.
29
+ if (!auth.user_id) {
30
+ response.redirect("/login");
31
+ return null;
32
+ }
33
+ // No active workspace: render the page bare — /dashboard shows the
34
+ // full-screen ProvisionWorkspace flow, and the subpages redirect back to
35
+ // /dashboard themselves. The shell's org switcher has nothing to show yet.
36
+ if (!auth.tenant_id) {
37
+ return <>{children}</>;
38
+ }
39
+ const me = use(serverData.get<{ email?: string }>("User", auth.user_id));
40
+ const org = use(serverData.get<{ name?: string }>("Org", auth.tenant_id));
41
+ // Longest matching NAV href wins so /dashboard/projects highlights
42
+ // Projects, not Overview.
43
+ const path = (url ?? "").split("?")[0];
44
+ const nav =
45
+ NAV.filter((n) => path === n.href || path.startsWith(n.href + "/")).sort(
46
+ (a, b) => b.href.length - a.href.length,
47
+ )[0] ?? NAV[0];
48
+ return (
49
+ <DashboardShell
50
+ active={nav.key}
51
+ title={nav.label}
52
+ userEmail={me?.email ?? ""}
53
+ orgName={org?.name}
54
+ >
55
+ {children}
56
+ </DashboardShell>
57
+ );
58
+ }
@@ -1,6 +1,5 @@
1
- import React, { use } from "react";
1
+ import React from "react";
2
2
  import { type Metadata, type PageProps } from "@pylonsync/react";
3
- import { DashboardShell } from "@/components/dashboard-shell";
4
3
  import { Members } from "../dashboard-client";
5
4
 
6
5
  export const metadata: Metadata = {
@@ -10,30 +9,18 @@ export const metadata: Metadata = {
10
9
 
11
10
  // `/dashboard/members` — the active org's roster + invites. The roster (with
12
11
  // real emails) is loaded client-side from the framework's org-members endpoint;
13
- // invites are gated to owners/admins both here and on the server.
14
- export default function MembersPage({ auth, response, serverData }: PageProps) {
15
- if (!auth.user_id) {
16
- response.redirect("/login");
17
- return null;
18
- }
12
+ // invites are gated to owners/admins both here and on the server. Auth gate +
13
+ // shell chrome come from the dashboard layout.
14
+ export default function MembersPage({ auth, response }: PageProps) {
19
15
  if (!auth.tenant_id) {
20
16
  response.redirect("/dashboard");
21
17
  return null;
22
18
  }
23
- const me = use(serverData.get<{ email?: string }>("User", auth.user_id));
24
- const org = use(serverData.get<{ name?: string }>("Org", auth.tenant_id));
25
19
  return (
26
- <DashboardShell
27
- active="members"
28
- title="Members"
29
- userEmail={me?.email ?? ""}
30
- orgName={org?.name}
31
- >
32
- <Members
33
- tenantId={auth.tenant_id}
34
- currentUserId={auth.user_id}
35
- role={auth.roles?.[0] ?? ""}
36
- />
37
- </DashboardShell>
20
+ <Members
21
+ tenantId={auth.tenant_id}
22
+ currentUserId={auth.user_id!}
23
+ role={auth.roles?.[0] ?? ""}
24
+ />
38
25
  );
39
26
  }
@@ -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 { ProvisionWorkspace } from "./provision-workspace";
5
4
  import {
6
5
  Overview,
@@ -14,29 +13,20 @@ export const metadata: Metadata = {
14
13
  robots: "noindex",
15
14
  };
16
15
 
17
- // `app/dashboard/page.tsx` → `/dashboard`. Server-side auth gate, then the
18
- // active org (`auth.tenant_id`) + this org's rows are read during the render
19
- // via `serverData` + React 19 `use()` — resolved server-side and replayed on
20
- // hydration, so the dashboard paints with real data on the first byte (no
21
- // client fetch, no empty-state flash). /dashboard sits outside the
22
- // `(marketing)` route group, so the marketing nav/footer never render here;
23
- // the shell is the only chrome.
24
- export default function DashboardPage({
25
- auth,
26
- response,
27
- serverData,
28
- }: PageProps) {
29
- if (!auth.user_id) {
30
- response.redirect("/login");
31
- return null;
32
- }
16
+ // `app/dashboard/page.tsx` → `/dashboard`. The dashboard layout owns the auth
17
+ // gate and the shell chrome; this page reads the active org's rows during the
18
+ // render via `serverData` + React 19 `use()` — resolved server-side and
19
+ // replayed on hydration, so the dashboard paints with real data on the first
20
+ // byte (no client fetch, no empty-state flash).
21
+ export default function DashboardPage({ auth, serverData }: PageProps) {
33
22
  // No active workspace (signup's auto-provision failed, or the user left/
34
23
  // deleted their last org). Every read below is tenant-scoped, so instead of
35
24
  // an empty shell, provision one client-side and reload into a ready dashboard.
25
+ // The layout renders this bare (no shell) when there's no tenant.
36
26
  if (!auth.tenant_id) {
37
27
  return <ProvisionWorkspace />;
38
28
  }
39
- const me = use(serverData.get<{ email?: string }>("User", auth.user_id));
29
+ const me = use(serverData.get<{ email?: string }>("User", auth.user_id!));
40
30
  const org = use(serverData.get<{ name?: string }>("Org", auth.tenant_id));
41
31
  const projects = use(serverData.list<Project>("Project"));
42
32
  const members = use(serverData.list<OrgMemberRow>("OrgMember"));
@@ -51,20 +41,13 @@ export default function DashboardPage({
51
41
  );
52
42
  const plan = active ? active.plan : "free";
53
43
  return (
54
- <DashboardShell
55
- active="overview"
56
- title="Overview"
57
- userEmail={me?.email ?? ""}
44
+ <Overview
45
+ tenantId={auth.tenant_id}
58
46
  orgName={org?.name}
59
- >
60
- <Overview
61
- tenantId={auth.tenant_id}
62
- orgName={org?.name}
63
- userEmail={me?.email}
64
- projects={projects}
65
- memberCount={memberCount}
66
- plan={plan}
67
- />
68
- </DashboardShell>
47
+ userEmail={me?.email}
48
+ projects={projects}
49
+ memberCount={memberCount}
50
+ plan={plan}
51
+ />
69
52
  );
70
53
  }
@@ -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
- export default function ProjectsPage({
14
- auth,
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
- <DashboardShell
35
- active="settings"
36
- title="Settings"
37
- userEmail={me?.email ?? ""}
38
- orgName={org?.name}
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
  }
@@ -6,11 +6,10 @@ interface LayoutProps {
6
6
  }
7
7
 
8
8
  // The root layout is only the document shell: <html>, <head>, <body>, and the
9
- // site-wide theme variables. Section chrome lives in route-group layouts
10
- // `(marketing)/layout.tsx` adds the nav + footer for every marketing page.
11
- // Routes outside the group (/login, /signup, /dashboard) render bare here:
12
- // the auth screens are full-screen and the dashboard brings its own sidebar
13
- // shell.
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.
14
13
  export default function RootLayout({ children }: LayoutProps) {
15
14
  return (
16
15
  <html
@@ -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
- const NAV: { key: NavKey; label: string; href: string; Icon: LucideIcon }[] = [
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. /dashboard sits outside the `(marketing)` route group,
29
- // so this shell is the only chrome here. `userEmail` is
30
- // resolved on the server (serverData.get("User", …)) and passed in, so the menu
31
- // shows a real email instead of a raw id.
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,
@@ -1,28 +0,0 @@
1
- import React from "react";
2
- import { type Metadata, type PageProps } from "@pylonsync/react";
3
- import { AuthShell } from "../auth-shell";
4
- import { AuthForm } from "../auth-form";
5
-
6
- export const metadata: Metadata = {
7
- title: "Sign in — Acme",
8
- robots: "noindex",
9
- };
10
-
11
- // `app/login/page.tsx` → `/login`. Split-screen auth (form left, brand right).
12
- // Auth routes sit outside the `(marketing)` route group, so they render bare —
13
- // no marketing nav or footer.
14
- export default function LoginPage({ auth, response }: PageProps) {
15
- // Already signed in? Skip the form. `response.redirect` runs in the
16
- // synchronous shell render, so it's a real 307 before any HTML is sent.
17
- if (auth.user_id) response.redirect("/dashboard");
18
- return (
19
- <AuthShell
20
- title="Welcome back"
21
- switchPrompt="New to Acme?"
22
- switchLabel="Create an account"
23
- switchHref="/signup"
24
- >
25
- <AuthForm mode="login" />
26
- </AuthShell>
27
- );
28
- }
@@ -1,24 +0,0 @@
1
- import React from "react";
2
- import { type Metadata, type PageProps } from "@pylonsync/react";
3
- import { AuthShell } from "../auth-shell";
4
- import { AuthForm } from "../auth-form";
5
-
6
- export const metadata: Metadata = {
7
- title: "Create your account — Acme",
8
- robots: "noindex",
9
- };
10
-
11
- // `app/signup/page.tsx` → `/signup`. Split-screen auth, register mode.
12
- export default function SignupPage({ auth, response }: PageProps) {
13
- if (auth.user_id) response.redirect("/dashboard");
14
- return (
15
- <AuthShell
16
- title="Create an account"
17
- switchPrompt="Already have an account?"
18
- switchLabel="Sign in"
19
- switchHref="/login"
20
- >
21
- <AuthForm mode="signup" />
22
- </AuthShell>
23
- );
24
- }