From e2d962484077bc11e93a00182affc3d00fdda5cb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 12 Aug 2026 22:34:38 +0000 Subject: [PATCH 1/2] Add docs example: dogfood flatbread with a mono, ASCII-styled Next.js site Add examples/docs, a Next.js 15 + Tailwind v4 site that reads its own content through flatbread. Markdown files under docs/ become a DocPage collection; the site reads them through the generated typed read API, so the docs eat the same dog food as any user app. - Symlink examples/docs/content/markdown/docs -> docs/ so the canonical guides stay the single source of truth. - Add frontmatter (id, title, section, order, summary, related) to the seven top-level guides so flatbread can model and sort them. - Build a monospaced, dark 'engineering paper' shell: one mono family, hairline borders, a single lime accent, and an AsciiRule component that renders box-drawing section breaks. - Add Motion.dev text reveals (MotionText staggers the hero headline word by word; MotionReveal fades section blocks in on scroll) and a shared layoutId nav marker that travels to the active page. - Wire a catch-all docs route with generateStaticParams and prev/next navigation, plus a landing page that lists every guide by section. - Pin the example to port 3001 so it can run alongside examples/nextjs, and add a root pnpm play:docs script. Change-Id: Ib3f45c8f22f3a9efbb19f6bc158d484f8529c373 --- docs/data-ownership.md | 11 + docs/edit-file-see-query-update-demo.md | 10 + docs/glossary.md | 11 + docs/json-export.md | 10 + docs/local-dev-loop.md | 10 + docs/pmf-decision-rubric.md | 11 + docs/positioning.md | 12 + examples/docs/.gitignore | 19 ++ examples/docs/app/components/AsciiRule.tsx | 37 +++ examples/docs/app/components/Doc.tsx | 110 +++++++++ examples/docs/app/components/MotionReveal.tsx | 41 ++++ examples/docs/app/components/MotionText.tsx | 61 +++++ examples/docs/app/components/Nav.tsx | 91 +++++++ examples/docs/app/components/NavWrapper.tsx | 16 ++ examples/docs/app/components/Shell.tsx | 84 +++++++ examples/docs/app/docs/[[...slug]]/page.tsx | 94 +++++++ examples/docs/app/globals.css | 229 ++++++++++++++++++ examples/docs/app/layout.tsx | 30 +++ examples/docs/app/not-found.tsx | 17 ++ examples/docs/app/page.tsx | 97 ++++++++ examples/docs/content/markdown/docs | 1 + examples/docs/flatbread.config.js | 62 +++++ examples/docs/generated/graphql.ts | 228 +++++++++++++++++ examples/docs/lib/graphql.ts | 57 +++++ examples/docs/lib/read.ts | 30 +++ examples/docs/next.config.ts | 11 + examples/docs/package.json | 28 +++ examples/docs/postcss.config.mjs | 5 + examples/docs/queries/docs.graphql | 40 +++ examples/docs/tsconfig.json | 27 +++ package.json | 1 + pnpm-lock.yaml | 171 ++++++++++++- 32 files changed, 1651 insertions(+), 11 deletions(-) create mode 100644 examples/docs/.gitignore create mode 100644 examples/docs/app/components/AsciiRule.tsx create mode 100644 examples/docs/app/components/Doc.tsx create mode 100644 examples/docs/app/components/MotionReveal.tsx create mode 100644 examples/docs/app/components/MotionText.tsx create mode 100644 examples/docs/app/components/Nav.tsx create mode 100644 examples/docs/app/components/NavWrapper.tsx create mode 100644 examples/docs/app/components/Shell.tsx create mode 100644 examples/docs/app/docs/[[...slug]]/page.tsx create mode 100644 examples/docs/app/globals.css create mode 100644 examples/docs/app/layout.tsx create mode 100644 examples/docs/app/not-found.tsx create mode 100644 examples/docs/app/page.tsx create mode 120000 examples/docs/content/markdown/docs create mode 100644 examples/docs/flatbread.config.js create mode 100644 examples/docs/generated/graphql.ts create mode 100644 examples/docs/lib/graphql.ts create mode 100644 examples/docs/lib/read.ts create mode 100644 examples/docs/next.config.ts create mode 100644 examples/docs/package.json create mode 100644 examples/docs/postcss.config.mjs create mode 100644 examples/docs/queries/docs.graphql create mode 100644 examples/docs/tsconfig.json diff --git a/docs/data-ownership.md b/docs/data-ownership.md index 785e9012..a9027670 100644 --- a/docs/data-ownership.md +++ b/docs/data-ownership.md @@ -1,3 +1,14 @@ +--- +id: data-ownership +title: Data ownership and exit story +section: concepts +order: 30 +summary: Your flat files stay the source of truth; exports and types keep them portable. +related: + - json-export + - positioning +--- + # Data ownership and exit story Flatbread's portability story starts with a simple constraint: **your flat files diff --git a/docs/edit-file-see-query-update-demo.md b/docs/edit-file-see-query-update-demo.md index 17786582..29245417 100644 --- a/docs/edit-file-see-query-update-demo.md +++ b/docs/edit-file-see-query-update-demo.md @@ -1,3 +1,13 @@ +--- +id: edit-file-see-query-update-demo +title: Edit file, see query update demo +section: guides +order: 20 +summary: A single-process demo of editing a file and watching the query update. +related: + - local-dev-loop +--- + # Edit file → see query update demo This is a single-process demo harness, not the long-running `flatbread start` diff --git a/docs/glossary.md b/docs/glossary.md index 1acc4d97..be0cf3a4 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -1,3 +1,14 @@ +--- +id: glossary +title: Flatbread glossary +section: concepts +order: 20 +summary: Definitions for collections, records, refs, and relations. +related: + - positioning + - pmf-decision-rubric +--- + # Flatbread glossary — relational content primitives This page defines words used by Flatbread. Flatbread turns files in your diff --git a/docs/json-export.md b/docs/json-export.md index 3f8b1d34..04964a35 100644 --- a/docs/json-export.md +++ b/docs/json-export.md @@ -1,3 +1,13 @@ +--- +id: json-export +title: Snapshot export +section: concepts +order: 40 +summary: Turn the content graph into portable JSON and CSV review artifacts. +related: + - data-ownership +--- + # Snapshot export Snapshot exports are part of Flatbread's data ownership story: they turn the diff --git a/docs/local-dev-loop.md b/docs/local-dev-loop.md index 71934d34..3eff3238 100644 --- a/docs/local-dev-loop.md +++ b/docs/local-dev-loop.md @@ -1,3 +1,13 @@ +--- +id: local-dev-loop +title: Local dev loop and watch boundaries +section: guides +order: 10 +summary: The four moving parts of Flatbread's watch loop and what reloads when. +related: + - edit-file-see-query-update-demo +--- + # Local dev loop and watch boundaries Flatbread's local loop has four moving parts: diff --git a/docs/pmf-decision-rubric.md b/docs/pmf-decision-rubric.md index 63fed5ba..6e638775 100644 --- a/docs/pmf-decision-rubric.md +++ b/docs/pmf-decision-rubric.md @@ -1,3 +1,14 @@ +--- +id: pmf-decision-rubric +title: Comparing Flatbread with other tools +section: reference +order: 10 +summary: Where Flatbread fits next to databases, CMSs, and file-based tools. +related: + - positioning + - glossary +--- + # Comparing Flatbread with other tools This page helps explain where Flatbread fits. It compares Flatbread with tools diff --git a/docs/positioning.md b/docs/positioning.md index db3cfffa..54411d4b 100644 --- a/docs/positioning.md +++ b/docs/positioning.md @@ -1,3 +1,15 @@ +--- +id: positioning +title: Flatbread positioning +section: concepts +order: 10 +summary: What Flatbread is, who it is for, and what it does not do. +related: + - glossary + - pmf-decision-rubric + - data-ownership +--- + # Flatbread positioning For installation and usage, see the [main README](../README.md). For diff --git a/examples/docs/.gitignore b/examples/docs/.gitignore new file mode 100644 index 00000000..8afcb30a --- /dev/null +++ b/examples/docs/.gitignore @@ -0,0 +1,19 @@ +# dependencies +/node_modules + +# next.js +/.next/ +/out/ + +# flatbread +.flatbread-codegen-cache.json + +# misc +.DS_Store + +# env files +.env* + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/examples/docs/app/components/AsciiRule.tsx b/examples/docs/app/components/AsciiRule.tsx new file mode 100644 index 00000000..e8945ae9 --- /dev/null +++ b/examples/docs/app/components/AsciiRule.tsx @@ -0,0 +1,37 @@ +import type { ReactNode } from 'react'; + +type AsciiRuleProps = { + label?: string; + align?: 'left' | 'right' | 'center'; + className?: string; + children?: ReactNode; +}; + +/** + * A horizontal rule built from box-drawing characters with an inline label, + * e.g. `──[ CONCEPTS ]──`. Rendered as flexbox + borders so it stays crisp at + * any width and copies as plain text. + */ +export function AsciiRule({ + label, + align = 'left', + className, + children, +}: AsciiRuleProps) { + return ( +
+ {label ? ( + + [ + {` ${label} `} + ] + + ) : null} + {children} +
+ ); +} diff --git a/examples/docs/app/components/Doc.tsx b/examples/docs/app/components/Doc.tsx new file mode 100644 index 00000000..9ecfb71d --- /dev/null +++ b/examples/docs/app/components/Doc.tsx @@ -0,0 +1,110 @@ +import Link from 'next/link'; +import { AsciiRule } from './AsciiRule'; +import { MotionText } from './MotionText'; +import { MotionReveal } from './MotionReveal'; + +export type DocPageView = { + id: string; + slug: string; + title: string; + section: string; + order: number; + summary: string | null; + html: string; + timeToRead: number | null; +}; + +export type RelatedLink = { + slug: string; + title: string; +}; + +type DocProps = { + doc: DocPageView; + related: RelatedLink[]; + prev: RelatedLink | null; + next: RelatedLink | null; +}; + +const SECTION_LABELS: Record = { + concepts: 'Concepts', + guides: 'Guides', + reference: 'Reference', +}; + +/** + * Renders a single doc page: a section rule, an animated title, the summary + * line, the rendered prose, related links, and prev/next navigation. + */ +export function Doc({ doc, related, prev, next }: DocProps) { + const sectionLabel = SECTION_LABELS[doc.section] ?? doc.section; + return ( +
+ + + + + + + {doc.summary ? ( +

{doc.summary}

+ ) : null} + +
+ {doc.timeToRead ? {doc.timeToRead} min read : null} + · + docs/{doc.slug}.md +
+ + +
+ + + {related.length > 0 ? ( +
+ +
    + {related.map((r) => ( +
  • + + → {r.title} + +
  • + ))} +
+
+ ) : null} + + +
+ ); +} diff --git a/examples/docs/app/components/MotionReveal.tsx b/examples/docs/app/components/MotionReveal.tsx new file mode 100644 index 00000000..f8f7f3db --- /dev/null +++ b/examples/docs/app/components/MotionReveal.tsx @@ -0,0 +1,41 @@ +'use client'; + +import { motion, type Variants } from 'motion/react'; +import type { ReactNode } from 'react'; + +const EASE = [0.22, 1, 0.36, 1] as const; + +const variants: Variants = { + hidden: { opacity: 0, y: 12 }, + visible: { + opacity: 1, + y: 0, + transition: { duration: 0.5, ease: EASE }, + }, +}; + +type MotionRevealProps = { + children: ReactNode; + className?: string; + delay?: number; +}; + +/** + * Fades and lifts its children into view once, when scrolled to. Used for + * section blocks so the page stays calm: motion is a finish on the typography, + * not a feature. + */ +export function MotionReveal({ children, className, delay = 0 }: MotionRevealProps) { + return ( + + {children} + + ); +} diff --git a/examples/docs/app/components/MotionText.tsx b/examples/docs/app/components/MotionText.tsx new file mode 100644 index 00000000..26cbaac2 --- /dev/null +++ b/examples/docs/app/components/MotionText.tsx @@ -0,0 +1,61 @@ +'use client'; + +import { motion, type Variants } from 'motion/react'; +import type { ReactNode } from 'react'; + +const EASE = [0.22, 1, 0.36, 1] as const; + +const container: Variants = { + hidden: {}, + visible: { + transition: { staggerChildren: 0.04, delayChildren: 0.05 }, + }, +}; + +const word: Variants = { + hidden: { opacity: 0, y: 8 }, + visible: { + opacity: 1, + y: 0, + transition: { duration: 0.5, ease: EASE }, + }, +}; + +type MotionTextProps = { + text: string; + as?: 'h1' | 'h2' | 'h3' | 'p' | 'span'; + className?: string; + children?: ReactNode; +}; + +/** + * Reveals text one word at a time. Used on the hero headline so it reads like a + * terminal settling into place, without a fake typewriter. Renders the words + * inline so the heading still wraps naturally. + */ +export function MotionText({ + text, + as = 'h1', + className, +}: MotionTextProps) { + const Tag = motion[as]; + const words = text.split(' '); + return ( + + {words.map((w, i) => ( + + + {w} + {i < words.length - 1 ? '\u00A0' : ''} + + + ))} + + ); +} diff --git a/examples/docs/app/components/Nav.tsx b/examples/docs/app/components/Nav.tsx new file mode 100644 index 00000000..24f4b154 --- /dev/null +++ b/examples/docs/app/components/Nav.tsx @@ -0,0 +1,91 @@ +'use client'; + +import { motion } from 'motion/react'; +import Link from 'next/link'; +import { AsciiRule } from './AsciiRule'; + +export type NavPage = { + id: string; + slug: string; + title: string; + section: string; + order: number; +}; + +type NavProps = { + pages: NavPage[]; + activeSlug?: string; +}; + +const SECTION_LABELS: Record = { + concepts: 'Concepts', + guides: 'Guides', + reference: 'Reference', +}; + +function sectionLabel(section: string): string { + return SECTION_LABELS[section] ?? section; +} + +/** + * Left rail navigation. Pages are grouped by `section` and sorted by `order`. + * The active page is marked with an accented `▸` that travels between items + * using a shared `layoutId`. + */ +export function Nav({ pages, activeSlug }: NavProps) { + const sections = new Map(); + for (const page of pages) { + const bucket = sections.get(page.section) ?? []; + bucket.push(page); + sections.set(page.section, bucket); + } + + return ( + + ); +} diff --git a/examples/docs/app/components/NavWrapper.tsx b/examples/docs/app/components/NavWrapper.tsx new file mode 100644 index 00000000..7703d4a5 --- /dev/null +++ b/examples/docs/app/components/NavWrapper.tsx @@ -0,0 +1,16 @@ +'use client'; + +import { usePathname } from 'next/navigation'; +import { Nav, type NavPage } from './Nav'; + +/** + * Reads the current pathname and derives the active doc slug so the Nav can + * mark the matching entry. Lives on the client because `usePathname` is a + * client hook; the nav data itself is fetched on the server by the Shell. + */ +export function NavWrapper({ pages }: { pages: NavPage[] }) { + const pathname = usePathname(); + const match = pathname?.match(/^\/docs\/([^/]+)/); + const activeSlug = match?.[1]; + return