Product: AutoPost — by Nestwhirr. "AutoPost" is the tool; "Nestwhirr" is the maker (vendor), and also the brand currently published-as (it appears as a selected brand in the switcher and on the rendered cards — never as the app name). Keep the two roles separate: the app is brand-neutral so the multi-company switcher holds.
The locked design direction for the AutoPost frontend. This is the why and the shape of the interface — read it before touching frontend/. It governs information architecture, visual language, the component kit, and per-screen blueprints. When a UI decision changes the direction, update this file in the same change (same rule as CONTEXT.md).
Status: Direction approved 2026-06-22, built out through Phase 14. Revamped 2026-07-22 → "Newsroom" identity + dual theme (user-confirmed): the original "minimal dark" was sound in concept but read stiff/unprofessional in execution because it relied on a system serif (Georgia/Times) both in the app and the rendered cards, had zero motion, flat low-contrast surfaces, and no light mode. The revamp keeps the editorial thesis but executes it properly — see the Newsroom section below.
Decisions that shaped it (user-confirmed 2026-06-22):
- Deliverable = written direction (this doc), build comes later.
- Fully responsive — every screen works on phone and desktop.
- Multi-company is coming soon — a brand/workspace switcher belongs in the IA now (not a later rewrite). Today it's a single static brand; the seam is what matters.
Revamp decisions (user-confirmed 2026-07-22):
- Newsroom / print-magazine lane (not the safer "editorial refined" evolution): masthead, column rules, drop-caps, footnote-scale metadata — the tool looks like a real publication.
- One real editorial type system across the app AND the backend card templates: Fraunces (display/masthead/headlines/numerals) + Newsreader (editorial serif — pull-quotes, card/post body). Geist Sans = UI, Geist Mono = metadata. This is the "studio = the artifact it produces" rule finally executed with a designed serif instead of Georgia. Self-hosted (no CDN): app via
next/font/google; backend via a base64-embedded@font-facepartial (backend/app/rendering/templates/_fonts.html, generated byassets/build_fonts_partial.py). - Dual theme now: warm-paper light + refined warm dark, one token contract, toggle in the rail footer + mobile bar + logged-out masthead, defaults to system, persisted, no-flash. The rendered cards stay dark always (fixed output identity) — only the chrome themes.
- Considered motion (Motion /
motion/react), restrained andprefers-reduced-motion-aware — shared variants infrontend/lib/motion.ts. - Landing centerpiece = a live studio walkthrough (
components/marketing/StudioWalkthrough.tsx), not a static card or MP4. See the Newsroom section.
This is not a multi-tenant SaaS dashboard, and designing it like one is the central mistake.
- A single-operator editorial cockpit. One power user, one company page (Nestwhirr) for now, run repeatedly. No customer onboarding, no billing surface. The user is an expert doing the same loop over and over.
- A content pipeline with a hard status spine:
trends → draft → approved → scheduled → published, fanning out to 5 platforms (LinkedIn, Bluesky, Mastodon, Threads, Instagram) from one editorial decision. The value is speed of the review loop + founder-voice quality + multi-platform fan-out from a single approve. - A tool that produces a specific aesthetic — the locked "minimal dark" Nestwhirr card identity (black bg, serif headline, sans body, accent line above brand strap). The app chrome must echo it.
The tool should look and behave like the artifact it produces: an editorial studio that wears the same identity as the cards it ships, not a Vercel-clone dashboard. Borrow the mental model from a newsroom CMS / code editor (single expert, keyboard-driven, status-pipeline, dense-but-calm) — not an analytics dashboard. The chrome adopts the same minimal-dark + serif-display identity as the rendered cards, so the studio feels like an extension of the brand. That single move is what makes it "intentionally designed for this product."
| Principle | Why it's true for this product |
|---|---|
| The studio looks like the artifact | The tool produces minimal-dark serif cards. The chrome adopts that identity so the app feels like the brand it publishes, not a template. This is the differentiation. |
| One operator, expert mode | No onboarding wizards. Optimize for a repeated loop done fast: density, keyboard, status-at-a-glance. Code editor / newsroom CMS, not analytics dashboard. |
| The pipeline is the product | trends → draft → approved → scheduled → published is a literal spine. The IA makes the funnel and "what needs me now" visible, not buried across disconnected pages. |
| Voice quality is a first-class surface | The whole philosophy is founder-voice-not-AI-spam. The backend already computes AI-tell violations (strip_ai_tells) — surface them. Make quality visible and editable, never hidden. |
| Multi-platform is one decision, many shapes | You approve once; it fans out to 5 platforms with different char budgets and rewrites. The UI shows "same idea, per-platform reality" side by side — that's the core review act. |
Kill the current ~800-line home wall (frontend/app/page.tsx), which mixes settings (5 "Connect X" account cards) with navigation (Trends/Drafts/Scheduled/Published link cards). Split into three zones: a persistent left rail (navigation), a brand switcher (multi-company seam, now), and a content area. Configuration moves entirely into Settings — it is not daily work.
┌────────────────────────────────────────────────────────────────┐
│ [◆ Nestwhirr ▾] ← brand/workspace switcher (top of rail) │
│ │
│ ● Today ← command center / default landing │
│ ◷ Inbox (3) ← trends, badge = unconsumed count │
│ ✎ Drafts (2) ← pipeline board, badge = needs-action │
│ ▤ Calendar ← scheduled queue, time view │
│ ✓ Published ← live history + (future) performance │
│ ─────────────── │
│ ⚙ Settings ← connections, prompts, templates, voice │
│ ⛊ Admin ← ADMIN-ONLY: access requests + user mgmt │
│ │
│ [token health pill] ← LinkedIn day-60 expiry, always visible │
│ [shivam@… ▾] │
└────────────────────────────────────────────────────────────────┘
- Left rail, not top tabs: 5 pipeline stages + settings is too many for top tabs, and a vertical rail leaves full canvas width for side-by-side platform previews (which need width). Rails scale to a multi-company switcher gracefully.
- Brand switcher lives now: CLAUDE.md mandates multi-company without rewrites. A
[◆ Nestwhirr ▾]switcher at the top of the rail makes the mental model "I'm working inside a brand," so brand #2 is a dropdown entry, not an IA redesign. Today it's one static entry — the seam is what matters. - Token-health pill is global chrome: the biggest operational failure mode is the LinkedIn day-60 token death (refresh tokens are structurally unavailable — see
CONTEXT.md/ memory). That risk follows the operator everywhere. Amber ≤10d, red ≤3d — promote the existingtokenExpiry()logic to global chrome. - Badges = "what needs me": Inbox badge = unconsumed trends; Drafts badge = posts in
draft/failedawaiting action. Navigation becomes a worklist. - Logged-out
/is a marketing landing (Phase 14), not the app: the root route gates on session — no session renders a public landing (hero · what-it-does · how-it-works = the pipeline spine · request-access form), a signed-in operator renders Today unchanged. The landing wears the same minimal-dark + serif-display identity (it IS the artifact, not a SaaS hero); AppShell renders it bare (no rail) since the rail only mounts with a session. The request form posts to the publicPOST /access-requests(honeypot-guarded) and feeds the/adminqueue./loginstays the invited-user door. Both the landing and/loginliterally render the artifact —components/marketing/PostCardMock.tsxis a web-scaled replica ofquote_card.html(black field, serif-italic pull-quote, open-quote glyph, hairline rule + uppercase brand strap) — leading the hero and the login split-panel. This is the "studio looks like the artifact it produces" rule applied to the outermost surfaces; never let either fall back to a token-less/SaaS hero. The landing hero centerpiece (2026-07-22) iscomponents/marketing/StudioWalkthrough.tsx— a live, code-based simulation of the studio, not a screenshot or MP4. A scripted synthetic cursor (an absolutely-positioned element, never the OS cursor) walks the whole status spine (trends → draft → approved → scheduled → published, so it rhymes with the "How it works" funnel directly below): ① a trend arrives in the Inbox → ② Generate → ③ the draft types itself in the founder voice and the voice-quality chip flips clean → ④ one Approve fans out to six platform chips with per-platform char meters → ⑤ Schedule queues each platform at its own staggered slot (status → Scheduled, mini-rail → Calendar) → ⑥ Publish takes them live (chips flip to emerald "Live", status → Published). It autoplays and loops; a "Try it yourself" button pauses autoplay, hides the cursor, and hands the mock controls to the visitor to walk the spine themselves. Built entirely on design tokens, so it themes light/dark for free;prefers-reduced-motionrenders a static, labelled end-state. It replaces the old staticPostCardMockin the hero. The cockpit is the real editor|preview two-pane (DESIGN §5C): left = the editable draft + a Visual source picker (Template · AI image · Upload — mirroring the real Visual panel'srender_kind) + the six-platform fan-out overview (each platform's char budget → scheduled slot → live state); right = a live per-platform social preview — the actual post mockups fromcomponents/marketing/WalkthroughPreview.tsx(a LinkedIn feed card, dark Bluesky/Mastodon/Threads/X microblog cards, an Instagram card), the same ideaPlatformPreview.tsxshows in the real cockpit, cycling through platforms with each surface's char budget and its own tighter rewrite for Bluesky/X. The chosen Visual renders inside each social card: the Template source is the actual rendered card (PostCardMock, the web replica ofquote_card.html— black field, Newsreader italic pull-quote, brand strap; always dark, fixed output identity), AI image is a diffusion-style field, Upload is a photo.PostCardMockalso still leads the/loginsplit-panel. - Admin is a role-gated rail entry (Phase 14), not a new IA pattern: it sits below Settings behind a hairline divider and renders only when
GET /mereportsrole === "admin"(the same gate hides it in the mobile account menu). Editors/viewers never see it. Inside, it reuses the Settings blueprint verbatim —?tab=sub-nav (Requests · Users), max-w-3xl column, minimal-dark tokens — so it adds a surface without adding a visual language. It's the operator console for the invite-only funnel: reviewaccess_requests, approve (→ Supabase invite) / deny, change roles, revoke.
The rendered cards have a locked identity (dark field, editorial serif headline, sans body, accent line above brand strap). The app inherits it. As of the 2026-07-22 revamp this is a Newsroom identity in two themes (see below). Tokens live in frontend/app/globals.css — semantic CSS vars on :root (light) / :root[data-theme="dark"], mapped into Tailwind v4 @theme inline so every bg-bg / text-ink-hi / border-border utility re-themes for free.
LIGHT (warm paper) DARK (refined warm)
--bg #faf8f3 newsprint #0c0b0d warm near-black
--surface-1 #f4f1e9 #131216
--surface-2 #ebe7db #1b1a1f
--surface-3 #e2ddce (wells) #232228
--border #ddd6c5 column rule #2a2930
--border-strong #c8c0ab #3b3a43
--ink-hi #1a1815 #f5f4f1
--ink-mid #56524a #a8a6a0
--ink-lo #8b8579 #6a6862
--accent #1a1815 (broadsheet ink) #e8e1d3 (parchment)
--accent-ink #faf8f3 #0c0b0d
- The accent is "the signature ink", not a hue: near-black on paper (light) / parchment on near-black (dark). Used for the CTA fill, active nav, masthead numerals, the italic hero word. Still NOT SaaS electric-blue.
- Status colors (
draft/approved/scheduled/publishing/published/failed) are tuned per theme but keep their meaning (seeglobals.css). - Rendered PNG cards stay dark in both themes — the product's output identity is fixed.
- Display: Fraunces (
font-display). Masthead, screen titles, headlines, big numerals. A warm high-contrast variable serif with optical sizing — the editorial signature. - Editorial serif: Newsreader (
font-serif). Pull-quotes, the post hook you edit, card/post body. A serif designed for on-screen reading. - UI: Geist Sans (
font-sans). Mono: Geist Mono for URNs, tokens, char counts, footnote metadata. - Both serifs are self-hosted (no CDN) — app via
next/font/google, backend card templates via the same fonts base64-embedded in_fonts.html. This is what makes "the studio looks like the artifact" literally true at the glyph level.
Flat, hairline-bordered, generous negative space; masthead + column rules; drop-caps on editorial openers (.dropcap); no drop shadows or gradients (except the avatar-initials gradient). Considered, restrained motion via Motion (motion/react) — shared variants in frontend/lib/motion.ts (reveal, stagger, pop), always prefers-reduced-motion-aware. Motion is what turns "minimal" from stiff into intentional; it is never SaaS flourish.
A small, strict kit (consistency beats variety for a solo tool):
StatusPill— pipeline state, one component, semantic color map. Single source of truth for status color (today the status/category color maps are duplicated across drafts/scheduled/published pages — consolidate).PlatformTab/PlatformBadge— per-platform identity chip with platform accent. Reused in the preview tab strip and on list rows to show fan-out targets at a glance.CharBudgetMeter— thin bar, chars-used vs the platformchar_limit, amber when over. Makes the per-platform reality (Bluesky 300 vs LinkedIn 3000) visible instead of failing at publish. Drives the existingPLATFORM_LIMITS.VoiceQualityChip— surfacesstrip_ai_tellsviolation count + whether the polish pass escalated. Green = clean, amber = N tells stripped. The differentiator made visible.CommandBar(⌘K) — generate draft, jump to a post, switch brand, refresh trends. Keyboard-first for a daily expert tool.ActionBar— the persistent draft action row (Edit / Render / Approve / Schedule / Publish now) with status-aware disable + tooltips. Exists implicitly indrafts/[id]today — formalize it.InlineToast/BannerStack— replace the ~8 ad-hoc result banners hand-rolled inpage.tsx.
Each: Goal · Layout · Key components · Rationale.
- Goal: In one glance answer "what needs me, and is the machine healthy?" then act in one click.
- Layout: three bands — (1) token-health banner only if amber/red; (2) three worklist counters (Needs review / Scheduled next / Published 7d); (3) the pipeline funnel strip (
Inbox → Draft → Approved → Scheduled → Live), a peek at freshest trends, one primary "Generate a draft" CTA. - Components: worklist counters (deep-links), pipeline funnel visualization, fresh-trends peek, primary CTA.
- Rationale: the current home makes you read 5 connection cards before working. A repeated loop should open on the work. The funnel is honest to the architecture and orients instantly. Connections move to Settings (touched ~once per 60 days).
- Goal: Triage fresh retrieval fast — judge, discard, or hand a trend straight into generation.
- Layout: dense scannable list (email-triage), not cards. Source + status filters in a left strip; rows keyboard-navigable (
j/k,econsume,ggenerate-from-this). - Components: source-coded badge, HN score, age, per-row Generate → (deep-links to the cockpit with this trend pre-selected), optimistic consume toggle.
- Rationale: trends are high-volume and disposable — "skim, judge, act" is a list, not a gallery. Per-row Generate collapses "read → remember → go to drafts → pick category → find trend again" into one click. Keeps the current filters + optimistic toggle.
- Goal: Review one generated idea, verify the founder voice, see how it lands on every target platform, fix it, approve/schedule — without leaving the screen. ~80% of operator time lives here.
- Layout — three panes (desktop):
- Editor (left): hook in serif (edit in the voice you ship), auto-resizing body, tag-chip input (the existing
DraftEditor, restyled). A live VoiceQualityChip re-runsstrip_ai_tellsclient-side as you type, flagging banned phrases inline (red underline on "delve", em-dash-as-separator, etc.). - Platform Preview (center, the visual center of gravity): the existing
PlatformPreviewtab strip promoted to center. Each tab shows the real per-platform commentary (target.commentary ?? canonical) with a CharBudgetMeter. Over-cap amber points at per-platform generation, as the code already intends. - Rail (right): status, category, target platforms, Visual render slot + template picker + carousel strip (the Phase-4 Chunk-G batch logic), failed-panel/retry/attempt-history when relevant.
- ActionBar (bottom, sticky): status-aware Edit/Render/Approve/Schedule/Publish with existing tooltips +
confirm()gate on publish-now.
- Editor (left): hook in serif (edit in the voice you ship), auto-resizing body, tag-chip input (the existing
- Rationale: today these are stacked vertical sections — you scroll between editing and previewing and lose the cause→effect link. Reviewing voice needs the edit and the platform result simultaneously — side-by-side, not scroll. Centering the preview (not the editor) reflects the real decision: "does this idea land on each surface?" The inline voice check operationalizes the project's entire reason for existing.
- Goal: See the whole editorial funnel; pull the next item that needs you.
- Layout: status-column board (columns =
PostStatus: Draft / Approved / Scheduled / Published). Each card: hook (serif, truncated), category pill, target platform badges, voice chip. Click → cockpit. Failed posts surface red with a retry affordance. - Rationale: a board is the honest representation of a status machine, and here the columns map 1:1 to the actual lifecycle enum — the data model rendered directly, not "kanban because trendy." Replaces 4 disconnected list pages with one funnel read left-to-right.
- Goal: Manage publishing cadence — see gaps, avoid clustering, reschedule by dragging.
- Layout: week/month grid; each scheduled post is a block at its slot, color = platform, hook on hover. Right rail lists "next up" chronologically (preserves current
/scheduledsort). - Components: calendar grid, drag-to-reschedule (writes through the idempotent
/scheduleendpoint withreplace_existing), per-platform color blocks, empty-slot affordance that opens generate. - Rationale: scheduling is the backbone (APScheduler, jobstore-persisted) yet has no time view. A flat list can't show "nothing goes out Thu–Sun" or "three stacked Monday morning." Brand consistency is a cadence problem; cadence is spatial-temporal. Mobile collapses to an agenda list.
- Goal: Confirm what went live, jump to the real post, (later) read performance.
- Layout: reverse-chron list; each row links out via the existing
linkedinUrlFromUrn()helper, shows all platform targets it fanned out to and each outcome. A reserved-but-empty "Performance" column header signals the deferred Phase-7 slot without faking data. - Rationale: analytics is deferred (
r_member_socialclosed in 2026 — see memory). Don't draw fake charts. Design the row so a metrics column drops in when Member Data Portability lands; show only real data now.
- Goal: Manage the plumbing: platform connections, prompt versions, visual templates, voice rules.
- Layout: sub-nav (Connections · Prompts · Templates · Voice). Connections = the existing 5 account sections (with connect/disconnect + Meta-gated
enabledhiding) moved verbatim out of home. - Rationale: touched ~once per 60 days; burying them reclaims the daily surface. Adding Prompts (the versioned
promptstable) and Templates as read/activate surfaces makes "voice tuning = DB write, not deploy" usable from the UI (today it's Alembic-only). A real workflow unlock.
The forcing function is the three-pane cockpit — it can't survive on a phone.
| Breakpoint | Behavior |
|---|---|
| Desktop ≥1280px | Left rail + full multi-pane cockpit; calendar grid; board columns side by side. Rail defaults expanded. |
| Tablet 768–1279 | Rail defaults collapsed to icons; cockpit drops to two panes (editor or preview, toggled). |
| Mobile <768 | Rail → bottom tab bar (primary nav) + a slim top app bar (brand switch, Settings, token-health dot, account/sign-out) so no rail affordance is lost. Cockpit stacks: preview first (what you judge on the go), then editor, sticky ActionBar at bottom. Calendar → agenda list. |
Rail collapse is user-controlled, not just a breakpoint. The default expanded/collapsed state follows the 1280px line, but the user pins either state via a toggle in the rail footer (or the [ shortcut), persisted to localStorage (autopost:rail-collapsed). When collapsed, hovering the icon rail peeks the full labelled rail as an opaque overlay so navigation clarity is never lost. Mobile keeps the bottom tab bar regardless (no collapse concept there).
Mobile's real job is approve-on-the-go: review the per-platform preview, tweak a word, approve or publish. Full multi-pane authoring stays a desktop act — drafts at the desk, approvals from the couch.
- The studio is the artifact — Fraunces-display + Newsreader-serif chrome, shared with the rendered cards at the glyph level, means the tool is visibly Nestwhirr. Not mistakable for a Tailwind dashboard template.
- Voice quality you can see and edit — inline AI-tell flagging turns the founding philosophy into a live editing surface.
- Side-by-side multi-platform reality — the cockpit shows one idea in 5 platform shapes with live char budgets. The fan-out is the product, so it's the visual center.
- The pipeline rendered literally — funnel on Today, board on Drafts, calendar on Schedule: the status enum made spatial, not hidden behind list pages.
Highest-leverage vertical first — it proves the whole language:
- App shell (left rail + brand switcher + global token-health pill) + design tokens (
globals.cssrewrite). - Component kit primitives (
StatusPill,PlatformBadge,CharBudgetMeter,VoiceQualityChip). - Draft Review Cockpit (§5C) — the three-pane vertical slice. Then: Today → Drafts board → Calendar → Inbox → Published → Settings.