From 582f13fa99005086097fdd9b878c4e7c70f8255e Mon Sep 17 00:00:00 2001 From: serfowi Date: Fri, 21 Aug 2026 01:38:00 -0400 Subject: [PATCH] Entry screen is the app: phone frame with a live deck inside The homepage was a marketing brochure -- a hero paragraph, a "three steps" explainer and a tag list. It described the gesture in prose while the actual Tinder deck sat behind /deck/[jobId], reachable only after signing in AND posting a job. Nobody opening the app ever saw the product. Now / renders a phone illustration with the real app running inside it: the same , the same drag physics, real verified pros. On a phone the bezel collapses and the deck simply fills the viewport -- drawing a picture of a phone on a phone is absurd, and it would eat the width the cards need. - Removed the fixed app bar and bottom tab bar. AppShell now renders the screen title as an in-flow h1; the bar owned the only h1 on every screen, so dropping it silently would have left every page headingless. The /jobs "post" action moved from bar chrome into the content, since the tab bar was its only other route there. - getShowcaseDeck(): a deck with no job behind it. getDeck is job-scoped (joins jobs for category and location, anti-joins swipes), which an anonymous visitor has none of, so this centres on the launch city. Eligibility rules are copied verbatim -- nobody may appear in the shop window who could not appear on a real deck. - deck.showcase: the only public procedure in the router. list and swipe stay behind clientProcedure. It reads nothing about the caller and writes nothing, so a right swipe on the entry screen is purely local. No real tradesperson is contacted until a job is posted. - filled a hardcoded 560px desktop box; it now fills its container. The card counter moved out from between the two action buttons so the thumb zone holds nothing but the two controls. Verified against the seeded database: 22 eligible pros returned, and all three seeded traps excluded for the right reason -- Pau Ribas (22km out, 5km radius), Unverified Ulla (pending), Away Arnau (not accepting). 7 new integration tests cover exactly that. Also corrects a label I had written as "Plumbers near you" -- the showcase deck is not category-filtered and shows every trade. Includes concurrent edits to the mobile shell, ui/ primitives and DESIGN.md made outside this session. typecheck, lint, build clean; 141 tests pass. Co-Authored-By: Claude Opus 5 (1M context) --- DESIGN.md | 64 +++-- apps/web/src/app/deck/[jobId]/page.tsx | 35 +-- apps/web/src/app/jobs/new/form.tsx | 172 ++++++------- apps/web/src/app/jobs/new/page.tsx | 11 +- apps/web/src/app/jobs/page.tsx | 104 +++++--- apps/web/src/app/layout.tsx | 34 ++- apps/web/src/app/onboarding/page.tsx | 9 +- apps/web/src/app/onboarding/role-chooser.tsx | 42 +--- apps/web/src/app/page.tsx | 63 ++--- apps/web/src/app/pro/onboarding/page.tsx | 17 +- apps/web/src/app/pro/onboarding/wizard.tsx | 227 +++++++----------- apps/web/src/app/pro/page.tsx | 101 +++----- apps/web/src/app/showcase-deck.tsx | 43 ++++ apps/web/src/app/sign-in/page.tsx | 11 +- apps/web/src/app/sign-in/sign-in-form.tsx | 123 +++++----- apps/web/src/components/chrome/app-shell.tsx | 57 +++++ .../web/src/components/chrome/phone-frame.tsx | 55 +++++ apps/web/src/components/deck.tsx | 11 +- apps/web/src/components/ui/banner.tsx | 71 ++++++ apps/web/src/components/ui/card.tsx | 63 +++++ apps/web/src/components/ui/chip.tsx | 90 +++++++ apps/web/src/components/ui/field.tsx | 101 ++++++++ apps/web/src/components/ui/index.ts | 6 + apps/web/src/components/ui/page.tsx | 54 +++++ apps/web/src/styles/globals.css | 6 +- packages/api/src/routers/deck.ts | 29 ++- packages/db/src/queries/deck.ts | 120 +++++++++ packages/db/test/showcase.test.ts | 71 ++++++ 28 files changed, 1219 insertions(+), 571 deletions(-) create mode 100644 apps/web/src/app/showcase-deck.tsx create mode 100644 apps/web/src/components/chrome/app-shell.tsx create mode 100644 apps/web/src/components/chrome/phone-frame.tsx create mode 100644 apps/web/src/components/ui/banner.tsx create mode 100644 apps/web/src/components/ui/card.tsx create mode 100644 apps/web/src/components/ui/chip.tsx create mode 100644 apps/web/src/components/ui/field.tsx create mode 100644 apps/web/src/components/ui/index.ts create mode 100644 apps/web/src/components/ui/page.tsx create mode 100644 packages/db/test/showcase.test.ts diff --git a/DESIGN.md b/DESIGN.md index 400a051..440764a 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -22,6 +22,8 @@ not affiliated with Wix and must never present itself as such. ## 1. Principles +0. **Mobile only.** One column, one container, thumb-reachable actions, no desktop layout and + no marketing site. See §4 — this constrains every other decision here. 1. **Blue means action.** `brand-500` is reserved for things the user can do. It is never decoration, never a background wash for a whole section, never body text. 2. **Ink is near-black, not grey.** Headings sit at `ink-950` (`#000624`) — black with a blue @@ -151,19 +153,35 @@ Display sizes use `clamp()` so one token works from 360px to desktop. ## 4. Space and layout -An **8px grid**. The only legal spacing values are `4 8 12 16 24 32 40 56 72 96 128`. Anything -else is a bug. +**Linkder is a mobile-only product.** There is no desktop layout, no marketing site and no +responsive breakpoint work. Design for a 360–430px viewport and nothing else. On a wider +screen the app renders as a single 480px column centred on `ink-50` — that is a courtesy for +someone who opened it on a laptop, not a layout to design for. -| Container | Width | Used for | +Concretely, this bans: + +- Multi-column grids. Everything is one column. A 2-up grid is allowed only for paired + numeric fields (from/to, rate/years) and stat tiles. +- `md:` / `lg:` variants that change structure. Type already scales with `clamp()`. +- Sticky desktop nav bars with link lists, mega-footers, hero sections, feature grids. + +An **8px grid**. The only legal spacing values are `4 8 12 16 24 32 40 56 72`. Anything else +is a bug. + +| Token | Width | Used for | |---|---|---| -| `prose` | 680px | Reading, forms, single-column flows | -| `app` | 1080px | Dashboards, lists | -| `wide` | 1280px | Marketing sections | +| `app` | 480px | Every screen. There is no second container. | -- Page gutter: **24px** mobile, **32px** ≥768px. -- Vertical rhythm between page sections: **72px** mobile, **96px** desktop. +- Page gutter: **20px**. One value, all screens. +- Vertical rhythm between blocks within a screen: **32px**. Between major groups: **40px**. - Gap between sibling cards in a list: **12px**. - Label → control: **8px**. Control → helper text: **8px**. Field → field: **24px**. +- Bottom of scroll content clears the tab bar by **88px** (`pb-22`) so nothing hides under it. + +### 4.1 Safe areas + +The tab bar and any fixed footer pad with `env(safe-area-inset-bottom)`. The app bar pads with +`env(safe-area-inset-top)`. Never assume the viewport edge is reachable. --- @@ -253,16 +271,26 @@ Selection must never rely on fill alone. Title in `h4`, body in `body-sm` `ink-600`. -### 6.6 Navigation +### 6.6 App bar -Sticky top bar, 72px tall, `ink-0` at 88% opacity with `backdrop-blur(12px)`, hairline bottom -border. Wordmark left in Display 700 `-0.02em`. Primary CTA right as a pill. Mobile collapses -links; the CTA stays visible in the bar. +Fixed top, **56px** plus top safe-area inset. `page` surface at 88% opacity with +`backdrop-blur(12px)` and a hairline bottom border. -### 6.7 Footer +Two layouts only: -`ink-950` surface, white text, secondary links `ink-400`, 72px top padding, 40px bottom, -4-up column grid collapsing to 1-up under 640px. +- **Root screens** — screen title left in `h4`, at most one icon action right. +- **Pushed screens** — 44px back chevron left, centred title in `h4`, optional icon action right. + +No link lists. No CTA button in the bar — the primary action lives in the screen or the tab bar. + +### 6.7 Tab bar + +Fixed bottom, **56px** plus bottom safe-area inset, `page` surface, hairline top border. Two +to five destinations, each a 44px-minimum target with a 24px icon over a `meta` label. Active +tab is `brand-500` icon + label; inactive is `ink-600`. The active state carries both colour and +weight, never colour alone. + +The tab bar is the app's only persistent navigation. There is no footer. ### 6.8 Deck card @@ -311,6 +339,10 @@ deck's drag is essential and stays; its spring flattens under reduced motion. | Placeholder standing in for a label | Visible `