JustHold UI + Layout
Copy justhold-ui-layout into your project's .agents/skills/justhold-ui-layout/ (or your Cursor skills folder). The JustHold app source stays private — these playbooks are published so you can reuse our engineering conventions.
Also follow justhold-design-tokens for colors/fonts and Shared web-service-stack for RSC defaults.
UI components
- Base UI primitives (
@base-ui/react) +class-variance-authorityfor variants, following shadcn'sbase-novastyle. Canonical pattern:packages/ui/src/components/button.tsx— import as@justhold/ui/components/button(cvavariants,cn()from@justhold/ui/lib/utils,data-slot). - Fluid Functionalism overlay (registry
@fluid, shape/springs contexts):apps/web/components/fluid/*— import as@/components/fluid/.... Chat composites (input-message,thinking-*) stay here; do not put them in@justhold/uior shadow primitives undercomponents/ui. - One-off styled links (nav CTAs, hero buttons) are hand-rolled
<Link>s with utility classes — match existing patterns (rounded-md bg-primary px-5 py-2.5 text-sm font-medium text-primary-foreground transition-opacity hover:opacity-90) instead of forcingButton. - Category/status pills:
inline-flex items-center rounded-full bg-secondary px-3 py-1 text-xs font-medium text-secondary-foreground. - Icons from
lucide-react, sized withsize-*(noth-*/w-*),aria-hidden="true"when decorative. - Meta rows (author · date · reading time) use
·separators and<time dateTime>; format viaformatDate/formatPrice/formatPercent/formatCurrencyfromlib/format.ts— never format dates/numbers inline. Post metadata comes fromlib/posts.ts. - Client components (
'use client') sparingly — only for interactivity (mobile nav, sidenote, live chart via SWR). Prefer server components by default. - Brand mark pattern:
components/logo.tsx— documented comment block + semantic Tailwind fills/strokes (fill-background,stroke-primary,fill-primary) so it tracks light/dark themes.
Product filters
Exclusive option sets (chart ranges, category types, record type, lots, history type, settings Positions/Sold) use one look: muted track, primary fill when selected, outlined items otherwise (outline turns primary on hover). Shared primitive: apps/web/components/segmented-control.tsx (SegmentedControl) — layout inline | fill, size xs | sm | md, optional count / href. Do not invent a second selected state (raised card or loose bordered chips) for the same job.
Leave as-is: holdings glance 1D–Max (gain/loss pills), locale/theme chrome, admin underline tabs, calendar amount capsules (display-only). Public write-up: /docs/engineering/ui.
Shells (pick the right one)
Public marketing (home, blog, pricing, products, etc.) — see app/[locale]/(home)/page.tsx:
<div className="flex min-h-screen flex-col">
<HomeHeader />
<main className="flex-1">{/* sections */}</main>
<HomeFooter />
</div>
Shell chrome lives under components/layout/ — header/{home,member,admin}, footer/home, nav/{member,admin}.
Member app — shell in app/[locale]/(member)/member/layout.tsx (components/layout/header/member + components/layout/nav/member). No marketing footer.
- Desktop: top navbar (home-style) with primary links + avatar account menu — no sidebar
- Mobile: top bar (logo + avatar) + fixed bottom tabs +
/member/accounthub for overflow (settings/referral/billing/public page, theme, sign out)
Admin — English-only root in app/admin/layout.tsx (admin sidebar + components/layout/header/admin + components/layout/nav/admin). No marketing shell. Same mobile chrome pattern as member:
- Desktop: collapsible icon sidebar with grouped tree nav + footer account menu
- Mobile: top bar (logo + avatar) + fixed bottom tabs (Overview / Holdings / Users / Contents / Analytics) +
/admin/accounthub for Settings, theme, member area, sign out.
Auth / errors — login is minimal (logo only). not-found / unauthorized use header + footer.
Container widths
- Marketing/listing pages:
mx-auto w-full max-w-340 px-4 md:px-6 - Article/reading content:
mx-auto max-w-2xl px-4 md:px-6 - Centered CTA copy:
mx-auto max-w-4xl px-4 md:px-6
Sections
Stack full-bleed <section> blocks, each with its own max-w-* inner wrapper and vertical rhythm py-12–py-16 (md:py-14–md:py-24 for hero). Section headings use font-sans text-2xl font-semibold tracking-tight (md:text-3xl on landing feature bands). Reserve font-heading for the brand wordmark.
Public product pages
Shared primitives under components/layout/product/ — ProductPageShell, ProductHero, ProductStage, ProductFeatures — plus the landing Cta. Match /holdings and /big-money.
- Mock / sample book (Holdings, Big Money): framed or landing mock in
ProductStage; secondary hero CTA to/…/demo. - Live interactive tool (Compound Interest, Position Sizer): omit hero CTAs; put the working widget in
ProductStage(noaspect-16/10mock frame) — the page is the product.
Landing product demos
- Multi-window / Play script → Shared skill
cursor-style-desktop-demo. Use a wider stage wrapper than the copy band. - Single framed widget → simple product chrome frame. Do not nest a desktop stage inside it.
Cards & interactive tiles
Clickable cards are a single <Link> wrapping the whole tile: rounded-xl border border-border bg-card p-6 transition-colors hover:border-primary/50, with group for child hover motion (group-hover:-translate-y-0.5 group-hover:translate-x-0.5).
Responsive grids
grid gap-6 sm:grid-cols-2 lg:grid-cols-3 for post/category grids. Use md:grid-cols-2 for two-up featured layouts.
Article layout
MDX articles use app/[locale]/(home)/blog/[slug]/layout.tsx ([counter-reset:sidenote] on <article>). Page body uses ArticleShell:
- Desktop (
xl+): left stickyBlog / category+ TOC, center lean column (max-w-2xl), right gutter for Tufte sidenotes. - Mobile: in-column
Blog / category, lean header, Cursor-style collapsible TOC under the header; sidenotes stay marker → inline (do not rewrite sidenote CSS).
ArticleHeader is lean (date · category, title, reading time). Metadata from lib/posts.ts, not the MDX body. TOC from extractToc on the R2 MDX source.