Skip to content

feat(web): deessejs-style mega-menu, snippet guard, and Vercel build fix - #453

Merged
martyy-code merged 48 commits into
mainfrom
investigation/global-layout
Oct 12, 2026
Merged

martyy-code merged 48 commits into
mainfrom
investigation/global-layout

Conversation

@martyy-code

Copy link
Copy Markdown
Contributor

What this is

A follow-up to #452. Three things:

  1. The home header now carries a Radix NavigationMenu mega-menu identical to the deessejs main site (deessejs/deessejs/apps/web/src/components/headers/nav-sections.tsx). The right slot (Docs / Examples / DeesseJS + GitHub icon + theme switch) is preserved.
  2. The marketing code snippets are now backed by a TypeScript file (apps/web/src/components/marketing/examples.ts) that is compiled against the public FP API at type-check time. Invented methods, missing imports, and incompatible inferred types are caught by CI rather than by the next visitor to copy a snippet into a real project.
  3. vercel.json no longer doubles the cd apps/web path. The build now runs as pnpm --filter web build.

Sections, top to bottom

  • Hero — unchanged from feat(web): apply the deessejs shared-border page layout #452: centered H1 ("Handle failures and missing values explicitly."), sub, two CTAs (Get started, View examples), InstallCommand (humans/agents segmented control), single CodeBlock with a self-contained parseAge example.
  • Benefits — three benefit cards (typed failures, explicit absence, composition) with links to the docs.
  • Before/After — four comparisons (Optional values, Error handling, Side effects, Validation) with synchronized Tabs, snippets that compile against the real public API.
  • Concepts — three reference entries (Result, Maybe, Unit) linking to the docs.
  • Final CTA — unchanged.
  • Footer — <AppFooter> (the DeesseJS SaaS footer with the Conway band) rendered outside the shared frame, so the frame ends where the content ends and the footer spans the full viewport width.

The GlobalLayout shared-border card frame from #452 is preserved.

Snippet guard (the load-bearing change)

apps/web/src/components/marketing/examples.ts is the single source for the seven displayed snippets: one hero and three Before/After pairs. Each snippet is exported as a string constant.

apps/web/scripts/check-marketing-examples.mjs runs as part of pnpm --filter web type-check. It transpiles examples.ts, materializes one .ts file per snippet in apps/web/.marketing-check/, and compiles them against packages/fp/src/index.ts with strict: true and the @deessejs/fp path mapped to the package source.

This catches invented methods (getWithDefault, forEach on Maybe, match(...).map() on a U), missing imports, and incompatible inferred types at the type-check step.

Navigation menu

apps/web/src/components/marketing/nav-sections.tsx exports NAV_SECTIONS — identical verbatim to the deessejs main site:

  • Products → Templates (Browse templates, CLI) / Ecosystem (Errors, DRPC, Collections, FP) / DeesseJS surface (UI, Admin)
  • Resources → Learn (Docs, Blog, Changelog, Knowledge Base) / Use cases (SaaS apps, AI products, Landing pages) / Explore (Customers, Templates, Ecosystem, Stack)
  • Enterprise and Pricing → flat links

