搜尋說明文件

搜尋文件頁面與標題。

JustHold UI + Layout

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-authority for variants, following shadcn's base-nova style. Canonical pattern: packages/ui/src/components/button.tsx — import as @justhold/ui/components/button (cva variants, 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/ui or shadow primitives under components/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 forcing Button.
  • 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 with size-* (not h-*/w-*), aria-hidden="true" when decorative.
  • Meta rows (author · date · reading time) use &middot; separators and <time dateTime>; format via formatDate / formatPrice / formatPercent / formatCurrency from lib/format.ts — never format dates/numbers inline. Post metadata comes from lib/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 1DMax (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/account hub 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/account hub 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-12py-16 (md:py-14md: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 (no aspect-16/10 mock 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 sticky Blog / 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.