@pylonsync/create-pylon 0.3.356 → 0.3.358

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/package.json +1 -1
  2. package/templates/marketplace/README.md +29 -14
  3. package/templates/marketplace/app/error.tsx +30 -16
  4. package/templates/marketplace/app/globals.css +140 -24
  5. package/templates/marketplace/app/layout.tsx +60 -31
  6. package/templates/marketplace/app/listing/[id]/page.tsx +160 -60
  7. package/templates/marketplace/app/me/page.tsx +3 -3
  8. package/templates/marketplace/app/not-found.tsx +19 -10
  9. package/templates/marketplace/app/page.tsx +287 -97
  10. package/templates/marketplace/app/sell/page.tsx +11 -9
  11. package/templates/marketplace/app.ts +5 -4
  12. package/templates/marketplace/client/AuthNav.tsx +4 -8
  13. package/templates/marketplace/client/LiveListingStatus.tsx +60 -0
  14. package/templates/marketplace/client/LiveTicker.tsx +22 -10
  15. package/templates/marketplace/client/LoginCard.tsx +108 -62
  16. package/templates/marketplace/client/MarketProvider.tsx +16 -2
  17. package/templates/marketplace/client/MyMarket.tsx +126 -56
  18. package/templates/marketplace/client/OfferPanel.tsx +222 -112
  19. package/templates/marketplace/client/ScrollToListingsLink.tsx +29 -0
  20. package/templates/marketplace/client/SeedOnEmpty.tsx +7 -6
  21. package/templates/marketplace/client/SellForm.tsx +198 -40
  22. package/templates/marketplace/client/ThemeToggle.tsx +62 -0
  23. package/templates/marketplace/client/WatchButton.tsx +19 -8
  24. package/templates/marketplace/client/market.ts +10 -4
  25. package/templates/marketplace/components/ui/alert.tsx +66 -0
  26. package/templates/marketplace/components/ui/badge.tsx +5 -1
  27. package/templates/marketplace/components/ui/button.tsx +9 -7
  28. package/templates/marketplace/components/ui/card.tsx +2 -2
  29. package/templates/marketplace/components/ui/empty.tsx +104 -0
  30. package/templates/marketplace/components/ui/field.tsx +246 -0
  31. package/templates/marketplace/components/ui/input.tsx +2 -2
  32. package/templates/marketplace/components/ui/label.tsx +4 -3
  33. package/templates/marketplace/components/ui/native-select.tsx +62 -0
  34. package/templates/marketplace/components/ui/separator.tsx +26 -0
  35. package/templates/marketplace/components/ui/skeleton.tsx +13 -0
  36. package/templates/marketplace/components/ui/spinner.tsx +16 -0
  37. package/templates/marketplace/components/ui/textarea.tsx +2 -2
  38. package/templates/marketplace/{ui → components/ui}/tokens.css +1 -1
  39. package/templates/marketplace/components.json +20 -0
  40. package/templates/marketplace/functions/seedMarket.ts +14 -12
  41. package/templates/marketplace/lib/catalog.ts +32 -0
  42. package/templates/marketplace/package.json +10 -9
  43. package/templates/marketplace/public/images/marketplace-hero.webp +0 -0
  44. package/templates/marketplace/tests/example.test.ts +76 -8
  45. package/templates/marketplace/tsconfig.json +13 -2
  46. package/templates/marketplace/components/ui/select.tsx +0 -39
  47. package/templates/marketplace/ui/badge.tsx +0 -30
  48. package/templates/marketplace/ui/button.tsx +0 -49
  49. package/templates/marketplace/ui/card.tsx +0 -48
  50. package/templates/marketplace/ui/input.tsx +0 -17
  51. package/templates/marketplace/ui/label.tsx +0 -18
  52. package/templates/marketplace/ui/textarea.tsx +0 -17
  53. package/templates/marketplace/ui/utils.ts +0 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pylonsync/create-pylon",
3
- "version": "0.3.356",
3
+ "version": "0.3.358",
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,7 +1,8 @@
1
1
  # __APP_NAME__
2
2
 