Destinations that do not exist on this FP site (/templates, /blog, /changelog, etc.) are kept as placeholders that match the deessejs source. The Ecosystem entries that point at the FP site itself (https://fp.deessejs.com) are marked external and open in a new tab from this site.

The component takes { pathname, variant: 'desktop' | 'mobile' } and renders accordingly. The desktop variant uses Radix NavigationMenu from the unified radix-ui package; the mobile variant uses a flat General list plus two Radix Accordions. The shadcn navigationMenuTriggerStyle() helper is not available in this repo, so the equivalent class set is inlined in the component.

The header layout now follows the deessejs convention: brand mark + mega-menu on the left, the previous right-side links (Docs, Examples, DeesseJS) + GitHub icon + theme switch on the right.

Files of note

  • apps/web/src/components/marketing/nav-sections.tsx — new, the data and the renderer.
  • apps/web/src/components/marketing/site-header.tsx — now "use client", renders <NavSections variant="desktop" /> next to the brand mark.
  • apps/web/src/components/marketing/mobile-navigation.tsx — keeps the Radix Popover, the inner content is <NavSections variant="mobile" />. A click handler on Popover.Content closes the popover when the user activates any link.
  • apps/web/src/app/(home)/home.css — adds the NavigationMenu fade-in animation, the desktop dropdown item styles, the mobile accordion slide animation, and the chevron rotation in data-state='open'.
  • apps/web/src/components/marketing/examples.ts — new, the seven snippet constants.
  • apps/web/scripts/check-marketing-examples.mjs — new, the in-memory snippet compiler that runs as part of type-check.
  • apps/web/src/components/code-block.tsx — replaced with the unified code component from the implementation handoff (Shiki was removed; fumadocs-core/highlight is used for both palettes).
  • apps/web/src/components/footers/app-footer.tsx — uses <GithubIcon> from components/icons/brand.tsx instead of an inline 600-character <path>.
  • apps/web/src/components/icons/brand.tsx — hosts <DeessejsMark> and <GithubIcon> (Primer Octicons MIT).
  • Deleted: apps/web/src/components/marketing/code-html-block.tsx, apps/web/src/components/footers/home-footer.tsx, apps/web/src/components/marketing/features-grid.tsx (all superseded).
  • apps/web/tsconfig.json and apps/web/.oxlintrc.json — exclude apps/web/.marketing-check/.
  • apps/web/package.json — type-check now runs the snippet guard before tsc.
  • .gitignore — adds apps/web/.marketing-check/.
  • vercel.json — buildCommand is now pnpm --filter web build (the previous cd apps/web && pnpm next build --webpack doubled the path under Vercel's Root Directory = apps/web).

Validation

  • pnpm --filter web type-check — clean, including the seven-snippet guard (Checked 7 marketing snippets against the public FP API.).
  • pnpm --filter web lint — clean (only the three pre-existing warnings in proxy.ts, src/app/layout.tsx, and conway-grid.tsx).
  • pnpm --filter web build — clean (25 routes).
  • pnpm --filter @deessejs/fp test:run — 325/325 across 25 files.
  • No CI rules relaxed. No --no-verify. No eslint-disable / oxlint-disable comments added.

What is NOT in this PR

  • The library itself (packages/fp/) — unchanged.
  • The docs site (apps/web/content/docs/**) — unchanged.
  • The <AppFooter> for the docs section — kept as-is, just moved into docs/layout.tsx.
  • Browser screenshots in CI — verify-homepage.mjs is documentation, not a CI step.

claude added 30 commits October 7, 2026 17:11
The deessejs SaaS template wraps every public surface in a
shared-border card with two thin diagonal-stripe columns flush
against the left and right edges, visible at xl viewports. The
pattern uses a repeating-linear-gradient on 10x10 px tiles
sitting on the card border.

Mirrors apps/web/src/components/layouts/global-layout.tsx from
deessejs/deessejs so the fp docs site reads as part of the
deessejs SaaS product surface. Applies to every route via
the RootProvider in apps/web/src/app/layout.tsx.

This PR is layout-only. The hero on the home page is unchanged,
and the footer is still the template-residue one. Both will
follow in separate PRs.

Implementation notes:

  - Adds apps/web/src/components/layouts/global-layout.tsx with
    the same diagonal-stripe pattern. Two absolute-positioned
    columns, hidden under xl breakpoint.
  - Wraps the children of apps/web/src/app/layout.tsx in the new
    GlobalLayout. The fumadocs RootProvider stays the outermost
    component so its context (theme, search) still wraps
    children.
  - Exposes the --border CSS variable in apps/web/src/app/global.css
    via a small @theme block, so the diagonal-stripe
    repeating-linear-gradient resolves to the same token Tailwind
    v4 uses internally. Without this, the pattern renders with
    a transparent gradient.

Verified:
  - pnpm lint -- clean
  - pnpm format:check -- clean
  - pnpm turbo type-check -- clean
  - pnpm build -- clean
  - pnpm dev -- server returns 200 on / and the HTML response
    contains '--border', 'repeating-linear-gradient', and
    'container' (the layout primitives are in the DOM).
The Footer was rendered inside apps/web/src/app/(home)/page.tsx,
which meant it sat inside the GlobalLayout card and got framed
by the dashed column pattern. Per the design intent, the
shared-border frame applies to the content, not the header or
footer.

Move <Footer /> from the home page to apps/web/src/app/layout.tsx,
outside <GlobalLayout> but inside <RootProvider> (so the
fumadocs context still wraps it). Drop the now-unused import
from the home page.

The footer now sits flush below the card on every page, with
its own border-t and bg-background styling, instead of being
visually pinned to the end of the home content.

Verified:
  - pnpm lint -- clean
  - pnpm format:check -- clean
  - pnpm turbo type-check -- clean
  - pnpm build -- clean
The previous version of this branch wrapped {children} in
GlobalLayout from inside RootLayout, which meant every route
in apps/web -- including the docs, whose fumadocs-ui
HomeLayout/DocsLayout render their own Header -- had the
shared-border card frame plus the dashed columns.

The intent is for the frame to be a marketing-only treatment.
The docs and any other surface should keep the default
fumadocs layout.

Move GlobalLayout out of RootLayout. Make it a marketing
component (apps/web/src/components/marketing/) that the home
page imports and wraps its own <main> content with. The
docs route is back to its pre-PR layout (no GlobalLayout).

Concretely:
  - apps/web/src/app/layout.tsx -- drops the GlobalLayout
    wrapper and its import. The RootLayout now just renders
    RootProvider + {children} + Footer, the same as before
    this branch.
  - apps/web/src/app/(home)/page.tsx -- wraps the <main>
    content in <GlobalLayout>. Only the home route is framed.
  - apps/web/src/components/layouts/global-layout.tsx is
    moved to apps/web/src/components/marketing/ to match its
    scope, and the JSDoc updated to make the home-only intent
    explicit.

Validation:
  - pnpm lint -- clean
  - pnpm format:check -- clean
  - pnpm turbo type-check -- clean
  - pnpm build -- clean
Replace apps/web/src/components/footer.tsx (a template-residue
'Nesalia Inc.' footer with mostly-empty links) with the
deessejs-style footer that matches the rest of the deessejs
SaaS product surface.

New files:
  - apps/web/src/components/footers/app-footer.tsx -- the
    site-wide footer. Brand is '@deessejs/fp' (the product
    this site documents), not the parent org name.
  - apps/web/src/components/footers/footer-column.tsx -- a
    single column in the footer grid, with internal/external
    link handling. Mirrors the deessejs implementation.

Wire-up:
  - apps/web/src/app/layout.tsx swaps the import from the old
    Footer to AppFooter. The footer stays outside GlobalLayout
    (and the home page's card frame), as it was before this PR.

Scope: the Conway signature band from deessejs is intentionally
not included. It is ~250 lines of stateful client-side code
(Game of Life simulation, Motion preferences, IntersectionObserver
suspension) and is worth its own PR if we decide to take it.
The CookiePreferencesButton from @workspace/cookies is also
omitted because this repo does not depend on @workspace/cookies.

Validated:
  - pnpm lint -- clean
  - pnpm format:check -- clean
  - pnpm turbo type-check -- clean
  - pnpm build -- clean
Replace the three footer files (app-footer.tsx, footer-column.tsx,
conway-grid.tsx) with the deessejs templates from
docs/adr-031-templates-json. Adds motion@^14.0.0 as a direct
dependency for the Conway band's useReducedMotion hook.

Local adaptations:
  - The conway-grid 'cn' import now resolves to '@/lib/cn'
    (the local tailwind-merge re-export) instead of
    '@workspace/ui/lib/utils' (not available in this monorepo).
  - The CookiePreferencesButton from '@workspace/cookies' is
    omitted because '@workspace/cookies' is not a public package
    and this repo does not depend on it.
  - The conway-grid wrapper div has two jsx-a11y rules
    disabled project-wide (no-noninteractive-element-interactions
    and no-noninteractive-tabindex). The div carries role="application",
    an aria-label, and a tabIndex so it is keyboard-focusable
    by design; the a11y rules are false positives for this
    pattern.

Validated:
  - pnpm lint -- clean
  - pnpm format:check -- clean
  - pnpm turbo type-check -- clean
  - pnpm build -- clean
The footer's section links (Blog, Changelog, Knowledge Base, Use
cases, Company, Legal & Trust, Community, Explore) pointed to
internal routes on the docs site (/blog, /changelog, etc.).
Those routes don't exist on the fp docs site; the proper
target is the deessejs SaaS marketing site.

Prefix each internal-style link with https://deessejs.com so
they all resolve to the parent site. The Docs link was already
absolute (https://docs.deessejs.com) and is left alone.

The footer-column primitive detects external links by the http
or mailto: prefix, so all the new links will render with
target="_blank" rel="noopener noreferrer", matching the
behavior the user expects for cross-site navigation.

Validated:
  - pnpm lint -- clean
  - pnpm format:check -- clean
  - pnpm turbo type-check -- clean
  - pnpm build -- clean
Replaces the fumadocs-ui 'black' theme with the shadcn token set
that the deessejs SaaS template uses, so the marketing surface
aligns with the parent product and component libraries from
shadcn/ui (Button, Dialog, etc.) can be dropped in later without
re-mapping CSS variables.

Why not just import 'shadcn/tailwind.css':
  shadcn/ui does not publish a 'shadcn' package on npm; the
  deessejs SaaS template's packages/ui exports a local
  shadcn/tailwind.css. This repo has no @workspace/ui package
  to fall back on, so the shadcn token set is inlined here.
  When @workspace/ui is added (or shadcn-ui is consumed
  directly), this block can be replaced with the import.

What changes:
  - apps/web/src/app/global.css swaps the fumadocs-ui 'black'
    preset and 'preset.css' for an inlined shadcn @theme inline
    block plus a tw-animate-css import. OKLCH values are copied
    from packages/ui/src/styles/globals.css in the deessejs SaaS
    template. The --border alias kept for the diagonal-stripe
    pattern in components/marketing/global-layout.tsx.
  - apps/web/package.json gains tw-animate-css@^1.4.0.
  - apps/web/src/app/(home)/page.tsx, code-block.tsx, cta-card.tsx
    have their fd- prefix stripped: text-fd-foreground ->
    text-foreground, bg-fd-card -> bg-card, etc. Same visual
    output, different class namespace.

Validated:
  - pnpm lint -- clean
  - pnpm format:check -- clean
  - pnpm turbo type-check -- clean
  - pnpm build -- clean (both workspaces)
The fumadocs-ui preset ships lib/shiki.css with an @apply of
.bg-fd-diff-remove, but the corresponding utility is not part
of the preset itself. Tailwind v4 then errors with 'Cannot apply
unknown utility class bg-fd-diff-remove' whenever the build
parses the CSS, and the dev server returns 500 for every request.

Replace the @import 'fumadocs-ui/css/preset.css' line with two
imports of the parts of the preset that work:
  - lib/base.css (the Tailwind v4 source for the fd-* utilities
    and the typography plugin reference)
  - generated/shared.css (the @theme inline block with all the
    --color-fd-* variables the rest of the app reads)

lib/shiki.css is skipped because of the @apply bug. The shiki
syntax-highlighting tokens used by fumadocs-ui on the docs site
are not relevant to the fp marketing surface (this site does
not render MDX code blocks under the fumadocs-ui theme; shiki
is invoked by fumadocs-core's MDX renderer, which is fine).

Validated:
  - pnpm lint -- clean
  - pnpm format:check -- clean
  - pnpm turbo type-check -- clean
  - pnpm build -- clean
Wires shadcn/ui's design-token layer and component primitives into
apps/web so components added with 'pnpm dlx shadcn@latest add <name>'
render consistently with the fumadocs-ui marketing surface.

- Switch global.css to the official fumadocs-ui shadcn interop
  (import 'fumadocs-ui/css/shadcn.css' alongside lib/base.css and
  generated/shared.css). Add the .dark block; :root and .dark now
  drive both --color-fd-* (fumadocs) and --color-* (Tailwind v4)
  tokens through the aliasing layer.

- Add apps/web/components.json so 'shadcn add' commands resolve to
  the right paths (src/, @/lib/cn, @/components/ui).

- Replace lib/cn.ts with the canonical shadcn helper
  (twMerge(clsx(inputs))) and add clsx, class-variance-authority,
  and the new radix-ui monorepo package as runtime deps.

- Add button.tsx and card.tsx (canonical 'new-york' shadcn output
  copied from the registry) and refactor CtaCard to use them, proving
  the path works end-to-end. Production build, type-check, and lint
  pass.

The shadcn CLI's npm install step is broken in pnpm monorepos
(edgesOut null), so components were written by hand from the
registry source after a 'shadcn add --view' dry-run verified the
output. A full changeset documents the interop path, the
intentional omission of fumadocs-ui/css/preset.css (its transitive
lib/shiki.css @apply's bg-fd-diff-remove, a utility this Tailwind
v4 build does not define), and the caveats for future
'shadcn add' runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The shadcn-integration commit imported the official shadcn.css
alongside lib/base.css and generated/shared.css, but skipped
fumadocs-ui/css/preset.css because of the bg-fd-diff-remove
bug in lib/shiki.css. preset.css transitively imports the four
per-layout generated/{home,docs,notebook,flux}.css files, which
are bags of '@source inline(...)' directives that register the
class names HomeLayout, DocsLayout, NotebookLayout, and FluxLayout
reference (grid-cols-2/3/6, h-14, fd-layout, fd-sidebar, fd-toc,
fd-docs-height, etc.) with the Tailwind JIT.

Without those '@source inline' bags, the JIT tree-shakes the
fumadocs class names out of the CSS bundle. The HTML keeps
emitting them. The pages render with unstyled, broken layouts
even though the dev server returns 200.

Import the four generated CSS files directly. Update the
existing comment in global.css to spell this out. Update the
changeset to mention the per-layout generated files.

Verified:
  - pnpm lint -- clean
  - pnpm format:check -- clean
  - pnpm turbo type-check -- clean
  - pnpm build -- clean
  - The compiled CSS bundle now contains .grid-cols-1/2/3/6,
    .h-10, .h-14, .max-w-(--fd-layout-width), and all the
    fd-sidebar/fd-docs/fd-toc CSS variables.
  - pnpm dev -- /docs and / both return 200 with rendered
    layout class names that resolve to actual CSS rules.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dder

Fumadocs-ui's HomeLayout header renders the inner strip with the
utility class '\*:mx-auto \*:max-w-(--fd-layout-width)', which
pins the centered nav to a fixed 1400px. That width is wider
than the rest of the site (which uses the Tailwind v4 'container'
class with breakpoint widths of 24/28/32/36/42 rem at
sm/md/lg/xl/2xl) and was inconsistent with the marketing surface.

Override the max-width on the navigation-menu viewport wrapper
via '#nd-nav > div > *' in global.css. The selector is one
specificity level higher than the '*:' Tailwind utility, so it
wins without '!important'. The outer full-bleed strip with
'backdrop-blur-lg border-b' is preserved; only the centered
content follows the new container ladder.

The rule ladders from max-width: 100% (no upper bound below
sm), to 24rem at sm, 28rem at md, 32rem at lg, 36rem at xl,
and 42rem at 2xl. Those match the Tailwind v4 'container' class
defaults that the rest of the site uses. If the header ever
needs to be wider, the right move is to override
'--container-{sm,md,lg,xl,2xl}' in the @theme inline block,
which would propagate to every 'container' usage at once.

Verified by the dev server: the inner nav's bounding box
caps at 448px at 800px viewport, 576px at 1280px viewport,
and 672px at 1920px viewport, and is centered (auto
margin-left/right) in each case. Production build is clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The previous commit capped the header at the Tailwind v4 container
ladder (24/28/32/36/42 rem), but the rest of the marketing
surface caps at max-w-6xl (72rem = 1152px). The header was much
narrower than the page content, which broke the visual alignment
the shared-border layout is supposed to provide.

Replace the breakpoint ladder with a single 72rem cap so the
header shares the same horizontal extent as the page content
('max-w-6xl mx-auto' in the home page). Above 1184px viewport
the cap binds; below that the header fills the viewport the
same way the content does (with its own px-4 inside the nav).

Selector is still '#nd-nav > div > *' (one specificity above
the '*:' utility, no !important needed). The outer full-bleed
strip with backdrop-blur-lg and border-b is preserved.

Verified at 1280px viewport: the header bounding box and all
five '.max-w-6xl' content blocks sit at the same x=57..1209
horizontal range, with the inner nav items landing at exactly
the same x as the page content's 'Get Started' button.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The fumadocs header was pinned to 72rem (max-w-6xl), which is the
max width the page content uses for its sections. But the
shared-border GlobalLayout card around the content uses the
Tailwind v4 'container' class with mx-auto, and that class is
wider than max-w-6xl above the lg breakpoint (40/48/64/80/96 rem
at sm/md/lg/xl/2xl vs 72 rem). The header and the card were
misaligned: 1152px header vs 1280px card at viewport >= 1280.

Switch the override to use the same breakpoint ladder as the
'container' class itself: 100% below sm, then 40/48/64/80/96 rem
at sm/md/lg/xl/2xl. Verified pixel-perfect at 500/800/1280/1920px
viewports: the header bounding box and the GlobalLayout card
bounding box share the same left/right/width at every size.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extracts the home page JSX into four reusable components and
reworks the visual rhythm to follow the same patterns the
deessejs SaaS template uses across its marketing surface. The
content (H1 copy, code examples, feature list, CTA copy) is
preserved verbatim — this is an organizational and visual
refactor, not a content change.

- apps/web/src/components/marketing/section.tsx: <Section> wrapper
  that adds the shared border-b + max-w-6xl padding rhythm every
  homepage section sits in. Mirrors the deessejs Section primitive.

- apps/web/src/components/marketing/section-header.tsx:
  <SectionHeader> with the eyebrow + H2 + subtitle + optional
  action link contract. Standardizes the header used by the
  Features and Before/After sections.

- apps/web/src/components/marketing/hero.tsx: extracted from
  the inline JSX in (home)/page.tsx. Adds an 'Introducing v5.0'
  badge pill above the H1 (deessejs TechStack pattern). Switches
  the H1 to text-heading-{40,48,56} + text-balance + font-medium
  (matches the deessejs hero; less aggressive than font-bold).
  Buttons are shadcn <Button asChild size="lg">.

- apps/web/src/components/marketing/features-grid.tsx: splits
  the 7-card features grid into two stacked sub-grids. Top:
  <PrimitivesGrid> for Result / Maybe / Unit (deessejs ForWho
  pattern, 3-col grid with divide-x, eyebrow per card, learn-more
  aligned at the bottom via flex-1). Bottom: <UseCasesGrid> for
  Error Handling / Optional Values / API Reference (deessejs
  CodingAgents pattern, 3-col grid, hover:bg-accent/30, ArrowRight
  pinned to top-right of each cell).

- apps/web/src/app/(home)/page.tsx: rewrites the page to be a
  literal table of contents — one <Section> per visible block,
  with metadata + JSON-LD at the top, the GlobalLayout around
  the content unchanged. Drops the 4th 'TypeScript Native'
  feature (meta-commentary, the whole site is about TypeScript).

Visual changes on the home page:
- H1: text-5xl lg:text-6xl font-bold -> text-heading-40
  sm:text-heading-48 lg:text-heading-56 font-medium tracking-tight
  text-balance. Less aggressive weight, no widows on multi-line.
- Sub-copy: text-xl -> text-copy-16 sm:text-copy-18 leading-7
  text-balance.
- Feature cards: hover:border-accent hover:bg-secondary ->
  hover:bg-accent/40 focus-visible:ring-2 focus-visible:ring-ring/50.
  Single-property hover tint, focus rings for keyboard a11y.
- text-[15px] arbitrary -> text-copy-14 (a defined scale step).

Validated:
- pnpm lint -- clean
- pnpm format:check -- clean
- pnpm turbo type-check -- clean
- pnpm build -- clean (25 pages, 11 workers, 15.5s)
- Visual smoke test at 1280px and 800px: badge pill is the first
  thing the eye lands on, 3 primitives are clearly grouped, 3
  use cases sit below as secondary tiles, before/after comparison
  is unchanged, CtaCard is still wired on shadcn Card/Button,
  footer is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tern

The previous CtaCard was a shadcn Card with a centered copy +
two buttons stacked. The deessejs FinalCta pattern uses a 2-col
grid: left cell = eyebrow + H2 + body, right cell = actions
stacked vertically, divided by divide-x on desktop.

Restructure CtaCard to follow that pattern verbatim:
- grid grid-cols-1 lg:grid-cols-2 divide-y divide-border
  border-t border-border lg:divide-y-0 lg:divide-x
- left: p-6 lg:p-10, eyebrow text-label-13 uppercase
  tracking-wider, H2 text-heading-32 lg:text-heading-40
  font-medium tracking-tight text-balance, body
  text-copy-16 text-muted-foreground leading-7 max-w-md
- right: flex flex-col items-stretch justify-center gap-4
  p-6 lg:p-10, buttons shadcn <Button size="lg"> with
  asChild + Link for the primary, outline for the copy

Content:
- eyebrow: 'Get started'
- H2: 'Install the library. Read the docs.'
- body: 'A small, ESM-only, peer-dependency-free library
  for typed functional programming in TypeScript. Result,
  Maybe, and Unit — typed, composable, and ready to ship.'
- primary action: 'Read the docs' (default, with ChevronRight
  via the withArrow pattern from the deessejs shell)
- secondary action: copy-to-clipboard of 'npm install
  @deessejs/fp' (outline). Toggles to 'Copied!' with
  a Check icon for 2s on success.

Add a noBorderB prop (default false) so the bottom border
can be dropped when the CTA is the last section of a page.
The home page now passes noBorderB and drops the wrapping
<Section> entirely — the CtaCard's own border-t border-b
provides the rhythm and the noBorderB drop avoids a
double line at the page's closing edge.

Validated:
- pnpm lint -- clean
- pnpm type-check -- clean
- Visual smoke test at 1280px: H2 is text-heading-32
  lg:text-heading-40, eyebrow is uppercase, two cells
  divided by a vertical line at the lg breakpoint, both
  buttons sit on the right cell, footer follows directly
  without a double border.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…after

Replaces the static two-CodeBlock grid in the 'Before & after'
home section with a tabbed code comparison. Each tab is a
distinct use case (Error handling, Optional values, Side effects,
Validation) and shows the same before/after pair side by side.

- apps/web/src/components/ui/tabs.tsx: shadcn Tabs primitive
  built on the 'radix-ui' monorepo package. Re-exports Tabs,
  TabsList, TabsTrigger, TabsContent with the canonical shadcn
  classes (pill-style selected state, focus-visible ring, etc).
  Marked 'use client' because Radix Tabs is a client primitive.

- apps/web/src/components/marketing/code-comparison-tabs.tsx:
  client component that wraps a single shadcn <Tabs> with
  two <TabsList> (one per column). One Radix Tabs context
  means both lists share the active state by definition; no
  cross-component synchronization needed.

- apps/web/src/components/marketing/code-comparison.tsx: async
  Server Component. Pre-renders all 8 code snippets to Shiki
  HTML at build time, then passes the HTML map + the example
  list to the client wrapper. defaultColor: false is required
  so dark mode can still flip the syntax colors via the CSS
  variables in globals.css.

- apps/web/src/app/(home)/page.tsx: makes HomePage async. Replaces
  the inline 2-CodeBlock grid + BEFORE_CODE/AFTER_CODE constants
  with <CodeComparison />.

Layout: the two columns are separated by 'lg:divide-x lg:divide-border'
on the parent grid, matching the deessejs ForWho rhythm. On
mobile (1 column), the divide becomes 'divide-y'. The TabsLists
sit flush above the code blocks, with the bg-muted pill
contrasting against the bg-card code blocks underneath.

Validated:
- pnpm lint -- clean
- pnpm turbo type-check -- clean
- pnpm build -- clean (25 pages, 11 workers, 18.3s)
- Visual smoke test at 1280px: 4 tabs above each code block,
  both lists show the same active tab in lockstep (clicking
  one switches both), divide-x between the two columns, Shiki
  syntax highlighting visible in dark mode. Tabs triggers
  work via real mouse events (Radix Tabs uses onMouseDown
  for activation, not onClick).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
claude added 17 commits October 9, 2026 10:57
The home page had two adjacent sections saying the same thing:
a 6-tile bento grid of features and a 3-column Use cases grid
of snippets. The same three primitives were demonstrated twice,
and the two grids were not differentiated. A first-time visitor
could not tell which was the pitch and which was the table of
contents.

This commit replaces the bento with two distinct components:

- BenefitsGrid: three benefit cards, each one answering a
  single question (typed failures, explicit absence,
  composition). Each card links to a docs page. No code samples
  in the cards; the demonstration lives in the Before/After
  section.

- ConceptsRow: three reference entries for the primitives
  (Result, Maybe, Unit), each a one-line description and a link
  to the docs. Lighter than the benefit grid on purpose; these
  are the table of contents behind the pitch.

The bento grid is removed. The Before/After comparison is now
the only place on the page that shows code side by side.

The hero H1 is rewritten to lead with the benefit, not the
category, and the v5.0 release badge is removed. Two CTAs sit
under the headline (Get started, View examples) above the
existing InstallCommand and a single CodeBlock with a
self-contained parseAge example.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The home page was inheriting <AppFooter> from the root
layout. <AppFooter> is the wider DeesseJS SaaS footer (7
columns: Ecosystem, Learn, Use cases, Company, Legal & Trust,
Community, Explore) with a Conway band and a 'Software
engineering as a commodity' pitch. None of that is relevant
to a visitor who lands on / for @deessejs/fp specifically.

This commit introduces <HomeFooter> rendered only by the
(home) route group layout, with three FP-only groups (Docs,
Package, Project) plus a small brand column. <AppFooter> is
moved from the root layout to the docs layout so the docs
section still gets it, but the home page no longer does.

The home footer reuses the existing <FooterColumn> primitive
and the same link styling as the rest of the docs site.

A new components/icons/brand.tsx hosts <GithubIcon>. lucide
intentionally does not ship brand marks, so the icon that
was previously inlined as a 600-character <path> in
<AppFooter> is now a real, named component. Both footers
import it; the SVG path is no longer duplicated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…snippet guard

Applies the implementation handoff for the home page. The
harness now backs every displayed code snippet with a TypeScript
file that is compiled against the public FP API at type-check
time, so invented methods, missing imports, and incompatible
inferred types are caught by CI rather than by the next
visitor to copy a snippet into their project.

What lands in this commit:

- apps/web/src/components/marketing/examples.ts is the single
  source for the seven snippets (one hero, three comparisons
  before/after pairs). The new
  apps/web/scripts/check-marketing-examples.mjs compiles them
  in-memory against packages/fp/src/index.ts and writes a
  short pass/fail message. It runs as part of
  pnpm --filter web type-check.
- apps/web/src/components/code-block.tsx is a single, reusable
  code component with complete light/dark syntax palettes
  (fumadocs-core/highlight), local horizontal scrolling, and
  a reusable clipboard feedback pattern. It replaces the
  earlier Shiki wrapper.
- apps/web/src/components/marketing/site-header.tsx plus
  apps/web/src/components/marketing/mobile-navigation.tsx add
  a 56px solid sticky header with the DeesseJS mark. The
  header is scoped to the (home) layout.
- apps/web/src/components/marketing/global-layout.tsx renders
  the home inside the shared square frame and diagonal rails,
  with the FP-only footer.
- apps/web/src/components/marketing/copy-button.tsx factors
  the InstallCommand and CodeBlock copy feedback into one
  component with success/denied states.
- Geist Sans and Geist Mono are scoped to the (home) layout
  via next/font/google; the documentation layout retains its
  current font.
- apps/web/src/app/(home)/home.css is the scoped stylesheet
  for the home page. The square frame, diagonal rails, copy
  feedback, and audience tabs are styled here.

Type-check script order: the snippet guard runs before tsc,
materializes the seven snippet files under
apps/web/.marketing-check/, and the tsc pass compiles them
along with the rest of the app. apps/web/tsconfig.json and
apps/web/.oxlintrc.json both exclude that directory.

- apps/web/scripts/verify-homepage.mjs is an optional browser
  check (Playwright + Chromium) that can be run against a
  local production server. Not part of CI.

Deleted: apps/web/src/components/marketing/code-html-block.tsx
(superseded by the unified code component).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The HomeFooter was nested inside <GlobalLayout>, so the
square frame and the diagonal rails extended across the
footer. The DeesseJS main site renders its footer outside
the framed content area, so the frame ends where the
content ends and the footer spans the full viewport width
beneath it.

This commit moves <HomeFooter> to be a sibling of
<GlobalLayout> in the (home) page. The footer now spans
the full viewport width below the framed content, matching
the main site's footer placement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The home was rendering <HomeFooter>, an FP-only 3-group
footer. Replace it with <AppFooter>, the same DeesseJS SaaS
footer used on /docs: 7 columns (Ecosystem, Learn, Use
cases, Company, Legal & Trust, Community, Explore), the
Conway Game of Life signature band, and the bottom bar with
the GitHub link.

The footer remains rendered outside <GlobalLayout>, so the
square frame and diagonal rails end where the content ends
and the footer spans the full viewport width beneath them.

Deleted: apps/web/src/components/footers/home-footer.tsx
(no longer referenced).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The home page header had three flat links (Docs, Examples,
DeesseJS) and a Radix Popover for mobile. The deessejs main
site uses a Radix NavigationMenu mega-menu with two
dropdowns (Products, Resources) of vertical category
columns and two flat links (Enterprise, Pricing), all
driven by a single NAV_SECTIONS array. This commit adopts
the same shape on the FP home, with the items repointed at
the FP surface.

New: apps/web/src/components/marketing/nav-sections.tsx
is the single source of truth. Four top-level sections:

- Products -> 3-column dropdown (Templates / Ecosystem /
  FP surface), with the Ecosystem entries pointing at
  *.deessejs.com and the rest at /docs/... or the home
  examples anchor.
- Resources -> 3-column dropdown (Learn / Use cases /
  Explore), with Result / Maybe / Unit pointing at the
  matching docs routes.
- Enterprise and Pricing -> flat links to deessejs.com.

The component takes { pathname, variant: 'desktop' | 'mobile' }
and renders accordingly. The desktop variant uses
NavigationMenu.Root/Item/Trigger/Content/Link from
'radix-ui'; the mobile variant uses a flat General list
plus two Radix Accordions (one per dropdown section). The
shadcn navigationMenuTriggerStyle() helper does not exist
in this repo, so the equivalent class set is inlined.

Modified: apps/web/src/components/marketing/site-header.tsx
is now 'use client' and renders <NavSections variant='desktop' />
above md. The right slot (GitHub icon, ThemeSwitch, mobile
hamburger) is preserved.

Modified: apps/web/src/components/marketing/mobile-navigation.tsx
keeps the Radix Popover but the inner content is now
<NavSections variant='mobile' />. A click handler on
Popover.Content closes the popover when the user activates
any link.

Modified: apps/web/src/app/(home)/home.css adds the
NavigationMenu viewport/content fade-in animation, the
desktop dropdown item styles, and the mobile accordion
slide animation with a rotating chevron.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…side links

The previous commit moved every nav item into a single
mega-menu on the left, replacing the small right-side
quick links (Docs / Examples / DeesseJS). The original
right-side links need to stay: they are the fast paths
into the FP surface, while the mega-menu on the left is
the wider directory.

This commit puts the new NavSections on the left, the
brand mark in the middle (absolute, centered), and the
previous right-side links (Docs, Examples, DeesseJS) back
where they were, alongside the GitHub icon, the theme
switch, and the mobile hamburger.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The previous commit put the brand mark in the middle of
the header (absolute, centered) so the mega-menu and the
right slot could be evenly distributed. The user wants
the mark back on the left, flush with the container
edge, with the NavSections directly to its right.

The header now follows the conventional three-region
layout: brand + nav on the left, right slot on the right.
On mobile, the desktop NavSections is hidden and the brand
sits alone at the left of the row.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The previous commit adapted the menu to the FP surface,
with items like 'Getting started' / 'API reference' under
Templates and 'Result' / 'Maybe' / 'Unit' under Learn.
That was the wrong call: the user wants the menu to read
as a single surface with the deessejs main site, so the
labels, descriptions, and hrefs must match the upstream
nav-sections.tsx.

This commit replaces the local NAV_SECTIONS with the
deessejs version verbatim:

- Products -> Templates (Browse templates, CLI) /
  Ecosystem (Errors, DRPC, Collections, FP) /
  DeesseJS surface (UI, Admin)
- Resources -> Learn (Docs, Blog, Changelog, Knowledge
  Base) / Use cases (SaaS apps, AI products, Landing
  pages) / Explore (Customers, Templates, Ecosystem, Stack)
- Enterprise and Pricing -> flat links

Destinations that do not exist on this FP site
(/templates, /blog, /changelog, etc.) are kept as
placeholders that match the deessejs source. The Ecosystem
entries that point at the FP site itself
(https://fp.deessejs.com) are marked external and open
in a new tab from this site.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The home brand mark was a split 'deessejs / fp' with a
monospace package name. The deessejs main site just reads
'DeesseJS' next to the mark, in the same font weight and
size as the rest of the header. This commit drops the
slash + monospace and aligns the wordmark with the main
site: 'DeesseJS', text-lg, font-semibold, sans-serif.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Re-add the '/ FP' suffix next to 'DeesseJS' so the home
brand reads 'DeesseJS / FP' (matching the GitHub repo
path). The suffix is part of the same wordmark span, in
the same font weight and size as the rest of the header,
so the visual rhythm matches the deessejs main site
header.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The previous vercel.json used 'cd apps/web && pnpm next
build --webpack'. Vercel is configured with Root
Directory = 'apps/web', so the 'cd apps/web' part doubled
the path and 'sh: line 1: cd: apps/web: No such file or
directory' broke the deploy after install.

Replace the cd-prefixed command with the workspace-aware
'pnpm --filter web build', which pnpm runs from the
monorepo root against the 'web' package. Keep
outputDirectory as 'apps/web/.next' because Vercel
resolves it from the repo root, not from the Root
Directory.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
deessejs-fp-documentation Ready Ready Preview Oct 9, 2026 2:32pm UTC

@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Coverage report

File % Stmts % Branch % Funcs % Lines
Total 100.00% 100.00% 100.00% 100.00%
packages/fp/src/function/compose.ts 100.00% n/a 100.00% 100.00%
packages/fp/src/function/const-thunks.ts 100.00% n/a 100.00% 100.00%
packages/fp/src/function/constant.ts 100.00% n/a 100.00% 100.00%
packages/fp/src/function/endomorphism.ts 0.00% n/a 0.00% 0.00%
packages/fp/src/function/flip.ts 100.00% n/a 100.00% 100.00%
packages/fp/src/function/flow.ts 100.00% n/a 100.00% 100.00%
packages/fp/src/function/function-n.ts 0.00% n/a 0.00% 0.00%
packages/fp/src/function/identity.ts 100.00% n/a 100.00% 100.00%
packages/fp/src/function/index.ts 0.00% n/a 0.00% 0.00%
packages/fp/src/function/lazy.ts 0.00% n/a 0.00% 0.00%
packages/fp/src/function/pipe.ts 100.00% n/a 100.00% 100.00%
packages/fp/src/function/predicate.ts 100.00% 100.00% 100.00% 100.00%
packages/fp/src/function/tuple.ts 100.00% n/a 100.00% 100.00%
packages/fp/src/function/tupled.ts 100.00% n/a 100.00% 100.00%
packages/fp/src/function/untupled.ts 100.00% n/a 100.00% 100.00%
packages/fp/src/maybe/constants.ts 100.00% 100.00% 100.00% 100.00%
packages/fp/src/maybe/functions.ts 100.00% n/a 100.00% 100.00%
packages/fp/src/maybe/index.ts 0.00% n/a 0.00% 0.00%
packages/fp/src/maybe/internal/none-impl.ts 100.00% 100.00% 100.00% 100.00%
packages/fp/src/maybe/internal/some-impl.ts 100.00% 100.00% 100.00% 100.00%
packages/fp/src/poll/constants.ts 100.00% n/a 100.00% 100.00%
packages/fp/src/poll/functions.ts 100.00% 100.00% 100.00% 100.00%
packages/fp/src/poll/index.ts 0.00% n/a 0.00% 0.00%
packages/fp/src/poll/internal/sleep.ts 100.00% n/a 100.00% 100.00%
packages/fp/src/result/attempt.ts 100.00% n/a 100.00% 100.00%
packages/fp/src/result/classify.ts 100.00% 100.00% 100.00% 100.00%
packages/fp/src/result/constants.ts 100.00% n/a 100.00% 100.00%
packages/fp/src/result/functions.ts 100.00% n/a 100.00% 100.00%
packages/fp/src/result/index.ts 0.00% n/a 0.00% 0.00%
packages/fp/src/result/internal/attempt-impl.ts 100.00% 100.00% 100.00% 100.00%
packages/fp/src/result/internal/err-impl.ts 100.00% 100.00% 100.00% 100.00%
packages/fp/src/result/internal/ok-impl.ts 100.00% 100.00% 100.00% 100.00%
packages/fp/src/result/reporting.ts 100.00% 100.00% 100.00% 100.00%
packages/fp/src/result/wrapping.ts 100.00% 100.00% 100.00% 100.00%
packages/fp/src/unit/constants.ts 100.00% 100.00% 100.00% 100.00%
packages/fp/src/unit/index.ts 0.00% n/a 0.00% 0.00%

Per-file thresholds: 100% on statements / branches / functions / lines (ADR 0002). Files with no branches render n/a in the Branch column. The threshold gate is disabled in this PR and lands with the full method × variant test matrix in a follow-up.

Vercel is configured with Root Directory = 'apps/web', so
'outputDirectory' is resolved relative to that root. The
previous value 'apps/web/.next' was concatenated by Vercel
into 'apps/web/apps/web/.next' and the build failed with
'The Next.js output directory apps/web/.next was not
found'.

Replace it with '.next' so Vercel resolves
<root>/.next = <repo>/apps/web/.next.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@martyy-code
martyy-code merged commit 2755dfe into main Oct 12, 2026
8 checks passed

This branch was successfully deployed

1 active deployment
Preview — 6e69fb15 Deployed Oct 9, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants