@pylonsync/create-pylon 0.3.267 → 0.3.269
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 +18 -10
- package/package.json +1 -1
- package/templates/b2b/AGENTS.md +61 -0
- package/templates/b2b/README.md +62 -0
- package/templates/b2b/app/auth-form.tsx +142 -0
- package/templates/b2b/app/dashboard/dashboard-client.tsx +192 -0
- package/templates/b2b/app/dashboard/page.tsx +63 -0
- package/templates/b2b/app/error.tsx +43 -0
- package/templates/b2b/app/globals.css +139 -0
- package/templates/b2b/app/layout.tsx +71 -0
- package/templates/b2b/app/login/page.tsx +47 -0
- package/templates/b2b/app/not-found.tsx +29 -0
- package/templates/b2b/app/page.tsx +114 -0
- package/templates/b2b/app/robots.ts +12 -0
- package/templates/b2b/app/signup/page.tsx +44 -0
- package/templates/b2b/app/sitemap.ts +27 -0
- package/templates/b2b/app.ts +179 -0
- package/templates/b2b/components/ui/button.tsx +56 -0
- package/templates/b2b/components/ui/card.tsx +90 -0
- package/templates/b2b/components.json +20 -0
- package/templates/b2b/functions/_keep.ts +13 -0
- package/templates/b2b/gitignore +10 -0
- package/templates/b2b/lib/utils.ts +10 -0
- package/templates/b2b/package.json +33 -0
- package/templates/b2b/tsconfig.json +18 -0
- package/templates/barebones/AGENTS.md +61 -0
- package/templates/barebones/README.md +45 -0
- package/templates/barebones/app/error.tsx +43 -0
- package/templates/barebones/app/globals.css +139 -0
- package/templates/barebones/app/items-client.tsx +96 -0
- package/templates/barebones/app/layout.tsx +27 -0
- package/templates/barebones/app/not-found.tsx +29 -0
- package/templates/barebones/app/page.tsx +28 -0
- package/templates/barebones/app/robots.ts +12 -0
- package/templates/barebones/app/sitemap.ts +27 -0
- package/templates/barebones/app.ts +55 -0
- package/templates/barebones/components/ui/button.tsx +56 -0
- package/templates/barebones/components/ui/card.tsx +90 -0
- package/templates/barebones/components.json +20 -0
- package/templates/barebones/functions/_keep.ts +13 -0
- package/templates/barebones/gitignore +10 -0
- package/templates/barebones/lib/utils.ts +10 -0
- package/templates/barebones/package.json +33 -0
- package/templates/barebones/tsconfig.json +18 -0
- package/templates/chat/AGENTS.md +61 -0
- package/templates/chat/README.md +51 -0
- package/templates/chat/app/chat-client.tsx +113 -0
- package/templates/chat/app/error.tsx +43 -0
- package/templates/chat/app/globals.css +139 -0
- package/templates/chat/app/layout.tsx +25 -0
- package/templates/chat/app/not-found.tsx +29 -0
- package/templates/chat/app/page.tsx +26 -0
- package/templates/chat/app/robots.ts +12 -0
- package/templates/chat/app/sitemap.ts +27 -0
- package/templates/chat/app.ts +59 -0
- package/templates/chat/components/ui/button.tsx +56 -0
- package/templates/chat/components/ui/card.tsx +90 -0
- package/templates/chat/components.json +20 -0
- package/templates/chat/functions/_keep.ts +13 -0
- package/templates/chat/gitignore +10 -0
- package/templates/chat/lib/utils.ts +10 -0
- package/templates/chat/package.json +33 -0
- package/templates/chat/tsconfig.json +18 -0
- package/templates/consumer/AGENTS.md +61 -0
- package/templates/consumer/README.md +52 -0
- package/templates/consumer/app/error.tsx +43 -0
- package/templates/consumer/app/feed-client.tsx +154 -0
- package/templates/consumer/app/globals.css +139 -0
- package/templates/consumer/app/layout.tsx +27 -0
- package/templates/consumer/app/not-found.tsx +29 -0
- package/templates/consumer/app/page.tsx +27 -0
- package/templates/consumer/app/robots.ts +12 -0
- package/templates/consumer/app/sitemap.ts +27 -0
- package/templates/consumer/app.ts +89 -0
- package/templates/consumer/components/ui/button.tsx +56 -0
- package/templates/consumer/components/ui/card.tsx +90 -0
- package/templates/consumer/components.json +20 -0
- package/templates/consumer/functions/_keep.ts +13 -0
- package/templates/consumer/gitignore +10 -0
- package/templates/consumer/lib/utils.ts +10 -0
- package/templates/consumer/package.json +33 -0
- package/templates/consumer/tsconfig.json +18 -0
- package/templates/ssr/README.md +43 -28
- package/templates/ssr/app/dashboard/dashboard-client.tsx +154 -78
- package/templates/ssr/app/dashboard/page.tsx +16 -60
- package/templates/ssr/app/layout.tsx +46 -39
- package/templates/ssr/app/login/page.tsx +1 -1
- package/templates/ssr/app/page.tsx +182 -84
- package/templates/ssr/app/signup/page.tsx +1 -1
- package/templates/ssr/app.ts +134 -46
|
@@ -1,109 +1,107 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import React, { useState } from "react";
|
|
3
|
+
import React, { useCallback, useEffect, useState } from "react";
|
|
4
4
|
import { db } from "@pylonsync/react";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
useAuth,
|
|
7
|
+
OrganizationSwitcher,
|
|
8
|
+
listOrgMembers,
|
|
9
|
+
createInvite,
|
|
10
|
+
type OrgMember,
|
|
11
|
+
} from "@pylonsync/client";
|
|
6
12
|
import { Button } from "@/components/ui/button";
|
|
7
13
|
|
|
8
|
-
export interface
|
|
14
|
+
export interface Project {
|
|
9
15
|
id: string;
|
|
10
|
-
|
|
11
|
-
|
|
16
|
+
orgId: string;
|
|
17
|
+
name: string;
|
|
18
|
+
createdAt: string;
|
|
12
19
|
}
|
|
13
20
|
|
|
14
|
-
// The
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
// `initial` are the rows the server rendered into the HTML (see page.tsx).
|
|
21
|
-
// We show them on the first paint — before the local store has hydrated — so
|
|
22
|
-
// there's no empty flash, then hand off to the live data. Server-rendered for
|
|
23
|
-
// the first byte, local-first realtime after.
|
|
24
|
-
export function Dashboard({ initial }: { initial: Note[] }) {
|
|
25
|
-
const { signOut } = useAuth();
|
|
26
|
-
const [body, setBody] = useState("");
|
|
27
|
-
const { data: live, loading } = db.useQuery<Note>("Note");
|
|
28
|
-
const notes = !loading || live.length > 0 ? live : initial;
|
|
29
|
-
|
|
30
|
-
async function addNote(e: React.FormEvent) {
|
|
31
|
-
e.preventDefault();
|
|
32
|
-
const text = body.trim();
|
|
33
|
-
if (!text) return;
|
|
34
|
-
setBody("");
|
|
35
|
-
// We don't send ownerId — `field.owner()` stamps it from the session
|
|
36
|
-
// server-side and rejects any forged value, so this optimistic insert is
|
|
37
|
-
// safe.
|
|
38
|
-
await db.insert("Note", { body: text, done: false });
|
|
39
|
-
}
|
|
21
|
+
// The workspace. `<OrganizationSwitcher>` (from @pylonsync/client) lists your
|
|
22
|
+
// orgs, creates new ones, and switches your active tenant via
|
|
23
|
+
// /api/auth/select-org — all against the framework's built-in org system. The
|
|
24
|
+
// rest of the page keys off `tenantId` (your active org).
|
|
25
|
+
export function Workspace() {
|
|
26
|
+
const { tenantId, signOut } = useAuth();
|
|
40
27
|
|
|
41
28
|
async function onSignOut() {
|
|
42
|
-
// Clears the server session (DELETE /api/auth/session → the cookie is
|
|
43
|
-
// cleared), then we land back on the public homepage.
|
|
44
29
|
await signOut();
|
|
45
30
|
window.location.assign("/");
|
|
46
31
|
}
|
|
47
32
|
|
|
48
33
|
return (
|
|
49
|
-
<div className="space-y-
|
|
50
|
-
<div className="flex items-center justify-
|
|
34
|
+
<div className="space-y-6">
|
|
35
|
+
<div className="flex items-center justify-between gap-3">
|
|
36
|
+
<OrganizationSwitcher />
|
|
51
37
|
<Button variant="ghost" size="sm" onClick={onSignOut}>
|
|
52
38
|
Sign out
|
|
53
39
|
</Button>
|
|
54
40
|
</div>
|
|
55
41
|
|
|
56
|
-
|
|
42
|
+
{tenantId ? (
|
|
43
|
+
<div className="grid gap-6 sm:grid-cols-2">
|
|
44
|
+
<Projects orgId={tenantId} />
|
|
45
|
+
<Members orgId={tenantId} />
|
|
46
|
+
</div>
|
|
47
|
+
) : (
|
|
48
|
+
<div className="rounded-lg border border-dashed px-6 py-10 text-center text-sm text-muted-foreground">
|
|
49
|
+
Create or select an organization above to get started. Each org is an
|
|
50
|
+
isolated tenant — its projects and members are private to it.
|
|
51
|
+
</div>
|
|
52
|
+
)}
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Tenant-scoped data. `db.useQuery("Project")` returns only your active org's
|
|
58
|
+
// projects (the policy gates on `auth.tenantId == data.orgId`), and switching
|
|
59
|
+
// orgs re-syncs the list. `db.insert` is optimistic; we pass `orgId` = the
|
|
60
|
+
// active tenant so the row lands in this org — the policy rejects any other.
|
|
61
|
+
function Projects({ orgId }: { orgId: string }) {
|
|
62
|
+
const [name, setName] = useState("");
|
|
63
|
+
const { data: all } = db.useQuery<Project>("Project");
|
|
64
|
+
// Defensive filter while a tenant switch re-syncs the local replica.
|
|
65
|
+
const projects = all.filter((p) => p.orgId === orgId);
|
|
66
|
+
|
|
67
|
+
async function add(e: React.FormEvent) {
|
|
68
|
+
e.preventDefault();
|
|
69
|
+
const value = name.trim();
|
|
70
|
+
if (!value) return;
|
|
71
|
+
setName("");
|
|
72
|
+
await db.insert("Project", { orgId, name: value });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<section className="space-y-3">
|
|
77
|
+
<h2 className="text-sm font-semibold">Projects</h2>
|
|
78
|
+
<form onSubmit={add} className="flex items-center gap-2">
|
|
57
79
|
<input
|
|
58
|
-
value={
|
|
59
|
-
onChange={(e) =>
|
|
60
|
-
placeholder="
|
|
61
|
-
aria-label="
|
|
80
|
+
value={name}
|
|
81
|
+
onChange={(e) => setName(e.target.value)}
|
|
82
|
+
placeholder="New project…"
|
|
83
|
+
aria-label="Project name"
|
|
62
84
|
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
63
85
|
/>
|
|
64
|
-
<Button type="submit">
|
|
86
|
+
<Button type="submit" size="sm">
|
|
87
|
+
Add
|
|
88
|
+
</Button>
|
|
65
89
|
</form>
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
<p className="text-sm text-muted-foreground">
|
|
69
|
-
No notes yet — add one above. It appears instantly (optimistic) and
|
|
70
|
-
syncs; open this page in a second tab to watch it arrive live.
|
|
71
|
-
</p>
|
|
90
|
+
{projects.length === 0 ? (
|
|
91
|
+
<p className="text-sm text-muted-foreground">No projects yet.</p>
|
|
72
92
|
) : (
|
|
73
|
-
<ul className="space-y-
|
|
74
|
-
{
|
|
93
|
+
<ul className="space-y-1.5">
|
|
94
|
+
{projects.map((p) => (
|
|
75
95
|
<li
|
|
76
|
-
key={
|
|
77
|
-
className="flex items-center
|
|
96
|
+
key={p.id}
|
|
97
|
+
className="flex items-center justify-between rounded-md border px-3 py-2 text-sm"
|
|
78
98
|
>
|
|
99
|
+
<span className="truncate">{p.name}</span>
|
|
79
100
|
<button
|
|
80
101
|
type="button"
|
|
81
|
-
aria-label=
|
|
82
|
-
onClick={() =>
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
className={
|
|
86
|
-
note.done
|
|
87
|
-
? "text-emerald-600"
|
|
88
|
-
: "text-muted-foreground/50 hover:text-muted-foreground"
|
|
89
|
-
}
|
|
90
|
-
>
|
|
91
|
-
{note.done ? "✓" : "○"}
|
|
92
|
-
</button>
|
|
93
|
-
<span
|
|
94
|
-
className={
|
|
95
|
-
note.done
|
|
96
|
-
? "flex-1 line-through text-muted-foreground"
|
|
97
|
-
: "flex-1"
|
|
98
|
-
}
|
|
99
|
-
>
|
|
100
|
-
{note.body}
|
|
101
|
-
</span>
|
|
102
|
-
<button
|
|
103
|
-
type="button"
|
|
104
|
-
aria-label="Delete note"
|
|
105
|
-
onClick={() => db.delete("Note", note.id)}
|
|
106
|
-
className="text-muted-foreground/40 hover:text-red-600"
|
|
102
|
+
aria-label="Delete project"
|
|
103
|
+
onClick={() => db.delete("Project", p.id)}
|
|
104
|
+
className="text-muted-foreground/40 transition-colors hover:text-red-600"
|
|
107
105
|
>
|
|
108
106
|
✕
|
|
109
107
|
</button>
|
|
@@ -111,6 +109,84 @@ export function Dashboard({ initial }: { initial: Note[] }) {
|
|
|
111
109
|
))}
|
|
112
110
|
</ul>
|
|
113
111
|
)}
|
|
114
|
-
|
|
112
|
+
<p className="text-xs text-muted-foreground">
|
|
113
|
+
Tenant-scoped: only this org's projects, enforced by policy — switch
|
|
114
|
+
orgs and the list changes.
|
|
115
|
+
</p>
|
|
116
|
+
</section>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Membership + invites go through the framework's /api/auth/orgs/:id endpoints
|
|
121
|
+
// (the @pylonsync/client helpers). The framework gates invites to org admins,
|
|
122
|
+
// so a member calling createInvite gets a 403 — real RBAC, no extra code.
|
|
123
|
+
function Members({ orgId }: { orgId: string }) {
|
|
124
|
+
const [members, setMembers] = useState<OrgMember[] | null>(null);
|
|
125
|
+
const [email, setEmail] = useState("");
|
|
126
|
+
const [note, setNote] = useState("");
|
|
127
|
+
|
|
128
|
+
const refresh = useCallback(() => {
|
|
129
|
+
void listOrgMembers(orgId).then(setMembers);
|
|
130
|
+
}, [orgId]);
|
|
131
|
+
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
setMembers(null);
|
|
134
|
+
refresh();
|
|
135
|
+
}, [refresh]);
|
|
136
|
+
|
|
137
|
+
async function invite(e: React.FormEvent) {
|
|
138
|
+
e.preventDefault();
|
|
139
|
+
const value = email.trim();
|
|
140
|
+
if (!value) return;
|
|
141
|
+
setEmail("");
|
|
142
|
+
setNote("");
|
|
143
|
+
try {
|
|
144
|
+
await createInvite(orgId, value, "member");
|
|
145
|
+
setNote(`Invited ${value}.`);
|
|
146
|
+
refresh();
|
|
147
|
+
} catch {
|
|
148
|
+
setNote("Only org admins can invite members.");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<section className="space-y-3">
|
|
154
|
+
<h2 className="text-sm font-semibold">Members</h2>
|
|
155
|
+
{members === null ? (
|
|
156
|
+
<p className="text-sm text-muted-foreground">Loading…</p>
|
|
157
|
+
) : (
|
|
158
|
+
<ul className="space-y-1.5">
|
|
159
|
+
{members.map((m) => (
|
|
160
|
+
<li
|
|
161
|
+
key={m.user_id}
|
|
162
|
+
className="flex items-center justify-between rounded-md border px-3 py-2 text-sm"
|
|
163
|
+
>
|
|
164
|
+
<span className="truncate font-mono text-xs">
|
|
165
|
+
{shortId(m.user_id)}
|
|
166
|
+
</span>
|
|
167
|
+
<span className="text-xs text-muted-foreground">{m.role}</span>
|
|
168
|
+
</li>
|
|
169
|
+
))}
|
|
170
|
+
</ul>
|
|
171
|
+
)}
|
|
172
|
+
<form onSubmit={invite} className="flex items-center gap-2">
|
|
173
|
+
<input
|
|
174
|
+
type="email"
|
|
175
|
+
value={email}
|
|
176
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
177
|
+
placeholder="invite by email…"
|
|
178
|
+
aria-label="Invite email"
|
|
179
|
+
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
180
|
+
/>
|
|
181
|
+
<Button type="submit" size="sm" variant="outline">
|
|
182
|
+
Invite
|
|
183
|
+
</Button>
|
|
184
|
+
</form>
|
|
185
|
+
{note && <p className="text-xs text-muted-foreground">{note}</p>}
|
|
186
|
+
</section>
|
|
115
187
|
);
|
|
116
188
|
}
|
|
189
|
+
|
|
190
|
+
function shortId(id: string) {
|
|
191
|
+
return id.replace(/^user_/, "").slice(0, 10);
|
|
192
|
+
}
|
|
@@ -1,70 +1,26 @@
|
|
|
1
|
-
import React
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
type PageProps,
|
|
5
|
-
type ServerData,
|
|
6
|
-
} from "@pylonsync/react";
|
|
7
|
-
import { Dashboard, type Note } from "./dashboard-client";
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { type Metadata, type PageProps } from "@pylonsync/react";
|
|
3
|
+
import { Workspace } from "./dashboard-client";
|
|
8
4
|
|
|
9
5
|
export const metadata: Metadata = {
|
|
10
|
-
title: "Dashboard —
|
|
6
|
+
title: "Dashboard — Acme",
|
|
11
7
|
robots: "noindex",
|
|
12
8
|
};
|
|
13
9
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// arrives with your notes already in it (no empty flash); then the <Dashboard>
|
|
25
|
-
// island hydrates and takes over live.
|
|
26
|
-
function DashboardBody({
|
|
27
|
-
serverData,
|
|
28
|
-
userId,
|
|
29
|
-
}: {
|
|
30
|
-
serverData: ServerData;
|
|
31
|
-
userId: string;
|
|
32
|
-
}) {
|
|
33
|
-
const user = use(serverData.get<User>("User", userId));
|
|
34
|
-
const notes = use(serverData.list<Note>("Note"));
|
|
35
|
-
return (
|
|
36
|
-
<>
|
|
37
|
-
<p className="text-sm text-muted-foreground">
|
|
38
|
-
Signed in as{" "}
|
|
39
|
-
<span className="font-medium text-foreground">
|
|
40
|
-
{user?.displayName || user?.email || "you"}
|
|
41
|
-
</span>
|
|
42
|
-
.
|
|
43
|
-
</p>
|
|
44
|
-
<Dashboard initial={notes} />
|
|
45
|
-
</>
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// `app/dashboard/page.tsx` → `/dashboard`.
|
|
50
|
-
export default function DashboardPage({
|
|
51
|
-
auth,
|
|
52
|
-
response,
|
|
53
|
-
serverData,
|
|
54
|
-
}: PageProps) {
|
|
55
|
-
// Server-side auth gate: anonymous requests get a 307 to /login before any
|
|
56
|
-
// HTML. The redirect MUST fire here in the synchronous shell render — not
|
|
57
|
-
// inside the <Suspense> below — or React swallows it. No flash of the
|
|
58
|
-
// dashboard, works with JS disabled.
|
|
59
|
-
if (!auth.user_id) response.redirect("/login");
|
|
10
|
+
// `app/dashboard/page.tsx` → `/dashboard`. Server-side auth gate: anonymous
|
|
11
|
+
// requests get a 307 to /login before any HTML is sent (works with JS off, no
|
|
12
|
+
// flash of the dashboard). The redirect fires — and we return — in the
|
|
13
|
+
// synchronous shell render. The workspace itself is a client island that reads
|
|
14
|
+
// your active org from the session.
|
|
15
|
+
export default function DashboardPage({ auth, response }: PageProps) {
|
|
16
|
+
if (!auth.user_id) {
|
|
17
|
+
response.redirect("/login");
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
60
20
|
return (
|
|
61
21
|
<div className="space-y-6">
|
|
62
|
-
<h1 className="text-2xl font-semibold tracking-tight">
|
|
63
|
-
<
|
|
64
|
-
fallback={<p className="text-sm text-muted-foreground">Loading…</p>}
|
|
65
|
-
>
|
|
66
|
-
<DashboardBody serverData={serverData} userId={auth.user_id!} />
|
|
67
|
-
</Suspense>
|
|
22
|
+
<h1 className="text-2xl font-semibold tracking-tight">Workspace</h1>
|
|
23
|
+
<Workspace />
|
|
68
24
|
</div>
|
|
69
25
|
);
|
|
70
26
|
}
|
|
@@ -1,69 +1,76 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Link, type PageAuth } from "@pylonsync/react";
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
3
4
|
|
|
4
|
-
// A layout receives the page props plus `children`. `auth.user_id` is null
|
|
5
|
-
//
|
|
6
|
-
// server-side from the session cookie
|
|
7
|
-
// renders the right links on the first byte (no flash, no client fetch).
|
|
8
|
-
// `PageAuth` type is exported from @pylonsync/react so you never hand-roll it.
|
|
5
|
+
// A layout receives the page props plus `children`. `auth.user_id` is null for
|
|
6
|
+
// anonymous visitors and the signed-in user's id otherwise — resolved
|
|
7
|
+
// server-side from the session cookie before any HTML is sent, so the nav
|
|
8
|
+
// renders the right links on the first byte (no flash, no client fetch).
|
|
9
9
|
interface LayoutProps {
|
|
10
10
|
children: React.ReactNode;
|
|
11
11
|
url: string;
|
|
12
12
|
auth: PageAuth;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
// The root layout wraps every page.
|
|
15
|
+
// The root layout wraps every page: a marketing nav up top, a footer below.
|
|
16
|
+
// Rebrand "Acme" to your product.
|
|
16
17
|
export default function RootLayout({ children, auth }: LayoutProps) {
|
|
17
18
|
const signedIn = Boolean(auth?.user_id);
|
|
18
|
-
// Add `className="dark"` to this <html> to flip every shadcn token to its
|
|
19
|
-
// dark value. The classes below use semantic tokens (bg-background,
|
|
20
|
-
// text-foreground, …) so the whole UI re-themes from app/globals.css.
|
|
21
19
|
return (
|
|
22
20
|
<html lang="en">
|
|
23
21
|
<head>
|
|
24
22
|
<meta charSet="utf-8" />
|
|
25
23
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
26
|
-
<title>
|
|
24
|
+
<title>Acme</title>
|
|
27
25
|
{/* Tailwind is compiled by Pylon from app/globals.css and the
|
|
28
|
-
stylesheet link is injected here automatically
|
|
29
|
-
wire up. */}
|
|
26
|
+
stylesheet link is injected here automatically. */}
|
|
30
27
|
</head>
|
|
31
|
-
<body className="min-h-screen bg-background text-foreground antialiased">
|
|
32
|
-
<header className="sticky top-0 z-
|
|
33
|
-
<div className="mx-auto flex max-w-
|
|
34
|
-
<Link
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
28
|
+
<body className="flex min-h-screen flex-col bg-background text-foreground antialiased">
|
|
29
|
+
<header className="sticky top-0 z-20 border-b bg-background/70 backdrop-blur">
|
|
30
|
+
<div className="mx-auto flex max-w-5xl items-center justify-between px-4 py-3">
|
|
31
|
+
<Link href="/" className="flex items-center gap-2">
|
|
32
|
+
<span className="flex size-7 items-center justify-center rounded-md bg-primary text-sm font-bold text-primary-foreground">
|
|
33
|
+
A
|
|
34
|
+
</span>
|
|
35
|
+
<span className="text-sm font-semibold tracking-tight">Acme</span>
|
|
39
36
|
</Link>
|
|
40
|
-
<nav className="flex items-center gap-
|
|
41
|
-
<Link href="/" className="hover:text-foreground">
|
|
42
|
-
Home
|
|
43
|
-
</Link>
|
|
37
|
+
<nav className="flex items-center gap-2 text-sm">
|
|
44
38
|
{signedIn ? (
|
|
45
|
-
<
|
|
46
|
-
Dashboard
|
|
47
|
-
</
|
|
39
|
+
<Button asChild size="sm">
|
|
40
|
+
<Link href="/dashboard">Dashboard</Link>
|
|
41
|
+
</Button>
|
|
48
42
|
) : (
|
|
49
43
|
<>
|
|
50
|
-
<
|
|
51
|
-
Sign in
|
|
52
|
-
</
|
|
53
|
-
<
|
|
54
|
-
href="/signup"
|
|
55
|
-
|
|
56
|
-
>
|
|
57
|
-
Sign up
|
|
58
|
-
</Link>
|
|
44
|
+
<Button asChild size="sm" variant="ghost">
|
|
45
|
+
<Link href="/login">Sign in</Link>
|
|
46
|
+
</Button>
|
|
47
|
+
<Button asChild size="sm">
|
|
48
|
+
<Link href="/signup">Get started</Link>
|
|
49
|
+
</Button>
|
|
59
50
|
</>
|
|
60
51
|
)}
|
|
61
52
|
</nav>
|
|
62
53
|
</div>
|
|
63
54
|
</header>
|
|
64
|
-
|
|
65
|
-
<
|
|
66
|
-
|
|
55
|
+
|
|
56
|
+
<main className="mx-auto w-full max-w-5xl flex-1 px-4 py-10">
|
|
57
|
+
{children}
|
|
58
|
+
</main>
|
|
59
|
+
|
|
60
|
+
<footer className="border-t">
|
|
61
|
+
<div className="mx-auto flex max-w-5xl flex-col items-center justify-between gap-2 px-4 py-6 text-xs text-muted-foreground sm:flex-row">
|
|
62
|
+
<span>© Acme, Inc.</span>
|
|
63
|
+
<span>
|
|
64
|
+
Built with{" "}
|
|
65
|
+
<a
|
|
66
|
+
href="https://pylonsync.com"
|
|
67
|
+
className="font-medium text-foreground hover:underline"
|
|
68
|
+
>
|
|
69
|
+
Pylon
|
|
70
|
+
</a>{" "}
|
|
71
|
+
· one binary, one port
|
|
72
|
+
</span>
|
|
73
|
+
</div>
|
|
67
74
|
</footer>
|
|
68
75
|
</body>
|
|
69
76
|
</html>
|