3
- A live, two-sided marketplace built with [Pylon](https://pylonsync.com), with
4
- server-rendered listings and realtime offers.
3
+ A polished, two-sided resale marketplace built with
4
+ [Pylon](https://pylonsync.com), with server-rendered product discovery and
5
+ realtime offers.
5
6
 
6
7
  Anyone can list an item; anyone else can make an offer or buy it now; sellers
7
8
  watch offers arrive and accept or decline them. Every write reaches each open
@@ -16,7 +17,8 @@ __RUN_DEV__
16
17
  Open http://localhost:4321. The grid seeds itself on first load. Open a
17
18
  second tab, post something from `/sell` in one, and watch it hit the "just
18
19
  listed" ticker in the other with no refresh. Sign in (the `/sell` form prefills
19
- a demo account) to list, make offers, and watch your `/me` inbox light up live.
20
+ a demo account) to list, save finds, make offers, and watch your dashboard
21
+ update live.
20
22
 
21
23
  ## How it works
22
24
 
@@ -24,35 +26,48 @@ a demo account) to list, make offers, and watch your `/me` inbox light up live.
24
26
  page (`/listing/:slug`) render on the server with real rows (`serverData` +
25
27
  React 19 `use()`). View source and the products are *in the HTML*.
26
28
  - **Realtime where it matters.** The "just listed" ticker, the live offers on a
27
- listing, and your `/me` inbox all ride the sync engine one `db.useQuery` per
29
+ listing, and your `/me` dashboard all ride the sync engine: one `db.useQuery` per
28
30
  view, no polling. The public surface connects with an anonymous **guest
29
31
  session** (read-only); writing (list/offer/buy) requires a real sign-in.
30
32
  - **Unspoofable ownership.** `sellerId`/`buyerId` use `field.owner()`, so the
31
- framework stamps them from the session and rejects forged values — listings
33
+ framework stamps them from the session and rejects forged values. Listings
32
34
  and offers can be created with a plain optimistic `db.insert` and still can't
33
35
  be spoofed. The heavier logic (accept = mark sold + auto-decline the rest)
34
- runs in `functions/respondToOffer.ts` where it enforces "only the seller".
36
+ runs in `functions/respondToOffer.ts`, where it enforces "only the seller".
37
+ - **Commerce-ready discovery.** Browse by category, search title, description,
38
+ seller, or category, and sort by recency or price. The logic is pure and
39
+ covered by tests in `tests/example.test.ts`.
35
40
 
36
41
  ## Privacy and policies
37
42
 
38
43
  - `Listing` + `Offer` are **public-read** (buyers and sellers both see live
39
44
  state); writes are owner-scoped.
40
- - `Watch` (your saved listings) is **private** read/write only your own rows.
45
+ - `Watch` (your saved listings) is **private**. You can only read and write your
46
+ own rows.
41
47
  - `User` rows are readable only to signed-in users (for seller/buyer names);
42
48
  `passwordHash` is `serverOnly` and never serialized. Auth writes go through
43
49
  `/api/auth/password/*`, not the entity API.
44
50
 
45
51
  ## Listing photos
46
52
 
47
- Listings render a **deterministic gradient + category icon** from a `seed` —
48
- so the demo needs no image hosting. To use real photos, add an `imageUrl` field
49
- to `Listing` in `app.ts`, collect it in `client/SellForm.tsx` (upload via
50
- `/api/files`), and render an `<img>` in the grid + detail page instead of the
51
- gradient.
53
+ Seeded listings include purpose-matched
54
+ [Unsplash](https://unsplash.com/license) photography. New listings collect a
55
+ JPG, PNG, or WebP upload (up to 8 MB), with a direct-link fallback and preview
56
+ before publishing. Uploads use Pylon's direct-to-storage three-step flow
57
+ (`/api/files/init` → signed upload URL → `/api/files/confirm`) so large files
58
+ do not pass through the app server. `imageUrl` stays optional so older rows
59
+ fall back to deterministic category artwork. The generated hero asset is a
60
+ 44 KB WebP under `public/images`.
61
+
62
+ ## Payments and fulfillment
63
+
64
+ `buyNow` atomically records an accepted offer, marks the listing sold, and
65
+ declines competing bids. It does not transfer money. Add your payment provider
66
+ and shipping or pickup workflow before accepting real transactions.
52
67
 
53
68
  ## Rebrand it
54
69
 
55
- The brand ("Pylon Market") lives in `app/layout.tsx`; the demo catalog +
70
+ The brand ("Reprise") lives in `app/layout.tsx`; the demo catalog +
56
71
  seed account are in `functions/seedMarket.ts`. The design tokens are in
57
72
  `ui/tokens.css` + `app/globals.css`.
58
73
 
@@ -60,7 +75,7 @@ seed account are in `functions/seedMarket.ts`. The design tokens are in
60
75
 
61
76
  ```
62
77
  app.ts User + Listing + Offer + Watch + policies
63
- app/page.tsx SSR browse grid + category facets
78
+ app/page.tsx SSR discovery, search, sort, category facets
64
79
  app/listing/[id]/page.tsx SSR listing detail (+ generateMetadata)
65
80
  app/sell/page.tsx list an item (sign-in gated)
66
81
  app/me/page.tsx your listings, offers, watchlist (live)
@@ -1,26 +1,40 @@
1
1
  import React from "react";
2
2
  import { type ErrorBoundaryProps } from "@pylonsync/react";
3
+ import { Button } from "@/components/ui/button";
4
+ import {
5
+ Empty,
6
+ EmptyContent,
7
+ EmptyDescription,
8
+ EmptyHeader,
9
+ EmptyTitle,
10
+ } from "@/components/ui/empty";
3
11
 
4
12
  // `app/error.tsx` → the error boundary for this segment. Hydrated + interactive:
5
13
  // `reset()` re-attempts the route. The thrown error reaches the client as
6
14
  // `{ message, digest }` only — the stack stays in the dev overlay / server logs.
7
15
  export default function Error({ error, reset }: ErrorBoundaryProps) {
8
16
  return (
9
- <div className="mx-auto flex min-h-[60vh] max-w-3xl flex-col items-center justify-center px-6 text-center">
10
- <h1 className="text-2xl font-semibold tracking-tight">Something went wrong</h1>
11
- <p className="mt-2 text-muted-foreground">{error.message}</p>
12
- {error.digest ? (
13
- <p className="mt-1 text-xs text-muted-foreground">
14
- Reference: <code>{error.digest}</code>
15
- </p>
16
- ) : null}
17
- <button
18
- type="button"
19
- onClick={reset}
20
- className="mt-6 inline-flex h-10 items-center rounded-md bg-foreground px-5 text-sm font-medium text-background transition hover:opacity-90"
21
- >
22
- Try again
23
- </button>
24
- </div>
17
+ <Empty className="mx-auto min-h-[60vh] max-w-3xl border-0 px-6">
18
+ <EmptyHeader>
19
+ <EmptyTitle className="text-2xl">Something went wrong</EmptyTitle>
20
+ <EmptyDescription className="max-w-md">
21
+ Reprise could not load this page. Try again, or return to browse if
22
+ the problem continues.
23
+ </EmptyDescription>
24
+ {error.digest ? (
25
+ <p className="text-xs text-muted-foreground">
26
+ Reference: <code>{error.digest}</code>
27
+ </p>
28
+ ) : null}
29
+ </EmptyHeader>
30
+ <EmptyContent>
31
+ <Button type="button" onClick={reset}>
32
+ Try again
33
+ </Button>
34
+ <Button asChild variant="link">
35
+ <a href="/">Back to browse</a>
36
+ </Button>
37
+ </EmptyContent>
38
+ </Empty>
25
39
  );
26
40
  }
@@ -1,39 +1,88 @@
1
1
  @import "tailwindcss";
2
2
  @import "tw-animate-css";
3
- @import "../ui/tokens.css";
3
+ @import "../components/ui/tokens.css";
4
+
5
+ @custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
4
6
 
5
7
  /* Tailwind v4 scans these for class names (relative to this file). */
6
8
  @source "./";
7
9
  @source "../client";
10
+ @source "../components";
8
11
  @source "../../_shared/src";
9
12
 
10
13
  @theme {
11
- --font-sans: "Inter", -apple-system, system-ui, sans-serif;
12
- --font-mono: "JetBrains Mono", ui-monospace, "SF Mono", monospace;
13
-
14
- --color-background: oklch(0.99 0 0);
15
- --color-foreground: oklch(0.16 0 0);
16
- --color-card: oklch(1 0 0);
17
- --color-card-foreground: oklch(0.16 0 0);
18
- --color-popover: oklch(1 0 0);
19
- --color-popover-foreground: oklch(0.16 0 0);
20
- --color-primary: oklch(0.21 0 0);
21
- --color-primary-foreground: oklch(0.985 0 0);
22
- --color-secondary: oklch(0.97 0 0);
23
- --color-secondary-foreground: oklch(0.21 0 0);
24
- --color-muted: oklch(0.97 0 0);
25
- --color-muted-foreground: oklch(0.55 0 0);
26
- --color-accent: oklch(0.97 0 0);
27
- --color-accent-foreground: oklch(0.21 0 0);
14
+ --font-sans: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI",
15
+ sans-serif;
16
+ --font-mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
17
+
18
+ --color-background: oklch(0.975 0.004 85);
19
+ --color-foreground: oklch(0.17 0.004 85);
20
+ --color-card: oklch(0.995 0.002 85);
21
+ --color-card-foreground: oklch(0.17 0.004 85);
22
+ --color-popover: oklch(0.995 0.002 85);
23
+ --color-popover-foreground: oklch(0.17 0.004 85);
24
+ --color-primary: oklch(0.19 0.004 85);
25
+ --color-primary-foreground: oklch(0.98 0.002 85);
26
+ --color-secondary: oklch(0.94 0.006 85);
27
+ --color-secondary-foreground: oklch(0.21 0.004 85);
28
+ --color-muted: oklch(0.945 0.006 85);
29
+ --color-muted-foreground: oklch(0.48 0.008 85);
30
+ --color-accent: oklch(0.93 0.009 85);
31
+ --color-accent-foreground: oklch(0.21 0.004 85);
28
32
  --color-destructive: oklch(0.577 0.245 27.325);
29
33
  --color-destructive-foreground: oklch(0.985 0 0);
30
- --color-border: oklch(0.92 0 0);
31
- --color-input: oklch(0.92 0 0);
32
- --color-ring: oklch(0.71 0 0);
34
+ --color-success: oklch(0.62 0.14 153);
35
+ --color-success-foreground: oklch(0.38 0.1 153);
36
+ --color-warning: oklch(0.72 0.14 75);
37
+ --color-warning-foreground: oklch(0.42 0.1 65);
38
+ --color-border: oklch(0.89 0.007 85);
39
+ --color-input: oklch(0.86 0.008 85);
40
+ --color-ring: oklch(0.53 0.015 85);
41
+
42
+ --radius-lg: 1rem;
43
+ --radius-md: 0.625rem;
44
+ --radius-sm: 0.5rem;
45
+ }
46
+
47
+ :root {
48
+ color-scheme: light;
49
+ --shadow-border:
50
+ 0 0 0 1px rgb(0 0 0 / 0.06),
51
+ 0 1px 2px -1px rgb(0 0 0 / 0.06),
52
+ 0 8px 24px -16px rgb(41 37 33 / 0.18);
53
+ --shadow-border-hover:
54
+ 0 0 0 1px rgb(0 0 0 / 0.09),
55
+ 0 2px 4px -2px rgb(0 0 0 / 0.08),
56
+ 0 16px 32px -18px rgb(41 37 33 / 0.24);
57
+ }
58
+
59
+ :root[data-theme="light"] {
60
+ color-scheme: light;
61
+ }
33
62
 
34
- --radius-lg: 0.625rem;
35
- --radius-md: calc(0.625rem - 2px);
36
- --radius-sm: calc(0.625rem - 4px);
63
+ :root[data-theme="dark"] {
64
+ color-scheme: dark;
65
+ --color-background: oklch(0.15 0.004 85);
66
+ --color-foreground: oklch(0.94 0.004 85);
67
+ --color-card: oklch(0.19 0.005 85);
68
+ --color-card-foreground: oklch(0.94 0.004 85);
69
+ --color-popover: oklch(0.19 0.005 85);
70
+ --color-popover-foreground: oklch(0.94 0.004 85);
71
+ --color-primary: oklch(0.93 0.004 85);
72
+ --color-primary-foreground: oklch(0.17 0.004 85);
73
+ --color-secondary: oklch(0.24 0.006 85);
74
+ --color-secondary-foreground: oklch(0.94 0.004 85);
75
+ --color-muted: oklch(0.23 0.006 85);
76
+ --color-muted-foreground: oklch(0.7 0.007 85);
77
+ --color-accent: oklch(0.25 0.008 85);
78
+ --color-accent-foreground: oklch(0.94 0.004 85);
79
+ --color-border: oklch(0.3 0.007 85);
80
+ --color-input: oklch(0.34 0.008 85);
81
+ --color-ring: oklch(0.68 0.01 85);
82
+ --color-success-foreground: oklch(0.82 0.12 153);
83
+ --color-warning-foreground: oklch(0.84 0.12 75);
84
+ --shadow-border: 0 0 0 1px rgb(255 255 255 / 0.08);
85
+ --shadow-border-hover: 0 0 0 1px rgb(255 255 255 / 0.14);
37
86
  }
38
87
 
39
88
  html,
@@ -41,6 +90,70 @@ body {
41
90
  height: 100%;
42
91
  }
43
92
 
93
+ html {
94
+ -webkit-tap-highlight-color: rgb(23 23 23 / 0.08);
95
+ }
96
+
97
+ h1,
98
+ h2,
99
+ h3 {
100
+ scroll-margin-top: 6rem;
101
+ text-wrap: balance;
102
+ }
103
+
104
+ button,
105
+ a,
106
+ input,
107
+ select,
108
+ textarea {
109
+ touch-action: manipulation;
110
+ }
111
+
112
+ :where(a, button, input, select, textarea):focus-visible {
113
+ outline: 2px solid var(--color-ring);
114
+ outline-offset: 2px;
115
+ }
116
+
117
+ p,
118
+ li {
119
+ text-wrap: pretty;
120
+ }
121
+
122
+ .hide-scrollbar {
123
+ scrollbar-width: none;
124
+ }
125
+
126
+ .hide-scrollbar::-webkit-scrollbar {
127
+ display: none;
128
+ }
129
+
130
+ @media (prefers-color-scheme: dark) {
131
+ :root:not([data-theme]) {
132
+ color-scheme: dark;
133
+ --color-background: oklch(0.15 0.004 85);
134
+ --color-foreground: oklch(0.94 0.004 85);
135
+ --color-card: oklch(0.19 0.005 85);
136
+ --color-card-foreground: oklch(0.94 0.004 85);
137
+ --color-popover: oklch(0.19 0.005 85);
138
+ --color-popover-foreground: oklch(0.94 0.004 85);
139
+ --color-primary: oklch(0.93 0.004 85);
140
+ --color-primary-foreground: oklch(0.17 0.004 85);
141
+ --color-secondary: oklch(0.24 0.006 85);
142
+ --color-secondary-foreground: oklch(0.94 0.004 85);
143
+ --color-muted: oklch(0.23 0.006 85);
144
+ --color-muted-foreground: oklch(0.7 0.007 85);
145
+ --color-accent: oklch(0.25 0.008 85);
146
+ --color-accent-foreground: oklch(0.94 0.004 85);
147
+ --color-border: oklch(0.3 0.007 85);
148
+ --color-input: oklch(0.34 0.008 85);
149
+ --color-ring: oklch(0.68 0.01 85);
150
+ --color-success-foreground: oklch(0.82 0.12 153);
151
+ --color-warning-foreground: oklch(0.84 0.12 75);
152
+ --shadow-border: 0 0 0 1px rgb(255 255 255 / 0.08);
153
+ --shadow-border-hover: 0 0 0 1px rgb(255 255 255 / 0.14);
154
+ }
155
+ }
156
+
44
157
  /* Live "just listed" ticker. Items are doubled in the markup, so translating
45
158
  the track by -50% loops seamlessly. */
46
159
  @keyframes market-marquee {
@@ -61,4 +174,7 @@ body {
61
174
  .market-marquee {
62
175
  animation: none;
63
176
  }
177
+ .animate-pulse {
178
+ animation: none;
179
+ }
64
180
  }
@@ -1,6 +1,9 @@
1
1
  import React from "react";
2
2
  import { Link } from "@pylonsync/react";
3
+ import { Button } from "@/components/ui/button";
4
+ import { Separator } from "@/components/ui/separator";
3
5
  import { AuthNav } from "../client/AuthNav";
6
+ import { ThemeToggle } from "../client/ThemeToggle";
4
7
 
5
8
  // Root layout. Server-rendered shell; Pylon's SSR head adapter injects the
6
9
  // compiled Tailwind <link> from app/globals.css automatically.
@@ -14,45 +17,71 @@ export default function RootLayout({
14
17
  <head>
15
18
  <meta charSet="utf-8" />
16
19
  <meta name="viewport" content="width=device-width, initial-scale=1" />
17
- <link rel="preconnect" href="https://rsms.me/" />
18
- <link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
20
+ <script
21
+ dangerouslySetInnerHTML={{
22
+ __html:
23
+ "try{var t=localStorage.getItem('reprise:theme');document.documentElement.dataset.theme=t==='light'||t==='dark'?t:matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'}catch(e){}",
24
+ }}
25
+ />
26
+ <meta
27
+ name="theme-color"
28
+ content="#f8f6f2"
29
+ media="(prefers-color-scheme: light)"
30
+ />
31
+ <meta
32
+ name="theme-color"
33
+ content="#181817"
34
+ media="(prefers-color-scheme: dark)"
35
+ />
36
+ <link rel="preconnect" href="https://images.unsplash.com" />
19
37
  </head>
20
38
  <body className="min-h-screen bg-background text-foreground antialiased">
21
- <header className="sticky top-0 z-20 h-14 border-b bg-background/90 backdrop-blur">
22
- <div className="mx-auto flex h-full max-w-5xl items-center justify-between gap-4 px-5">
23
- <Link href="/" className="flex items-center gap-2 font-medium text-sm">
24
- <span className="grid size-7 place-items-center rounded-md bg-foreground text-sm font-semibold text-background">
25
- M
39
+ <a
40
+ href="#main-content"
41
+ className="fixed left-4 top-4 z-50 -translate-y-20 rounded-lg bg-foreground px-4 py-2 text-sm font-medium text-background shadow-lg transition-transform focus:translate-y-0"
42
+ >
43
+ Skip to content
44
+ </a>
45
+ <header className="sticky top-0 z-20 h-16 border-b bg-background/85 backdrop-blur-xl">
46
+ <div className="mx-auto flex h-full max-w-6xl items-center justify-between gap-3 px-5">
47
+ <Link
48
+ href="/"
49
+ translate="no"
50
+ className="flex min-h-11 items-center gap-2.5 text-sm font-semibold tracking-[-0.01em]"
51
+ >
52
+ <span className="grid size-8 place-items-center rounded-lg bg-foreground text-sm font-semibold text-background shadow-sm">
53
+ R
26
54
  </span>
27
- Pylon Market
55
+ Reprise
28
56
  </Link>
29
- <nav className="flex items-center gap-1 text-sm">
30
- <Link
31
- href="/"
32
- className="rounded-md px-3 py-1.5 text-muted-foreground transition hover:bg-muted hover:text-foreground"
33
- >
34
- Browse
35
- </Link>
36
- <Link
37
- href="/sell"
38
- className="rounded-md px-3 py-1.5 text-muted-foreground transition hover:bg-muted hover:text-foreground"
39
- >
40
- Sell
41
- </Link>
42
- <Link
43
- href="/me"
44
- className="rounded-md px-3 py-1.5 text-muted-foreground transition hover:bg-muted hover:text-foreground"
45
- >
46
- My Market
47
- </Link>
57
+ <nav
58
+ aria-label="Primary navigation"
59
+ className="flex items-center gap-0.5 text-sm sm:gap-1"
60
+ >
61
+ <Button asChild variant="ghost" size="sm" className="hidden sm:inline-flex">
62
+ <Link href="/">Browse</Link>
63
+ </Button>
64
+ <Button asChild variant="ghost" size="sm">
65
+ <Link href="/sell">Sell</Link>
66
+ </Button>
67
+ <Button asChild variant="ghost" size="sm" className="hidden md:inline-flex">
68
+ <Link href="/me">Dashboard</Link>
69
+ </Button>
70
+ <ThemeToggle />
48
71
  <AuthNav />
49
72
  </nav>
50
73
  </div>
51
74
  </header>
52
- <main className="mx-auto max-w-5xl px-5 py-6">{children}</main>
53
- <footer className="border-t py-6 text-center text-xs text-muted-foreground">
54
- Pylon Market · server-rendered listings + realtime offers from one
55
- binary
75
+ <main
76
+ id="main-content"
77
+ className="mx-auto min-h-[calc(100dvh-8rem)] max-w-6xl px-5 py-6 sm:py-8"
78
+ >
79
+ {children}
80
+ </main>
81
+ <Separator />
82
+ <footer className="px-5 py-7 text-center text-xs text-muted-foreground">
83
+ Reprise, built with Pylon. Server-rendered listings and realtime offers
84
+ from one binary.
56
85
  </footer>
57
86
  </body>
58
87
  </html>