From d45f56073bd91785827dbc2b02c74e52c73538cb Mon Sep 17 00:00:00 2001 From: Peter Koomen Date: Wed, 24 Dec 2025 02:25:54 -0800 Subject: [PATCH 01/37] update command docs --- pages/index.mdx | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/pages/index.mdx b/pages/index.mdx index 49f6f6e..1ebd9f5 100644 --- a/pages/index.mdx +++ b/pages/index.mdx @@ -107,17 +107,14 @@ Scratch compiles Javascript (.js), Typescript (.ts), JSX (.jsx), TSX (.tsx), Mar ```bash # Create a new project -scratch create my-site # interactive -scratch create --minimal # omit src/ and page examples -scratch create --full # include src/, examples, and package.json +scratch create [path] # create project at path (default: current directory) # Start dev server with hot module reloading scratch dev # Build for production scratch build -scratch build --ssg false # disable static site generation -scratch build --development # unminified, with source maps +scratch build --no-ssg # disable static site generation # Preview production build locally scratch preview @@ -126,9 +123,9 @@ scratch preview scratch clean # Revert a file to its template version -scratch revert [file] # Revert a file to its template version -scratch revert [dir] # Revert files in to their template version -scratch revert --list # list available template files +scratch revert [file] # revert a file to its template version +scratch revert [file]--force # overwrite without confirmation +scratch revert --list # list available template files # Update scratch to latest version scratch update From f88d292309f09fe4866618221540566b72ea04e2 Mon Sep 17 00:00:00 2001 From: Peter Koomen Date: Wed, 24 Dec 2025 02:32:26 -0800 Subject: [PATCH 02/37] update scratch project file section --- pages/index.mdx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pages/index.mdx b/pages/index.mdx index 1ebd9f5..ba14e96 100644 --- a/pages/index.mdx +++ b/pages/index.mdx @@ -79,18 +79,19 @@ A simple Scratch project (created with `scratch create`) looks like this: Use `scratch build` to compile this project into a [static website](https://scratch.dev/template). -Borrowing heavily from [Tailwind Typography](https://github.com/tailwindlabs/tailwindcss-typography), Scratch uses default styles and Markdown components to render your prose with a clean aesthetic. Code blocks use syntax highlighting by [Shiki](https://shiki.style/). +Scratch uses [Tailwind Typography](https://github.com/tailwindlabs/tailwindcss-typography), to render your prose with a clean aesthetic. Code blocks use syntax highlighting by [Shiki](https://shiki.style/). -You can change styles and customize the page wrapper component by including the `src/` directory when you run `scratch create`: +You can change styles and customize the page wrapper component by modifying the files in the `src/` directory: mysite/ ├── pages/ ├── public/ - └── src/ - ├── markdown/ - ├── PageWrapper.tsx - └── tailwind.css + ├── src/ + │ ├── markdown/ + │ ├── PageWrapper.tsx + │ └── tailwind.css + └── package.json Component files and js/ts libraries can live anywhere in `pages/` and `src/`. They are auto-detected by Scratch and don't need to be explicitly imported in your .mdx files as long as the filename matches the component name. @@ -115,6 +116,7 @@ scratch dev # Build for production scratch build scratch build --no-ssg # disable static site generation +scratch build --development # unminified, with source maps # Preview production build locally scratch preview From d8174a491886705541eb4e4780e6692902d25d73 Mon Sep 17 00:00:00 2001 From: Peter Koomen Date: Wed, 24 Dec 2025 02:32:56 -0800 Subject: [PATCH 03/37] fix commands typo --- pages/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/index.mdx b/pages/index.mdx index ba14e96..0428709 100644 --- a/pages/index.mdx +++ b/pages/index.mdx @@ -126,7 +126,7 @@ scratch clean # Revert a file to its template version scratch revert [file] # revert a file to its template version -scratch revert [file]--force # overwrite without confirmation +scratch revert [file] --force # overwrite without confirmation scratch revert --list # list available template files # Update scratch to latest version From f8e0ab02ddda174bf3551ff8509626533610b289 Mon Sep 17 00:00:00 2001 From: Peter Koomen Date: Wed, 24 Dec 2025 02:34:40 -0800 Subject: [PATCH 04/37] swap flag and [file] in commands section --- pages/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/index.mdx b/pages/index.mdx index 0428709..88422df 100644 --- a/pages/index.mdx +++ b/pages/index.mdx @@ -126,7 +126,7 @@ scratch clean # Revert a file to its template version scratch revert [file] # revert a file to its template version -scratch revert [file] --force # overwrite without confirmation +scratch revert --force [file] # overwrite without confirmation scratch revert --list # list available template files # Update scratch to latest version From c61a74905d98d8da12fb20dcf1462e44fec1c7d1 Mon Sep 17 00:00:00 2001 From: Peter Koomen Date: Wed, 24 Dec 2025 11:11:11 -0800 Subject: [PATCH 05/37] update files component --- pages/Files.tsx | 265 ++++++++++++++++++++++++++++++++---------------- pages/index.mdx | 87 ++++++++-------- 2 files changed, 223 insertions(+), 129 deletions(-) diff --git a/pages/Files.tsx b/pages/Files.tsx index 892dbf4..659e320 100644 --- a/pages/Files.tsx +++ b/pages/Files.tsx @@ -1,115 +1,182 @@ -import React from "react"; +import React, { useState } from "react"; -interface FileNode { +// Tree node structure from parsing +interface TreeNode { + id: string; + name: string; + children: TreeNode[]; + startCollapsed: boolean; +} + +// Flattened node for rendering +interface RenderNode { + id: string; name: string; depth: number; isLast: boolean; parentIsLast: boolean[]; + isFolder: boolean; + hasChildren: boolean; } -function parseTree(text: string): FileNode[] { - const lines = text.trim().split("\n"); - const nodes: FileNode[] = []; - - // Stack of { depth, isLast } for each open folder level - const stack: { depth: number; isLast: boolean }[] = []; - - for (const line of lines) { - if (!line.trim()) continue; +function parseTree(text: string): TreeNode[] { + const lines = text.split("\n").filter((line) => line.trim().length > 0); + if (lines.length === 0) return []; - // Find where the actual filename starts - const nameMatch = line.match(/[^\s│|├└─]/); - if (!nameMatch) continue; + // Parse lines - support both whitespace and dot-prefix for indentation + const items = lines.map((line, index) => { + const dotMatch = line.match(/^(\.+)/); + let name: string; + let indent: number; - const nameStart = nameMatch.index!; - const prefix = line.slice(0, nameStart); - const name = line.slice(nameStart).trim(); + if (dotMatch) { + name = line.slice(dotMatch[1].length); + indent = dotMatch[1].length; + } else { + name = line.trim(); + indent = line.length - line.trimStart().length; + } - if (!name) continue; + // Check for (collapsed) suffix + const collapsedMatch = name.match(/\s*\(collapsed\)\s*$/i); + const startCollapsed = !!collapsedMatch; + if (collapsedMatch) { + name = name.slice(0, collapsedMatch.index).trim(); + } - const isLast = prefix.includes("└"); - const hasBranch = prefix.includes("├") || prefix.includes("└"); + return { name, indent, startCollapsed, lineIndex: index }; + }); - // Count │ to know how many non-last ancestors - const pipeCount = (prefix.match(/[│|]/g) || []).length; + // Normalize indents by subtracting the minimum + const baseIndent = Math.min(...items.map((item) => item.indent)); + items.forEach((item) => (item.indent -= baseIndent)); - let depth: number; + // Build tree using a stack + const roots: TreeNode[] = []; + const stack: { node: TreeNode; indent: number }[] = []; - if (!hasBranch && prefix.length === 0) { - // Root item - depth = 0; - stack.length = 0; - } else { - // Pop stack until we find the right parent level - // pipeCount tells us how many ancestors are still "open" (not last) - while (stack.length > 0) { - const openCount = stack.filter((s) => !s.isLast).length; - if (openCount <= pipeCount) break; - stack.pop(); - } + for (const { name, indent, startCollapsed, lineIndex } of items) { + const node: TreeNode = { + id: `node-${lineIndex}`, + name, + children: [], + startCollapsed, + }; - depth = stack.length + 1; + // Pop stack until we find the parent (item with smaller indent) + while (stack.length > 0 && stack[stack.length - 1].indent >= indent) { + stack.pop(); } - // Update stack - if (isLast) { - // Pop items at this depth or deeper - while (stack.length >= depth) { - stack.pop(); - } + if (stack.length === 0) { + roots.push(node); + } else { + stack[stack.length - 1].node.children.push(node); } - // Build parentIsLast array from stack - const parentIsLast = stack.map((s) => s.isLast); + stack.push({ node, indent }); + } - const isFolder = name.endsWith("/") || !name.includes("."); + return roots; +} - nodes.push({ - name, - depth, - isLast, - parentIsLast, +function flattenTree( + roots: TreeNode[], + collapsedIds: Set +): RenderNode[] { + const result: RenderNode[] = []; + + function traverse( + nodes: TreeNode[], + depth: number, + parentIsLast: boolean[] + ): void { + nodes.forEach((node, index) => { + const isLast = index === nodes.length - 1; + const isFolder = node.name.endsWith("/") || !node.name.includes("."); + const hasChildren = node.children.length > 0; + + result.push({ + id: node.id, + name: node.name, + depth, + isLast, + parentIsLast: [...parentIsLast], + isFolder, + hasChildren, + }); + + // Only traverse children if not collapsed + if (hasChildren && !collapsedIds.has(node.id)) { + traverse(node.children, depth + 1, [...parentIsLast, isLast]); + } }); + } + + traverse(roots, 0, []); + return result; +} + +function getInitialCollapsed(roots: TreeNode[]): Set { + const collapsed = new Set(); - // If this is a folder, push to stack for potential children - if (isFolder) { - stack.push({ depth, isLast }); + function traverse(nodes: TreeNode[]): void { + for (const node of nodes) { + if (node.startCollapsed) { + collapsed.add(node.id); + } + traverse(node.children); } } - return nodes; + traverse(roots); + return collapsed; } -function FileRow({ node }: { node: FileNode }) { - const isFolder = node.name.endsWith("/") || !node.name.includes("."); +interface FileRowProps { + node: RenderNode; + isCollapsed: boolean; + onToggle: () => void; +} - return ( -
- {/* Render vertical lines for each parent level */} - {node.parentIsLast.map((parentLast, i) => ( -
- {!parentLast && ( -
- )} -
- ))} +function FileRow({ node, isCollapsed, onToggle }: FileRowProps) { + const isClickable = node.isFolder && node.hasChildren; - {/* Render the branch for this node */} - {node.depth > 0 && ( -
- {/* Vertical line (full height if not last, half if last) */} -
- {/* Horizontal line */} -
-
+ return ( +
+ {/* Caret for folders with children */} + {isClickable ? ( + + ) : ( +
)} - {/* File/folder name */} - + {node.name}
@@ -127,7 +194,6 @@ function extractText(children: React.ReactNode): string { if (typeof children === "object" && "props" in children) { const el = children as React.ReactElement; - // If it's a

tag or similar, extract and add newline if (el.type === "p" || el.type === "br") { return extractText(el.props.children) + "\n"; } @@ -137,14 +203,41 @@ function extractText(children: React.ReactNode): string { return ""; } -export default function Files({ children }: { children: React.ReactNode }) { - const text = extractText(children); - const nodes = parseTree(text); +interface FilesProps { + content?: string; + children?: React.ReactNode; +} + +export default function Files({ content, children }: FilesProps) { + const text = content ?? extractText(children); + const [tree] = useState(() => parseTree(text)); + const [collapsedIds, setCollapsedIds] = useState(() => + getInitialCollapsed(tree) + ); + + const nodes = flattenTree(tree, collapsedIds); + + const toggleCollapse = (id: string) => { + setCollapsedIds((prev) => { + const next = new Set(prev); + if (next.has(id)) { + next.delete(id); + } else { + next.add(id); + } + return next; + }); + }; return (

- {nodes.map((node, i) => ( - + {nodes.map((node) => ( + toggleCollapse(node.id)} + /> ))}
); diff --git a/pages/index.mdx b/pages/index.mdx index 88422df..4265950 100644 --- a/pages/index.mdx +++ b/pages/index.mdx @@ -28,18 +28,17 @@ scratch dev ## What can you do with Scratch? -Scratch was designed for collaborative writing with coding agents like [Claude Code](https://www.claude.com/product/claude-code). Use your favorite editor to write in [Markdown](https://daringfireball.net/projects/markdown/) and embed React components when it's easier to express yourself with code. +Scratch lets you write in Markdown and embed interactive React components, like this counter: -Scratch supports Github-flavored Markdown features like tables and todolists: + -| Feature | Supported? | -|---------|-----------| -| Compiles `.md` `.mdx` `.tsx` `.ts` `.jsx` `.js` | ✅ | -| Dev server with HMR | ✅ | -| Tailwind CSS styling | ✅ | -| Code syntax highlighting | ✅ | +Scratch was designed for collaborative writing with coding agents like [Claude Code](https://www.claude.com/product/claude-code). Use your favorite editor to write in Markdown, and ask a coding agent for help when it's easier to express yourself with code. + +You can use React components to style text or embed fully working demos in your product specs: + + -Code blocks use syntax highlighting by [Shiki](https://shiki.style/): +Scratch uses [Tailwind Typography](https://github.com/tailwindlabs/tailwindcss-typography), to render your prose with a clean aesthetic. Code blocks use syntax highlighting by [Shiki](https://shiki.style/). ```python def greet(name: str) -> str: @@ -48,51 +47,53 @@ def greet(name: str) -> str: print(greet("World")) ``` -You can use React components to style text or embed fully working demos in your product specs: +Scratch also supports Github-flavored Markdown features like checklists and tables: - +| Feature | Supported? | +|---------|-----------| +| Compiles Markdown, TS, JS & CSS | ✅ | +| Dev server with HMR | ✅ | +| Tailwind CSS styling | ✅ | +| Code syntax highlighting | ✅ | -In fact, coding agents have gotten so good that if you can describe it, you can add it to a document: +Unlike traditional word processors, Scratch makes it easy to express any idea. If you can describe it to a coding agent, you can add it to your document: + +```text +Make me a component that looks like a TV screen with a bouncing DVD logo. Count the number of times the logo hits a corner and display that below the TV. +``` +Collaborating with AI makes writing more fun. Scratch makes that easy. + ## No Boilerplate Scratch uses an opinionated project structure and requires **no boilerplate or configuration**: just create a project, run the dev server with `scratch dev`, and start writing. A simple Scratch project (created with `scratch create`) looks like this: - - mysite/ - ├── pages/ - │ ├── index.mdx - │ ├── Counter.tsx - │ └── examples/ - │ ├── index.md - │ ├── markdown.md - │ ├── todolist-spec.mdx - │ └── TodoList.tsx - └── public/ - ├── logo.png - └── favicon.svg - - -Use `scratch build` to compile this project into a [static website](https://scratch.dev/template). - -Scratch uses [Tailwind Typography](https://github.com/tailwindlabs/tailwindcss-typography), to render your prose with a clean aesthetic. Code blocks use syntax highlighting by [Shiki](https://shiki.style/). - -You can change styles and customize the page wrapper component by modifying the files in the `src/` directory: - - - mysite/ - ├── pages/ - ├── public/ - ├── src/ - │ ├── markdown/ - │ ├── PageWrapper.tsx - │ └── tailwind.css - └── package.json - + + +Use `scratch build` to compile this project into a static website, like this one. You can change styles and customize the page wrapper component by modifying the files in the `src/` directory: + + Component files and js/ts libraries can live anywhere in `pages/` and `src/`. They are auto-detected by Scratch and don't need to be explicitly imported in your .mdx files as long as the filename matches the component name. From ee156860151a35cf4e2476e0ef8498f66e33d54b Mon Sep 17 00:00:00 2001 From: Peter Koomen Date: Wed, 24 Dec 2025 11:19:01 -0800 Subject: [PATCH 06/37] more files tweaks --- pages/Files.tsx | 31 +++++++++++++++++++++++-------- pages/index.mdx | 2 +- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/pages/Files.tsx b/pages/Files.tsx index 659e320..4c158b1 100644 --- a/pages/Files.tsx +++ b/pages/Files.tsx @@ -4,6 +4,7 @@ import React, { useState } from "react"; interface TreeNode { id: string; name: string; + comment?: string; children: TreeNode[]; startCollapsed: boolean; } @@ -12,6 +13,7 @@ interface TreeNode { interface RenderNode { id: string; name: string; + comment?: string; depth: number; isLast: boolean; parentIsLast: boolean[]; @@ -28,6 +30,7 @@ function parseTree(text: string): TreeNode[] { const dotMatch = line.match(/^(\.+)/); let name: string; let indent: number; + let comment: string | undefined; if (dotMatch) { name = line.slice(dotMatch[1].length); @@ -37,6 +40,13 @@ function parseTree(text: string): TreeNode[] { indent = line.length - line.trimStart().length; } + // Check for # comment + const commentMatch = name.match(/\s*#\s*(.*)$/); + if (commentMatch) { + comment = commentMatch[1].trim(); + name = name.slice(0, commentMatch.index).trim(); + } + // Check for (collapsed) suffix const collapsedMatch = name.match(/\s*\(collapsed\)\s*$/i); const startCollapsed = !!collapsedMatch; @@ -44,7 +54,7 @@ function parseTree(text: string): TreeNode[] { name = name.slice(0, collapsedMatch.index).trim(); } - return { name, indent, startCollapsed, lineIndex: index }; + return { name, indent, comment, startCollapsed, lineIndex: index }; }); // Normalize indents by subtracting the minimum @@ -55,10 +65,11 @@ function parseTree(text: string): TreeNode[] { const roots: TreeNode[] = []; const stack: { node: TreeNode; indent: number }[] = []; - for (const { name, indent, startCollapsed, lineIndex } of items) { + for (const { name, indent, comment, startCollapsed, lineIndex } of items) { const node: TreeNode = { id: `node-${lineIndex}`, name, + comment, children: [], startCollapsed, }; @@ -93,12 +104,13 @@ function flattenTree( ): void { nodes.forEach((node, index) => { const isLast = index === nodes.length - 1; - const isFolder = node.name.endsWith("/") || !node.name.includes("."); + const isFolder = node.name.endsWith("/"); const hasChildren = node.children.length > 0; result.push({ id: node.id, name: node.name, + comment: node.comment, depth, isLast, parentIsLast: [...parentIsLast], @@ -140,14 +152,14 @@ interface FileRowProps { } function FileRow({ node, isCollapsed, onToggle }: FileRowProps) { - const isClickable = node.isFolder && node.hasChildren; + const isClickable = node.isFolder; return (
- {/* Caret for folders with children */} + {/* Caret for folders */} {isClickable ? (
); } diff --git a/pages/index.mdx b/pages/index.mdx index 4265950..e5bb060 100644 --- a/pages/index.mdx +++ b/pages/index.mdx @@ -74,7 +74,7 @@ A simple Scratch project (created with `scratch create`) looks like this: Date: Wed, 24 Dec 2025 11:46:51 -0800 Subject: [PATCH 07/37] more index.mdx and Files cleanup --- bun.lock | 184 ++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 16 +++++ pages/Files.tsx | 90 ++++++++++++----------- pages/index.mdx | 62 +++++++++------- 4 files changed, 286 insertions(+), 66 deletions(-) create mode 100644 bun.lock create mode 100644 package.json diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..18a0122 --- /dev/null +++ b/bun.lock @@ -0,0 +1,184 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "scratch.dev", + "dependencies": { + "@mdx-js/react": "latest", + "@tailwindcss/cli": "latest", + "@tailwindcss/typography": "latest", + "react": "latest", + "react-dom": "latest", + "tailwindcss": "latest", + }, + }, + }, + "packages": { + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="], + + "@parcel/watcher": ["@parcel/watcher@2.5.1", "", { "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", "micromatch": "^4.0.5", "node-addon-api": "^7.0.0" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-freebsd-x64": "2.5.1", "@parcel/watcher-linux-arm-glibc": "2.5.1", "@parcel/watcher-linux-arm-musl": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", "@parcel/watcher-linux-arm64-musl": "2.5.1", "@parcel/watcher-linux-x64-glibc": "2.5.1", "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-ia32": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1" } }, "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg=="], + + "@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.1", "", { "os": "android", "cpu": "arm64" }, "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA=="], + + "@parcel/watcher-darwin-arm64": ["@parcel/watcher-darwin-arm64@2.5.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw=="], + + "@parcel/watcher-darwin-x64": ["@parcel/watcher-darwin-x64@2.5.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg=="], + + "@parcel/watcher-freebsd-x64": ["@parcel/watcher-freebsd-x64@2.5.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ=="], + + "@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.5.1", "", { "os": "linux", "cpu": "arm" }, "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA=="], + + "@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.5.1", "", { "os": "linux", "cpu": "arm" }, "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q=="], + + "@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w=="], + + "@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg=="], + + "@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A=="], + + "@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg=="], + + "@parcel/watcher-win32-arm64": ["@parcel/watcher-win32-arm64@2.5.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw=="], + + "@parcel/watcher-win32-ia32": ["@parcel/watcher-win32-ia32@2.5.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ=="], + + "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="], + + "@tailwindcss/cli": ["@tailwindcss/cli@4.1.18", "", { "dependencies": { "@parcel/watcher": "^2.5.1", "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "enhanced-resolve": "^5.18.3", "mri": "^1.2.0", "picocolors": "^1.1.1", "tailwindcss": "4.1.18" }, "bin": { "tailwindcss": "dist/index.mjs" } }, "sha512-sMZ+lZbDyxwjD2E0L7oRUjJ01Ffjtme5OtjvvnC+cV4CEDcbqzbp25TCpxHj6kWLU9+DlqJOiNgSOgctC2aZmg=="], + + "@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="], + + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="], + + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.18", "", { "os": "android", "cpu": "arm64" }, "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q=="], + + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A=="], + + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw=="], + + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.18", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA=="], + + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18", "", { "os": "linux", "cpu": "arm" }, "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA=="], + + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw=="], + + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg=="], + + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g=="], + + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ=="], + + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.18", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.0", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA=="], + + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA=="], + + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.18", "", { "os": "win32", "cpu": "x64" }, "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q=="], + + "@tailwindcss/typography": ["@tailwindcss/typography@0.5.19", "", { "dependencies": { "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg=="], + + "@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="], + + "@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="], + + "enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + + "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], + + "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="], + + "react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], + + "react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], + + "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.0", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA=="], + + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + + "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "lightningcss/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..1194b55 --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "scratch.dev", + "private": true, + "scripts": { + "dev": "scratch dev", + "build": "scratch build" + }, + "dependencies": { + "react": "latest", + "react-dom": "latest", + "@mdx-js/react": "latest", + "tailwindcss": "latest", + "@tailwindcss/cli": "latest", + "@tailwindcss/typography": "latest" + } +} diff --git a/pages/Files.tsx b/pages/Files.tsx index 4c158b1..630cf98 100644 --- a/pages/Files.tsx +++ b/pages/Files.tsx @@ -25,16 +25,16 @@ function parseTree(text: string): TreeNode[] { const lines = text.split("\n").filter((line) => line.trim().length > 0); if (lines.length === 0) return []; - // Parse lines - support both whitespace and dot-prefix for indentation + // Parse lines - support both whitespace and dash-prefix for indentation const items = lines.map((line, index) => { - const dotMatch = line.match(/^(\.+)/); + const dashMatch = line.match(/^(-+)/); let name: string; let indent: number; let comment: string | undefined; - if (dotMatch) { - name = line.slice(dotMatch[1].length); - indent = dotMatch[1].length; + if (dashMatch) { + name = line.slice(dashMatch[1].length); + indent = dashMatch[1].length; } else { name = line.trim(); indent = line.length - line.trimStart().length; @@ -153,46 +153,56 @@ interface FileRowProps { function FileRow({ node, isCollapsed, onToggle }: FileRowProps) { const isClickable = node.isFolder; + const isDotfile = node.name.startsWith("."); return ( -
- {/* Caret for folders */} - {isClickable ? ( - - ) : ( -
- )} + + + + + ) : ( +
+ )} + + + {node.name} + +
- - {node.name} - + {/* Right side: comment */} {node.comment && ( - {node.comment} + # {node.comment} )}
); diff --git a/pages/index.mdx b/pages/index.mdx index e5bb060..3434ac5 100644 --- a/pages/index.mdx +++ b/pages/index.mdx @@ -73,37 +73,32 @@ Scratch uses an opinionated project structure and requires **no boilerplate or c A simple Scratch project (created with `scratch create`) looks like this: -Use `scratch build` to compile this project into a static website, like this one. You can change styles and customize the page wrapper component by modifying the files in the `src/` directory: - - -Component files and js/ts libraries can live anywhere in `pages/` and `src/`. They are auto-detected by Scratch and don't need to be explicitly imported in your .mdx files as long as the filename matches the component name. - -Scratch installs build dependencies automatically. You can add additional third-party dependencies by including a `package.json` file in your project root. +Use `scratch build` to compile this project into a static website, like this one. -## Built on Bun +Component files and libraries can live anywhere in `pages/` and `src/`. They are auto-detected by Scratch and don't need to be explicitly imported in your .mdx files as long as the filename matches the component name. -Scratch is built on [Bun](https://bun.com/) for lightning-fast builds, development with HMR, and native typescript support. It uses the [Tailwind CSS](https://tailwindcss.com/) framework to make component styling easy. - -Scratch compiles Javascript (.js), Typescript (.ts), JSX (.jsx), TSX (.tsx), Markdown (.md), and MDX (.mdx). +Modify `src/tailwind.css` directory to change the styling of your document. Add headers, footers and other site-wide elements by modifying `src/PageWrapper.jsx`. ## Commands @@ -133,3 +128,18 @@ scratch revert --list # list available template files # Update scratch to latest version scratch update ``` + +## Acknowledgements + + +Scratch is built on [Bun](https://bun.com/) for lightning-fast builds, development with HMR, and native typescript support. It uses the [Tailwind CSS](https://tailwindcss.com/) framework to make component styling easy. + +[React](https://react.dev/) and [MDX](https://mdxjs.com/) make it possible to write with Markdown and code. + +Content processing is handled by the [unified](https://unifiedjs.com/) ecosystem, with [remark-gfm](https://github.com/remarkjs/remark-gfm) for GitHub Flavored Markdown and [remark-frontmatter](https://github.com/remarkjs/remark-frontmatter) plus [gray-matter](https://github.com/jonschlinkert/gray-matter) for parsing front matter. + +[Shiki](https://shiki.style/) provides beautiful, accurate syntax highlighting with VS Code's grammar engine. + +[Commander.js](https://github.com/tj/commander.js) powers the CLI, and [fast-glob](https://github.com/mrmlnc/fast-glob) handles file discovery. + +Additional dependencies: [acorn](https://github.com/acornjs/acorn), [@mdx-js/esbuild](https://mdxjs.com/packages/esbuild/), [@shikijs/rehype](https://shiki.style/packages/rehype), [@types/mdast](https://github.com/DefinitelyTyped/DefinitelyTyped), [unist-util-visit](https://github.com/syntax-tree/unist-util-visit), [unist-util-is](https://github.com/syntax-tree/unist-util-is). From a6a69ae055a98e14ad1aeecd9e260039d1010cda Mon Sep 17 00:00:00 2001 From: Peter Koomen Date: Wed, 24 Dec 2025 11:48:30 -0800 Subject: [PATCH 08/37] clean up footer --- src/PageWrapper.jsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/PageWrapper.jsx b/src/PageWrapper.jsx index f39868c..7fcdfe6 100644 --- a/src/PageWrapper.jsx +++ b/src/PageWrapper.jsx @@ -18,9 +18,7 @@ export default function PageWrapper({ children }) {
{children}
- Released under the MIT License -
- Copyright 2025 Pete Koomen + MIT License · © 2025 Pete Koomen
); From 48e545a4da41a2cdd8b0ccd7d318360ce2856cf4 Mon Sep 17 00:00:00 2001 From: Peter Koomen Date: Wed, 24 Dec 2025 14:21:09 -0800 Subject: [PATCH 09/37] Fix file comments on mobile --- pages/Files.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/Files.tsx b/pages/Files.tsx index 630cf98..3773b99 100644 --- a/pages/Files.tsx +++ b/pages/Files.tsx @@ -202,7 +202,7 @@ function FileRow({ node, isCollapsed, onToggle }: FileRowProps) { {/* Right side: comment */} {node.comment && ( - # {node.comment} + # {node.comment} )}
); @@ -255,7 +255,7 @@ export default function Files({ content, children }: FilesProps) { }; return ( -
+
{nodes.map((node) => ( Date: Wed, 24 Dec 2025 14:32:50 -0800 Subject: [PATCH 10/37] clean up files --- pages/Files.tsx | 6 ++++-- pages/index.mdx | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/pages/Files.tsx b/pages/Files.tsx index 3773b99..8b04471 100644 --- a/pages/Files.tsx +++ b/pages/Files.tsx @@ -159,7 +159,7 @@ function FileRow({ node, isCollapsed, onToggle }: FileRowProps) {
{/* Left side: indent + caret + name */}
{/* Caret for folders */} @@ -202,7 +202,9 @@ function FileRow({ node, isCollapsed, onToggle }: FileRowProps) { {/* Right side: comment */} {node.comment && ( - # {node.comment} + + {node.comment} + )}
); diff --git a/pages/index.mdx b/pages/index.mdx index 3434ac5..2ea6dbf 100644 --- a/pages/index.mdx +++ b/pages/index.mdx @@ -94,7 +94,7 @@ my-scratch-project/ `} /> -Use `scratch build` to compile this project into a static website, like this one. +Use `scratch build` to compile this project into a static website, like [this one](https://github.com/scratch/scratch.dev). Component files and libraries can live anywhere in `pages/` and `src/`. They are auto-detected by Scratch and don't need to be explicitly imported in your .mdx files as long as the filename matches the component name. From 990b4dc809c84a074e41c1ef1eacde51dd5b5d74 Mon Sep 17 00:00:00 2001 From: Peter Koomen Date: Wed, 24 Dec 2025 16:24:52 -0800 Subject: [PATCH 11/37] tweaks --- pages/BouncingDvdLogo.tsx | 53 ++++++++++++++++++++++++++++++++------- pages/Counter.tsx | 2 +- pages/index.mdx | 24 ++++++++---------- 3 files changed, 55 insertions(+), 24 deletions(-) diff --git a/pages/BouncingDvdLogo.tsx b/pages/BouncingDvdLogo.tsx index 3f9ebf8..7419cfc 100644 --- a/pages/BouncingDvdLogo.tsx +++ b/pages/BouncingDvdLogo.tsx @@ -4,9 +4,19 @@ export default function BouncingDvdLogo() { const containerRef = useRef(null); const [cornerHits, setCornerHits] = useState(0); const [position, setPosition] = useState({ x: 20, y: 20 }); - const [velocity, setVelocity] = useState({ x: 2, y: 1.5 }); + const [speedMultiplier, setSpeedMultiplier] = useState(1); + const [velocity, setVelocity] = useState({ x: 2 * 1, y: 1.5 * 1 }); const [color, setColor] = useState("#8b5cf6"); + const toggleSpeed = () => { + const newMultiplier = speedMultiplier === 10 ? 1 : 10; + setSpeedMultiplier(newMultiplier); + setVelocity((v) => ({ + x: Math.sign(v.x) * 2 * newMultiplier, + y: Math.sign(v.y) * 1.5 * newMultiplier, + })); + }; + const logoWidth = 80; const logoHeight = 40; @@ -21,13 +31,17 @@ export default function BouncingDvdLogo() { const getRandomColor = () => { const newColor = colors[Math.floor(Math.random() * colors.length)]; - return newColor === color ? colors[(colors.indexOf(color) + 1) % colors.length] : newColor; + return newColor === color + ? colors[(colors.indexOf(color) + 1) % colors.length] + : newColor; }; useEffect(() => { const container = containerRef.current; if (!container) return; + const cornerTolerance = 10; // pixels of tolerance for corner detection + const animate = () => { setPosition((prev) => { const containerWidth = container.clientWidth; @@ -64,8 +78,15 @@ export default function BouncingDvdLogo() { if (!hitX) setColor(getRandomColor()); } - // Corner hit! - if (hitX && hitY) { + // Corner hit - check if near both edges (with tolerance) + const nearLeftOrRight = + newX <= cornerTolerance || + newX >= containerWidth - logoWidth - cornerTolerance; + const nearTopOrBottom = + newY <= cornerTolerance || + newY >= containerHeight - logoHeight - cornerTolerance; + + if ((hitX || hitY) && nearLeftOrRight && nearTopOrBottom) { setCornerHits((c) => c + 1); } @@ -80,7 +101,10 @@ export default function BouncingDvdLogo() { return (
{/* TV outer frame */} -
+
{/* TV screen bezel */}
{/* Screen container */} @@ -88,9 +112,17 @@ export default function BouncingDvdLogo() { ref={containerRef} className="relative w-full h-48 rounded overflow-hidden" style={{ - background: "linear-gradient(145deg, #1a1a2e 0%, #0f0f1a 50%, #1a1a2e 100%)", + background: + "linear-gradient(145deg, #1a1a2e 0%, #0f0f1a 50%, #1a1a2e 100%)", }} > + {/* Corner hits counter in center */} +
+ + {cornerHits} + +
+ {/* Bouncing logo */}
-

- Corner hits: {cornerHits} + {/* Prompt text */} +

+ "Make a component that looks like a TV screen with a bouncing DVD logo. + Count the number of times...

); diff --git a/pages/Counter.tsx b/pages/Counter.tsx index 03186e1..b55604a 100644 --- a/pages/Counter.tsx +++ b/pages/Counter.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState } from "react"; export default function Counter(): React.ReactElement { const [count, setCount] = useState(0); diff --git a/pages/index.mdx b/pages/index.mdx index 2ea6dbf..8cc68f8 100644 --- a/pages/index.mdx +++ b/pages/index.mdx @@ -58,10 +58,6 @@ Scratch also supports Github-flavored Markdown features like checklists and tabl Unlike traditional word processors, Scratch makes it easy to express any idea. If you can describe it to a coding agent, you can add it to your document: -```text -Make me a component that looks like a TV screen with a bouncing DVD logo. Count the number of times the logo hits a corner and display that below the TV. -``` - Collaborating with AI makes writing more fun. Scratch makes that easy. @@ -88,8 +84,8 @@ my-scratch-project/ ---CodeBlock.tsx ---Heading.tsx ---Link.tsx --AGENTS.md # context for coding agents --package.json # build and additional dependencies +-AGENTS.md # agent context +-package.json # dependencies -.gitignore `} /> @@ -121,9 +117,9 @@ scratch preview scratch clean # Revert a file to its template version -scratch revert [file] # revert a file to its template version -scratch revert --force [file] # overwrite without confirmation -scratch revert --list # list available template files +scratch get [file] # revert a file to its template version +scratch get --force [file] # overwrite without confirmation +scratch get --list # list available template files # Update scratch to latest version scratch update @@ -132,14 +128,14 @@ scratch update ## Acknowledgements -Scratch is built on [Bun](https://bun.com/) for lightning-fast builds, development with HMR, and native typescript support. It uses the [Tailwind CSS](https://tailwindcss.com/) framework to make component styling easy. +[Bun](https://bun.com/) for lightning-fast builds, development with HMR, native typescript support, and a portable executable. -[React](https://react.dev/) and [MDX](https://mdxjs.com/) make it possible to write with Markdown and code. +[React](https://react.dev/) and [MDX](https://mdxjs.com/) make it possible to write with Markdown and code. [Tailwind CSS](https://tailwindcss.com/) makes component styling easy. -Content processing is handled by the [unified](https://unifiedjs.com/) ecosystem, with [remark-gfm](https://github.com/remarkjs/remark-gfm) for GitHub Flavored Markdown and [remark-frontmatter](https://github.com/remarkjs/remark-frontmatter) plus [gray-matter](https://github.com/jonschlinkert/gray-matter) for parsing front matter. +Content preprocessing relies on [unified](https://unifiedjs.com/), with [remark-gfm](https://github.com/remarkjs/remark-gfm) for GitHub Flavored Markdown and [remark-frontmatter](https://github.com/remarkjs/remark-frontmatter) plus [gray-matter](https://github.com/jonschlinkert/gray-matter) for parsing front matter. [Shiki](https://shiki.style/) provides beautiful, accurate syntax highlighting with VS Code's grammar engine. -[Commander.js](https://github.com/tj/commander.js) powers the CLI, and [fast-glob](https://github.com/mrmlnc/fast-glob) handles file discovery. +[Commander.js](https://github.com/tj/commander.js) scaffolds the CLI. -Additional dependencies: [acorn](https://github.com/acornjs/acorn), [@mdx-js/esbuild](https://mdxjs.com/packages/esbuild/), [@shikijs/rehype](https://shiki.style/packages/rehype), [@types/mdast](https://github.com/DefinitelyTyped/DefinitelyTyped), [unist-util-visit](https://github.com/syntax-tree/unist-util-visit), [unist-util-is](https://github.com/syntax-tree/unist-util-is). +Additional dependencies: [fast-glob](https://github.com/mrmlnc/fast-glob), [acorn](https://github.com/acornjs/acorn), [@mdx-js/esbuild](https://mdxjs.com/packages/esbuild/), [@shikijs/rehype](https://shiki.style/packages/rehype), [@types/mdast](https://github.com/DefinitelyTyped/DefinitelyTyped), [unist-util-visit](https://github.com/syntax-tree/unist-util-visit), [unist-util-is](https://github.com/syntax-tree/unist-util-is). From 19ab9badd2fa80a6e1f2d6c89eb14963837c442f Mon Sep 17 00:00:00 2001 From: Peter Koomen Date: Wed, 24 Dec 2025 21:25:53 -0800 Subject: [PATCH 12/37] update tailwind --- src/tailwind.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tailwind.css b/src/tailwind.css index a31aad8..db0ab5b 100644 --- a/src/tailwind.css +++ b/src/tailwind.css @@ -44,9 +44,9 @@ @apply mr-2 align-middle ml-1; } -/* Tables - add horizontal padding, prevent overflow */ +/* Tables - center by default, prevent overflow */ .prose :where(table):not(:where([class~='not-prose'] *)) { - @apply mx-4 w-auto max-w-full; + @apply mx-auto w-auto max-w-full; } /* From 04c63bcb21cb4a6b034ca9d916073a44b3cdf5d2 Mon Sep 17 00:00:00 2001 From: Peter Koomen Date: Wed, 24 Dec 2025 22:12:36 -0800 Subject: [PATCH 13/37] some animation on Files component --- AGENTS.md | 126 +++++++++++++++++---- pages/Files.tsx | 285 ++++++++++++++++++++++++++++++++++++++++++++++- pages/index.mdx | 4 +- src/tailwind.css | 50 +++++++++ 4 files changed, 433 insertions(+), 32 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 5b5c85f..152a2ad 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,13 +29,16 @@ Run `scratch --help` to see all available commands. project/ ├── pages/ # MDX and Markdown content (required) │ ├── index.mdx # Homepage (resolves to /) +│ ├── Counter.tsx # Components can live alongside pages │ └── posts/ │ └── hello.mdx # Resolves to /posts/hello/ -├── components/ # React components (optional) -│ └── Button.jsx +├── src/ # React components and styles (optional) +│ ├── Button.jsx +│ ├── PageWrapper.jsx +│ ├── tailwind.css +│ └── markdown/ # Custom markdown renderers ├── public/ # Static assets (optional, copied as-is) │ └── logo.png -├── tailwind.css # Tailwind theme customization (optional) └── dist/ # Build output (generated) ``` @@ -79,7 +82,7 @@ The pattern: `index.mdx` resolves to its parent directory path, other files get ### Auto-Import (No Explicit Imports Needed!) -Components in `components/` or `pages/` are **automatically available** in MDX files without importing them. Just use them: +Components in `src/` or `pages/` are **automatically available** in MDX files without importing them. Just use them: ```mdx # My Page @@ -92,19 +95,53 @@ Components in `components/` or `pages/` are **automatically available** in MDX f The build automatically injects the necessary imports. **Important:** The component name must match the filename: -- `components/Button.jsx` → ` +important text +``` + +**Block children** (markdown with blank lines): +```mdx + + +## Warning Title + +This is a **markdown** paragraph inside the component. + +- List item one +- List item two + + +``` + +The blank lines after the opening tag and before the closing tag are required for block-level markdown to be parsed correctly. ### Styling with Tailwind Components can use Tailwind CSS utility classes - they're globally available: ```jsx -// components/Card.jsx +// src/Card.jsx export function Card({ children }) { return (
@@ -116,10 +153,10 @@ export function Card({ children }) { ### PageWrapper Component -If you create a `components/PageWrapper.jsx`, it will **automatically wrap all page content**. Useful for layouts: +If you create a `src/PageWrapper.jsx`, it will **automatically wrap all page content**. Useful for layouts: ```jsx -// components/PageWrapper.jsx +// src/PageWrapper.jsx export default function PageWrapper({ children }) { return (
@@ -133,7 +170,7 @@ export default function PageWrapper({ children }) { ### Markdown Components -Components in `components/markdown/` override default Markdown element rendering: +Components in `src/markdown/` override default Markdown element rendering: - `Heading.tsx` - Custom heading rendering (h1-h6) - `CodeBlock.tsx` - Custom code block rendering with syntax highlighting @@ -148,25 +185,66 @@ Files in `public/` are copied directly to the build output. Reference them with ## Theming -Scratch uses custom prose styling defined in `tailwind.css` for markdown content. The default template includes: +Scratch uses [Tailwind Typography](https://github.com/tailwindlabs/tailwindcss-typography) for markdown styling. The `prose` class is applied via PageWrapper. + +### Customizing Typography + +- **Size variants**: Add `prose-sm`, `prose-lg`, `prose-xl` in PageWrapper.jsx +- **Color themes**: Add `prose-slate`, `prose-zinc`, `prose-neutral`, etc. -- `scratch-prose` class for typography styling -- Dark mode support (follows system preference via `.dark` class) +### Overriding Prose Styling (No Custom Component) -### Customizing the Theme +Tailwind Typography supports element modifiers to override styling for specific element types directly in `PageWrapper.jsx`: -The `tailwind.css` file contains all prose styling for markdown elements. You can customize: +```jsx +
+``` -- Headings (h1-h4), paragraphs, links, lists -- Code blocks and inline code -- Blockquotes, tables, images -- Light and dark mode colors +Available element modifiers: +- `prose-headings:` - all headings (h1-h6) +- `prose-h1:`, `prose-h2:`, etc. - specific heading levels +- `prose-a:` - links +- `prose-p:` - paragraphs +- `prose-blockquote:` - blockquotes +- `prose-code:` - inline code +- `prose-pre:` - code blocks +- `prose-ol:`, `prose-ul:`, `prose-li:` - lists +- `prose-table:`, `prose-th:`, `prose-td:` - tables +- `prose-img:`, `prose-figure:`, `prose-figcaption:` - images + +You can also add CSS overrides in `src/tailwind.css`: +```css +.prose a { + @apply text-blue-600 hover:text-blue-800 no-underline; +} +``` -Simply edit the `.scratch-prose` rules in `tailwind.css` to match your design. +### Overriding with Custom Components -### Dark Mode +For more complex overrides (adding interactivity, conditional logic), create a custom component in `src/markdown/`: + +1. Create/edit a component (e.g., `Link.tsx`) +2. Export from `src/markdown/index.ts` and add to `MDXComponents` + +Example: +```tsx +// src/markdown/Link.tsx +export default function Link({ href, children, ...props }) { + const isExternal = href?.startsWith('http'); + return ( + + {children} + + ); +} +``` -Dark mode is enabled by default and follows system preferences. The `PageWrapper` component uses the `scratch-prose` class, and dark mode styles are automatically applied when the `.dark` class is present on a parent element. +Use `not-prose` class to exclude elements from typography styling. ## Generated Files diff --git a/pages/Files.tsx b/pages/Files.tsx index 8b04471..eb1121c 100644 --- a/pages/Files.tsx +++ b/pages/Files.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, useRef, useEffect } from "react"; // Tree node structure from parsing interface TreeNode { @@ -145,18 +145,46 @@ function getInitialCollapsed(roots: TreeNode[]): Set { return collapsed; } +const ROW_HEIGHT = 28; // h-7 = 1.75rem = 28px + interface FileRowProps { node: RenderNode; isCollapsed: boolean; onToggle: () => void; + animationState?: "entering" | "exiting" | "none"; + slideOffset?: number; // pixels to translate Y + isSliding?: boolean; + isHidden?: boolean; } -function FileRow({ node, isCollapsed, onToggle }: FileRowProps) { +function FileRow({ + node, + isCollapsed, + onToggle, + animationState = "none", + slideOffset = 0, + isSliding = false, + isHidden = false, +}: FileRowProps) { const isClickable = node.isFolder; const isDotfile = node.name.startsWith("."); + const classes = ["flex", "items-center", "h-7", "font-mono", "text-sm"]; + + if (animationState === "entering") classes.push("animate-fade-in"); + if (animationState === "exiting") classes.push("animate-fade-out"); + if (isSliding) classes.push("animate-slide"); + + const style: React.CSSProperties = {}; + if (isSliding && slideOffset !== 0) { + (style as Record)["--slide-offset"] = `${slideOffset}px`; + } + if (isHidden) { + style.opacity = 0; + } + return ( -
+
{/* Left side: indent + caret + name */}
@@ -235,6 +263,31 @@ interface FilesProps { children?: React.ReactNode; } +const ANIMATION_DURATION = 1000; // ms + +type AnimationPhase = + | "idle" + | "expanding-slide" // items below sliding down + | "expanding-fade" // new items fading in + | "collapsing-fade" // items fading out + | "collapsing-slide"; // items below sliding up + +interface AnimationInfo { + phase: AnimationPhase; + toggledFolderId: string | null; + enteringIds: Set; + exitingNodes: RenderNode[]; + itemCount: number; // number of items entering or exiting +} + +const IDLE_ANIMATION: AnimationInfo = { + phase: "idle", + toggledFolderId: null, + enteringIds: new Set(), + exitingNodes: [], + itemCount: 0, +}; + export default function Files({ content, children }: FilesProps) { const text = content ?? extractText(children); const [tree] = useState(() => parseTree(text)); @@ -242,9 +295,133 @@ export default function Files({ content, children }: FilesProps) { getInitialCollapsed(tree) ); - const nodes = flattenTree(tree, collapsedIds); + // Force re-render trigger + const [, forceUpdate] = useState(0); + + // Store animation state in ref for synchronous access during render + const animRef = useRef(IDLE_ANIMATION); + + // Track previous state for detecting changes + const prevStateRef = useRef<{ + collapsedIds: Set; + visibleIds: Set; + }>({ + collapsedIds: new Set(collapsedIds), + visibleIds: new Set(), + }); + + const currentNodes = flattenTree(tree, collapsedIds); + const currentVisibleIds = new Set(currentNodes.map((n) => n.id)); + + // Initialize prev visible IDs on first render + if (prevStateRef.current.visibleIds.size === 0) { + prevStateRef.current.visibleIds = currentVisibleIds; + } + + // Synchronously detect toggle and set up animation during render + const prevCollapsed = prevStateRef.current.collapsedIds; + const prevVisibleIds = prevStateRef.current.visibleIds; + + let toggledFolder: { id: string; expanded: boolean } | null = null; + for (const id of collapsedIds) { + if (!prevCollapsed.has(id)) { + toggledFolder = { id, expanded: false }; + break; + } + } + if (!toggledFolder) { + for (const id of prevCollapsed) { + if (!collapsedIds.has(id)) { + toggledFolder = { id, expanded: true }; + break; + } + } + } + + // If a toggle just happened and we're idle, start the animation synchronously + if (toggledFolder && animRef.current.phase === "idle") { + const allNodesFlat = flattenTree(tree, new Set()); + + if (toggledFolder.expanded) { + // Expanding: find entering nodes + const entering = new Set(); + for (const id of currentVisibleIds) { + if (!prevVisibleIds.has(id)) { + entering.add(id); + } + } + + if (entering.size > 0) { + animRef.current = { + phase: "expanding-slide", + toggledFolderId: toggledFolder.id, + enteringIds: entering, + exitingNodes: [], + itemCount: entering.size, + }; + } + } else { + // Collapsing: find exiting nodes + const exiting: RenderNode[] = []; + for (const id of prevVisibleIds) { + if (!currentVisibleIds.has(id)) { + const node = allNodesFlat.find((n) => n.id === id); + if (node) exiting.push(node); + } + } + + if (exiting.length > 0) { + animRef.current = { + phase: "collapsing-fade", + toggledFolderId: toggledFolder.id, + enteringIds: new Set(), + exitingNodes: exiting, + itemCount: exiting.length, + }; + } + } + + // Update prev state + prevStateRef.current = { + collapsedIds: new Set(collapsedIds), + visibleIds: currentVisibleIds, + }; + } + + // Get current animation state from ref + const animation = animRef.current; + + // Transition between animation phases using effect + useEffect(() => { + if (animRef.current.phase === "idle") return; + + const timer = setTimeout(() => { + switch (animRef.current.phase) { + case "expanding-slide": + animRef.current = { ...animRef.current, phase: "expanding-fade" }; + forceUpdate((n) => n + 1); + break; + case "expanding-fade": + animRef.current = IDLE_ANIMATION; + forceUpdate((n) => n + 1); + break; + case "collapsing-fade": + animRef.current = { ...animRef.current, phase: "collapsing-slide" }; + forceUpdate((n) => n + 1); + break; + case "collapsing-slide": + animRef.current = IDLE_ANIMATION; + forceUpdate((n) => n + 1); + break; + } + }, ANIMATION_DURATION); + + return () => clearTimeout(timer); + }, [animation.phase]); const toggleCollapse = (id: string) => { + if (animRef.current.phase !== "idle") return; // Don't allow toggling during animation + setCollapsedIds((prev) => { const next = new Set(prev); if (next.has(id)) { @@ -256,14 +433,110 @@ export default function Files({ content, children }: FilesProps) { }); }; + // Build the list of nodes to render + const { phase, toggledFolderId, enteringIds, exitingNodes, itemCount } = + animation; + + // For collapsing, we need to include exiting nodes in the render + const allNodes = [...currentNodes]; + + if (phase === "collapsing-fade") { + // Insert exiting nodes after the toggled folder (only during fade, not slide) + const folderIndex = allNodes.findIndex((n) => n.id === toggledFolderId); + if (folderIndex !== -1) { + allNodes.splice(folderIndex + 1, 0, ...exitingNodes); + } + } + + // Find the index of the toggled folder to determine which items slide + const toggledFolderIndex = allNodes.findIndex( + (n) => n.id === toggledFolderId + ); + + const getAnimationState = ( + nodeId: string + ): "entering" | "exiting" | "none" => { + if (phase === "expanding-fade" && enteringIds.has(nodeId)) { + return "entering"; + } + if ( + phase === "collapsing-fade" && + exitingNodes.some((n) => n.id === nodeId) + ) { + return "exiting"; + } + return "none"; + }; + + const getSlideOffset = (index: number): number => { + if (toggledFolderIndex === -1) return 0; + + // For expanding-slide: items below the new content need to slide down first + // The entering items are already in the list, so items after them slide + if (phase === "expanding-slide") { + // Find the first entering item's index + const firstEnteringIndex = allNodes.findIndex((n) => + enteringIds.has(n.id) + ); + if (firstEnteringIndex !== -1 && index >= firstEnteringIndex) { + // Items at or after the first entering item + if (enteringIds.has(allNodes[index].id)) { + // Entering items start invisible (handled by opacity) + return 0; + } + // Items below slide down from above (start offset, will animate to 0) + return -itemCount * ROW_HEIGHT; + } + } + + // For collapsing-slide: items below the folder slide up from their old positions + // Exiting nodes are no longer in the DOM, so items are at final positions + // We offset them down (positive) to their old visual positions, then animate to 0 + if (phase === "collapsing-slide") { + if (index > toggledFolderIndex) { + return itemCount * ROW_HEIGHT; + } + } + + return 0; + }; + + const isSliding = (index: number): boolean => { + if (toggledFolderIndex === -1) return false; + + if (phase === "expanding-slide") { + const firstEnteringIndex = allNodes.findIndex((n) => + enteringIds.has(n.id) + ); + if (firstEnteringIndex !== -1 && index >= firstEnteringIndex) { + return !enteringIds.has(allNodes[index].id); + } + } + + if (phase === "collapsing-slide") { + return index > toggledFolderIndex; + } + + return false; + }; + + // Hide entering items during slide phase (before they fade in) + const isHidden = (nodeId: string): boolean => { + return phase === "expanding-slide" && enteringIds.has(nodeId); + }; + return (
- {nodes.map((node) => ( + {allNodes.map((node, index) => ( toggleCollapse(node.id)} + animationState={getAnimationState(node.id)} + slideOffset={getSlideOffset(index)} + isSliding={isSliding(index)} + isHidden={isHidden(node.id)} /> ))}
diff --git a/pages/index.mdx b/pages/index.mdx index 8cc68f8..20470d4 100644 --- a/pages/index.mdx +++ b/pages/index.mdx @@ -100,14 +100,14 @@ Modify `src/tailwind.css` directory to change the styling of your document. Add ```bash # Create a new project -scratch create [path] # create project at path (default: current directory) +scratch create [path] # create project # Start dev server with hot module reloading scratch dev # Build for production scratch build -scratch build --no-ssg # disable static site generation +scratch build --no-ssg # disable server-side generation scratch build --development # unminified, with source maps # Preview production build locally diff --git a/src/tailwind.css b/src/tailwind.css index db0ab5b..f5f6bb5 100644 --- a/src/tailwind.css +++ b/src/tailwind.css @@ -72,3 +72,53 @@ pre > code { @apply absolute -left-6 top-0 opacity-0 group-hover:opacity-100 transition-opacity no-underline select-none; @apply text-gray-400 hover:text-gray-600; } + +/* Files component animations */ +@keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes fade-out { + from { + opacity: 1; + } + to { + opacity: 0; + } +} + +.animate-fade-in { + animation: fade-in 1000ms ease-out forwards; +} + +.animate-fade-out { + animation: fade-out 1000ms ease-out forwards; +} + +/* Slide animations use CSS custom properties for dynamic offset */ +@keyframes slide-down { + from { + transform: translateY(var(--slide-offset)); + } + to { + transform: translateY(0); + } +} + +@keyframes slide-up { + from { + transform: translateY(var(--slide-offset)); + } + to { + transform: translateY(0); + } +} + +.animate-slide { + animation: slide-down 1000ms ease-out forwards; +} From 4c2e167f4579aa18f299ccfdd7889fdc57364b4e Mon Sep 17 00:00:00 2001 From: Peter Koomen Date: Thu, 25 Dec 2025 11:53:56 -0800 Subject: [PATCH 14/37] simplify file animations --- pages/Files.tsx | 283 +---------------------------------------------- src/tailwind.css | 50 --------- 2 files changed, 5 insertions(+), 328 deletions(-) diff --git a/pages/Files.tsx b/pages/Files.tsx index eb1121c..48ba629 100644 --- a/pages/Files.tsx +++ b/pages/Files.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef, useEffect } from "react"; +import React, { useState } from "react"; // Tree node structure from parsing interface TreeNode { @@ -145,46 +145,18 @@ function getInitialCollapsed(roots: TreeNode[]): Set { return collapsed; } -const ROW_HEIGHT = 28; // h-7 = 1.75rem = 28px - interface FileRowProps { node: RenderNode; isCollapsed: boolean; onToggle: () => void; - animationState?: "entering" | "exiting" | "none"; - slideOffset?: number; // pixels to translate Y - isSliding?: boolean; - isHidden?: boolean; } -function FileRow({ - node, - isCollapsed, - onToggle, - animationState = "none", - slideOffset = 0, - isSliding = false, - isHidden = false, -}: FileRowProps) { +function FileRow({ node, isCollapsed, onToggle }: FileRowProps) { const isClickable = node.isFolder; const isDotfile = node.name.startsWith("."); - const classes = ["flex", "items-center", "h-7", "font-mono", "text-sm"]; - - if (animationState === "entering") classes.push("animate-fade-in"); - if (animationState === "exiting") classes.push("animate-fade-out"); - if (isSliding) classes.push("animate-slide"); - - const style: React.CSSProperties = {}; - if (isSliding && slideOffset !== 0) { - (style as Record)["--slide-offset"] = `${slideOffset}px`; - } - if (isHidden) { - style.opacity = 0; - } - return ( -
+
{/* Left side: indent + caret + name */}
; - exitingNodes: RenderNode[]; - itemCount: number; // number of items entering or exiting -} - -const IDLE_ANIMATION: AnimationInfo = { - phase: "idle", - toggledFolderId: null, - enteringIds: new Set(), - exitingNodes: [], - itemCount: 0, -}; - export default function Files({ content, children }: FilesProps) { const text = content ?? extractText(children); const [tree] = useState(() => parseTree(text)); @@ -295,133 +242,9 @@ export default function Files({ content, children }: FilesProps) { getInitialCollapsed(tree) ); - // Force re-render trigger - const [, forceUpdate] = useState(0); - - // Store animation state in ref for synchronous access during render - const animRef = useRef(IDLE_ANIMATION); - - // Track previous state for detecting changes - const prevStateRef = useRef<{ - collapsedIds: Set; - visibleIds: Set; - }>({ - collapsedIds: new Set(collapsedIds), - visibleIds: new Set(), - }); - - const currentNodes = flattenTree(tree, collapsedIds); - const currentVisibleIds = new Set(currentNodes.map((n) => n.id)); - - // Initialize prev visible IDs on first render - if (prevStateRef.current.visibleIds.size === 0) { - prevStateRef.current.visibleIds = currentVisibleIds; - } - - // Synchronously detect toggle and set up animation during render - const prevCollapsed = prevStateRef.current.collapsedIds; - const prevVisibleIds = prevStateRef.current.visibleIds; - - let toggledFolder: { id: string; expanded: boolean } | null = null; - for (const id of collapsedIds) { - if (!prevCollapsed.has(id)) { - toggledFolder = { id, expanded: false }; - break; - } - } - if (!toggledFolder) { - for (const id of prevCollapsed) { - if (!collapsedIds.has(id)) { - toggledFolder = { id, expanded: true }; - break; - } - } - } - - // If a toggle just happened and we're idle, start the animation synchronously - if (toggledFolder && animRef.current.phase === "idle") { - const allNodesFlat = flattenTree(tree, new Set()); - - if (toggledFolder.expanded) { - // Expanding: find entering nodes - const entering = new Set(); - for (const id of currentVisibleIds) { - if (!prevVisibleIds.has(id)) { - entering.add(id); - } - } - - if (entering.size > 0) { - animRef.current = { - phase: "expanding-slide", - toggledFolderId: toggledFolder.id, - enteringIds: entering, - exitingNodes: [], - itemCount: entering.size, - }; - } - } else { - // Collapsing: find exiting nodes - const exiting: RenderNode[] = []; - for (const id of prevVisibleIds) { - if (!currentVisibleIds.has(id)) { - const node = allNodesFlat.find((n) => n.id === id); - if (node) exiting.push(node); - } - } - - if (exiting.length > 0) { - animRef.current = { - phase: "collapsing-fade", - toggledFolderId: toggledFolder.id, - enteringIds: new Set(), - exitingNodes: exiting, - itemCount: exiting.length, - }; - } - } - - // Update prev state - prevStateRef.current = { - collapsedIds: new Set(collapsedIds), - visibleIds: currentVisibleIds, - }; - } - - // Get current animation state from ref - const animation = animRef.current; - - // Transition between animation phases using effect - useEffect(() => { - if (animRef.current.phase === "idle") return; - - const timer = setTimeout(() => { - switch (animRef.current.phase) { - case "expanding-slide": - animRef.current = { ...animRef.current, phase: "expanding-fade" }; - forceUpdate((n) => n + 1); - break; - case "expanding-fade": - animRef.current = IDLE_ANIMATION; - forceUpdate((n) => n + 1); - break; - case "collapsing-fade": - animRef.current = { ...animRef.current, phase: "collapsing-slide" }; - forceUpdate((n) => n + 1); - break; - case "collapsing-slide": - animRef.current = IDLE_ANIMATION; - forceUpdate((n) => n + 1); - break; - } - }, ANIMATION_DURATION); - - return () => clearTimeout(timer); - }, [animation.phase]); + const nodes = flattenTree(tree, collapsedIds); const toggleCollapse = (id: string) => { - if (animRef.current.phase !== "idle") return; // Don't allow toggling during animation - setCollapsedIds((prev) => { const next = new Set(prev); if (next.has(id)) { @@ -433,110 +256,14 @@ export default function Files({ content, children }: FilesProps) { }); }; - // Build the list of nodes to render - const { phase, toggledFolderId, enteringIds, exitingNodes, itemCount } = - animation; - - // For collapsing, we need to include exiting nodes in the render - const allNodes = [...currentNodes]; - - if (phase === "collapsing-fade") { - // Insert exiting nodes after the toggled folder (only during fade, not slide) - const folderIndex = allNodes.findIndex((n) => n.id === toggledFolderId); - if (folderIndex !== -1) { - allNodes.splice(folderIndex + 1, 0, ...exitingNodes); - } - } - - // Find the index of the toggled folder to determine which items slide - const toggledFolderIndex = allNodes.findIndex( - (n) => n.id === toggledFolderId - ); - - const getAnimationState = ( - nodeId: string - ): "entering" | "exiting" | "none" => { - if (phase === "expanding-fade" && enteringIds.has(nodeId)) { - return "entering"; - } - if ( - phase === "collapsing-fade" && - exitingNodes.some((n) => n.id === nodeId) - ) { - return "exiting"; - } - return "none"; - }; - - const getSlideOffset = (index: number): number => { - if (toggledFolderIndex === -1) return 0; - - // For expanding-slide: items below the new content need to slide down first - // The entering items are already in the list, so items after them slide - if (phase === "expanding-slide") { - // Find the first entering item's index - const firstEnteringIndex = allNodes.findIndex((n) => - enteringIds.has(n.id) - ); - if (firstEnteringIndex !== -1 && index >= firstEnteringIndex) { - // Items at or after the first entering item - if (enteringIds.has(allNodes[index].id)) { - // Entering items start invisible (handled by opacity) - return 0; - } - // Items below slide down from above (start offset, will animate to 0) - return -itemCount * ROW_HEIGHT; - } - } - - // For collapsing-slide: items below the folder slide up from their old positions - // Exiting nodes are no longer in the DOM, so items are at final positions - // We offset them down (positive) to their old visual positions, then animate to 0 - if (phase === "collapsing-slide") { - if (index > toggledFolderIndex) { - return itemCount * ROW_HEIGHT; - } - } - - return 0; - }; - - const isSliding = (index: number): boolean => { - if (toggledFolderIndex === -1) return false; - - if (phase === "expanding-slide") { - const firstEnteringIndex = allNodes.findIndex((n) => - enteringIds.has(n.id) - ); - if (firstEnteringIndex !== -1 && index >= firstEnteringIndex) { - return !enteringIds.has(allNodes[index].id); - } - } - - if (phase === "collapsing-slide") { - return index > toggledFolderIndex; - } - - return false; - }; - - // Hide entering items during slide phase (before they fade in) - const isHidden = (nodeId: string): boolean => { - return phase === "expanding-slide" && enteringIds.has(nodeId); - }; - return (
- {allNodes.map((node, index) => ( + {nodes.map((node) => ( toggleCollapse(node.id)} - animationState={getAnimationState(node.id)} - slideOffset={getSlideOffset(index)} - isSliding={isSliding(index)} - isHidden={isHidden(node.id)} /> ))}
diff --git a/src/tailwind.css b/src/tailwind.css index f5f6bb5..db0ab5b 100644 --- a/src/tailwind.css +++ b/src/tailwind.css @@ -72,53 +72,3 @@ pre > code { @apply absolute -left-6 top-0 opacity-0 group-hover:opacity-100 transition-opacity no-underline select-none; @apply text-gray-400 hover:text-gray-600; } - -/* Files component animations */ -@keyframes fade-in { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - -@keyframes fade-out { - from { - opacity: 1; - } - to { - opacity: 0; - } -} - -.animate-fade-in { - animation: fade-in 1000ms ease-out forwards; -} - -.animate-fade-out { - animation: fade-out 1000ms ease-out forwards; -} - -/* Slide animations use CSS custom properties for dynamic offset */ -@keyframes slide-down { - from { - transform: translateY(var(--slide-offset)); - } - to { - transform: translateY(0); - } -} - -@keyframes slide-up { - from { - transform: translateY(var(--slide-offset)); - } - to { - transform: translateY(0); - } -} - -.animate-slide { - animation: slide-down 1000ms ease-out forwards; -} From 44b5dcdda7b891717d89ab77b92c57525a404e1f Mon Sep 17 00:00:00 2001 From: Pete Koomen Date: Fri, 26 Dec 2025 00:50:06 -0800 Subject: [PATCH 15/37] move components into their own directory --- pages/{ => components}/BouncingDvdLogo.tsx | 4 ++-- pages/{ => components}/Counter.tsx | 0 {public => pages/components}/DVD_logo.svg | 0 pages/{ => components}/Files.tsx | 0 pages/{ => components}/Fire.tsx | 0 pages/{ => components}/TodoList.tsx | 0 6 files changed, 2 insertions(+), 2 deletions(-) rename pages/{ => components}/BouncingDvdLogo.tsx (97%) rename pages/{ => components}/Counter.tsx (100%) rename {public => pages/components}/DVD_logo.svg (100%) rename pages/{ => components}/Files.tsx (100%) rename pages/{ => components}/Fire.tsx (100%) rename pages/{ => components}/TodoList.tsx (100%) diff --git a/pages/BouncingDvdLogo.tsx b/pages/components/BouncingDvdLogo.tsx similarity index 97% rename from pages/BouncingDvdLogo.tsx rename to pages/components/BouncingDvdLogo.tsx index 7419cfc..7938642 100644 --- a/pages/BouncingDvdLogo.tsx +++ b/pages/components/BouncingDvdLogo.tsx @@ -139,11 +139,11 @@ export default function BouncingDvdLogo() { width: "100%", height: "100%", backgroundColor: color, - maskImage: "url(/DVD_logo.svg)", + maskImage: "url(/components/DVD_logo.svg)", maskSize: "contain", maskRepeat: "no-repeat", maskPosition: "center", - WebkitMaskImage: "url(/DVD_logo.svg)", + WebkitMaskImage: "url(/components/DVD_logo.svg)", WebkitMaskSize: "contain", WebkitMaskRepeat: "no-repeat", WebkitMaskPosition: "center", diff --git a/pages/Counter.tsx b/pages/components/Counter.tsx similarity index 100% rename from pages/Counter.tsx rename to pages/components/Counter.tsx diff --git a/public/DVD_logo.svg b/pages/components/DVD_logo.svg similarity index 100% rename from public/DVD_logo.svg rename to pages/components/DVD_logo.svg diff --git a/pages/Files.tsx b/pages/components/Files.tsx similarity index 100% rename from pages/Files.tsx rename to pages/components/Files.tsx diff --git a/pages/Fire.tsx b/pages/components/Fire.tsx similarity index 100% rename from pages/Fire.tsx rename to pages/components/Fire.tsx diff --git a/pages/TodoList.tsx b/pages/components/TodoList.tsx similarity index 100% rename from pages/TodoList.tsx rename to pages/components/TodoList.tsx From d615f45b9f9004f869498576701843353241bbe0 Mon Sep 17 00:00:00 2001 From: Pete Koomen Date: Fri, 26 Dec 2025 00:53:32 -0800 Subject: [PATCH 16/37] add view command to index.mdx --- pages/index.mdx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pages/index.mdx b/pages/index.mdx index 20470d4..2724813 100644 --- a/pages/index.mdx +++ b/pages/index.mdx @@ -107,19 +107,20 @@ scratch dev # Build for production scratch build -scratch build --no-ssg # disable server-side generation -scratch build --development # unminified, with source maps # Preview production build locally scratch preview +# Serve target file/directory on dev server +scratch view path + # Remove build artifacts scratch clean # Revert a file to its template version -scratch get [file] # revert a file to its template version -scratch get --force [file] # overwrite without confirmation -scratch get --list # list available template files +scratch checkout [file] # revert a file to its template version +scratch checkout --force [file] # overwrite without confirmation +scratch checkout --list # list available template files # Update scratch to latest version scratch update From 26b291ab112184796c350ab364becf9cab5436a4 Mon Sep 17 00:00:00 2001 From: Peter Koomen Date: Thu, 25 Dec 2025 11:56:17 -0800 Subject: [PATCH 17/37] make it clear that dvd logo component is clickable --- pages/components/BouncingDvdLogo.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/components/BouncingDvdLogo.tsx b/pages/components/BouncingDvdLogo.tsx index 7938642..ec3cf37 100644 --- a/pages/components/BouncingDvdLogo.tsx +++ b/pages/components/BouncingDvdLogo.tsx @@ -125,7 +125,7 @@ export default function BouncingDvdLogo() { {/* Bouncing logo */}
Date: Sun, 28 Dec 2025 00:35:58 -0800 Subject: [PATCH 18/37] new styling, header and footer --- pages/about.mdx | 6 + pages/docs.mdx | 6 + pages/index.mdx | 11 +- public/scratch.svg | 213 ++++++++++++++++++++++++++++++++ src/Footer.jsx | 12 ++ src/Header.jsx | 21 ++++ src/PageWrapper.jsx | 15 +-- src/ScratchBadge.jsx | 13 ++ src/{tailwind.css => index.css} | 10 ++ 9 files changed, 293 insertions(+), 14 deletions(-) create mode 100644 pages/about.mdx create mode 100644 pages/docs.mdx create mode 100644 public/scratch.svg create mode 100644 src/Footer.jsx create mode 100644 src/Header.jsx create mode 100644 src/ScratchBadge.jsx rename src/{tailwind.css => index.css} (88%) diff --git a/pages/about.mdx b/pages/about.mdx new file mode 100644 index 0000000..1e3319e --- /dev/null +++ b/pages/about.mdx @@ -0,0 +1,6 @@ +--- +title: About +description: About Scratch +--- + +# About diff --git a/pages/docs.mdx b/pages/docs.mdx new file mode 100644 index 0000000..5076ad8 --- /dev/null +++ b/pages/docs.mdx @@ -0,0 +1,6 @@ +--- +title: Docs +description: Documentation for Scratch +--- + +# Documentation diff --git a/pages/index.mdx b/pages/index.mdx index 2724813..390270a 100644 --- a/pages/index.mdx +++ b/pages/index.mdx @@ -1,17 +1,22 @@ --- title: "Scratch" -description: "A CLI for building static MDX websites with Bun" +description: "Write with Markdown and React" keywords: ["MDX", "static site", "React", "Bun", "markdown"] author: "Scratch" type: "website" lang: "en" --- -
+
Scratch
-Scratch compiles Markdown and React into beautiful static websites that can be hosted anywhere. +Scratch is a tool for writing with [Markdown](https://daringfireball.net/projects/markdown/) and [React](https://react.dev/). + +Write in Markdown and embed React components right in your text. Scratch compiles your work into beautiful static websites like this one. + +Scratch was designed for collaborative writing with coding agents like [Claude Code](https://www.claude.com/product/claude-code). Use your favorite editor to write in Markdown, and ask a coding agent for help when it's easier to express yourself with code. + ## Quick Start diff --git a/public/scratch.svg b/public/scratch.svg new file mode 100644 index 0000000..afaa536 --- /dev/null +++ b/public/scratch.svg @@ -0,0 +1,213 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Footer.jsx b/src/Footer.jsx new file mode 100644 index 0000000..84748da --- /dev/null +++ b/src/Footer.jsx @@ -0,0 +1,12 @@ +import ScratchBadge from './ScratchBadge'; + +export default function Footer() { + return ( +
+ +

+ MIT License · © 2025 Pete Koomen +

+
+ ); +} diff --git a/src/Header.jsx b/src/Header.jsx new file mode 100644 index 0000000..f081cf3 --- /dev/null +++ b/src/Header.jsx @@ -0,0 +1,21 @@ +export default function Header() { + return ( +
+ + Scratch + + + +
+ ); +} diff --git a/src/PageWrapper.jsx b/src/PageWrapper.jsx index 7fcdfe6..40df948 100644 --- a/src/PageWrapper.jsx +++ b/src/PageWrapper.jsx @@ -1,4 +1,6 @@ import React from 'react'; +import Header from './Header'; +import Footer from './Footer'; /** * A simple wrapper applied to every page in the demo project. Feel free to @@ -8,18 +10,9 @@ import React from 'react'; export default function PageWrapper({ children }) { return (
- +
{children} -
- MIT License · © 2025 Pete Koomen -
+
); } diff --git a/src/ScratchBadge.jsx b/src/ScratchBadge.jsx new file mode 100644 index 0000000..e78c853 --- /dev/null +++ b/src/ScratchBadge.jsx @@ -0,0 +1,13 @@ +export default function ScratchBadge() { + return ( + + Made from + Scratch + + ); +} diff --git a/src/tailwind.css b/src/index.css similarity index 88% rename from src/tailwind.css rename to src/index.css index db0ab5b..3789800 100644 --- a/src/tailwind.css +++ b/src/index.css @@ -11,6 +11,16 @@ * Custom prose overrides for elements not fully styled by @tailwindcss/typography */ +/* Links - only underline on hover */ +.prose :where(a):not(:where([class~='not-prose'] *)) { + @apply no-underline hover:underline; +} + +/* Headings - add top margin and center */ +.prose :where(h1, h2, h3, h4, h5, h6):not(:where([class~='not-prose'] *)) { + @apply text-center mt-24; +} + /* Inline code - add background and padding, remove backticks */ .prose :where(code):not(:where([class~='not-prose'], pre *)) { @apply text-gray-900 font-medium text-[0.9em] bg-gray-100 px-1.5 py-0.5 rounded; From 7c22d348a0befacd277584e5fff2fca7a339c4b4 Mon Sep 17 00:00:00 2001 From: Peter Koomen Date: Sun, 28 Dec 2025 00:53:06 -0800 Subject: [PATCH 19/37] fix header styling --- pages/index.mdx | 6 +++--- src/index.css | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pages/index.mdx b/pages/index.mdx index 390270a..7b3aa08 100644 --- a/pages/index.mdx +++ b/pages/index.mdx @@ -13,13 +13,15 @@ lang: "en" Scratch is a tool for writing with [Markdown](https://daringfireball.net/projects/markdown/) and [React](https://react.dev/). -Write in Markdown and embed React components right in your text. Scratch compiles your work into beautiful static websites like this one. +Write in Markdown and embed React components right in your text. Scratch compiles your work into beautiful static websites like [this one](https://github.com/scratch/scratch.dev). Scratch was designed for collaborative writing with coding agents like [Claude Code](https://www.claude.com/product/claude-code). Use your favorite editor to write in Markdown, and ask a coding agent for help when it's easier to express yourself with code. ## Quick Start +Scratch requires no configuration so it's easy to get started + ```bash # Install scratch curl -fsSL https://scratch.dev/install.sh | bash @@ -37,8 +39,6 @@ Scratch lets you write in Markdown and embed interactive React components, like -Scratch was designed for collaborative writing with coding agents like [Claude Code](https://www.claude.com/product/claude-code). Use your favorite editor to write in Markdown, and ask a coding agent for help when it's easier to express yourself with code. - You can use React components to style text or embed fully working demos in your product specs: diff --git a/src/index.css b/src/index.css index 3789800..d57654e 100644 --- a/src/index.css +++ b/src/index.css @@ -16,8 +16,8 @@ @apply no-underline hover:underline; } -/* Headings - add top margin and center */ -.prose :where(h1, h2, h3, h4, h5, h6):not(:where([class~='not-prose'] *)) { +/* H1 - center and add extra top margin */ +.prose :where(h1):not(:where([class~='not-prose'] *)) { @apply text-center mt-24; } From 837e6b889e76a61eb7ffbe2afe767f314dcb1f80 Mon Sep 17 00:00:00 2001 From: Peter Koomen Date: Sun, 28 Dec 2025 01:01:26 -0800 Subject: [PATCH 20/37] clean up todo list --- pages/components/TodoList.tsx | 60 +++++++++++++++++------------------ pages/index.mdx | 4 +-- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/pages/components/TodoList.tsx b/pages/components/TodoList.tsx index a1085ab..878f38d 100644 --- a/pages/components/TodoList.tsx +++ b/pages/components/TodoList.tsx @@ -8,6 +8,13 @@ interface Todo { const STORAGE_KEY = "scratch-demo-todos"; +const DEFAULT_TODOS: Todo[] = [ + { id: 1, text: "Create scratch project", completed: false }, + { id: 2, text: "Edit pages/index.mdx", completed: false }, + { id: 3, text: "Build with `scratch build`", completed: false }, + { id: 4, text: "Publish!", completed: false }, +]; + let globalTodos: Todo[] | null = null; let listeners: Set<(todos: Todo[]) => void> = new Set(); @@ -17,7 +24,7 @@ function getTodos(): Todo[] { globalTodos = []; } else { const stored = localStorage.getItem(STORAGE_KEY); - globalTodos = stored ? JSON.parse(stored) : []; + globalTodos = stored ? JSON.parse(stored) : DEFAULT_TODOS; } } return globalTodos; @@ -89,30 +96,9 @@ export default function TodoList() { return (
-
- setInput(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && handleAdd()} - placeholder="Add a todo..." - className="flex-1 px-3 py-2 border border-gray-300 rounded-md bg-white text-gray-900 placeholder-gray-400" - /> - -
- - {todos.length === 0 ? ( -

- No todos yet. Add one above! -

- ) : ( -
    - {todos.map((todo) => ( +

    Todo List Demo

    +
      + {todos.map((todo) => (
    • ))} -
    - )} - -
    +
  • + + setInput(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleAdd()} + placeholder="Add a todo..." + className="flex-1 bg-transparent border-b border-gray-300 text-gray-900 placeholder-gray-400 focus:outline-none focus:border-gray-500" + /> +
  • +
+ +
{todos.filter((t) => !t.completed).length} remaining diff --git a/pages/index.mdx b/pages/index.mdx index 7b3aa08..edad553 100644 --- a/pages/index.mdx +++ b/pages/index.mdx @@ -13,7 +13,7 @@ lang: "en" Scratch is a tool for writing with [Markdown](https://daringfireball.net/projects/markdown/) and [React](https://react.dev/). -Write in Markdown and embed React components right in your text. Scratch compiles your work into beautiful static websites like [this one](https://github.com/scratch/scratch.dev). +Write in Markdown and embed React components right in your text. Scratch compiles your work into beautiful static websites (like [this one](https://github.com/scratch/scratch.dev)) that can be hosted anywhere. Scratch was designed for collaborative writing with coding agents like [Claude Code](https://www.claude.com/product/claude-code). Use your favorite editor to write in Markdown, and ask a coding agent for help when it's easier to express yourself with code. @@ -35,7 +35,7 @@ scratch dev ## What can you do with Scratch? -Scratch lets you write in Markdown and embed interactive React components, like this counter: +Scratch embed interactive React components into your writing, like this counter: From 08f0d8fbb8536c74a6e099fcf622abf8da073ebb Mon Sep 17 00:00:00 2001 From: Peter Koomen Date: Sun, 28 Dec 2025 01:02:39 -0800 Subject: [PATCH 21/37] more todo cleanup --- pages/components/TodoList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/components/TodoList.tsx b/pages/components/TodoList.tsx index 878f38d..3a031df 100644 --- a/pages/components/TodoList.tsx +++ b/pages/components/TodoList.tsx @@ -150,7 +150,7 @@ export default function TodoList() { onChange={(e) => setInput(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleAdd()} placeholder="Add a todo..." - className="flex-1 bg-transparent border-b border-gray-300 text-gray-900 placeholder-gray-400 focus:outline-none focus:border-gray-500" + className="flex-1 bg-transparent border-b border-transparent text-gray-900 placeholder-gray-400 focus:outline-none focus:border-gray-300" /> From 123d2cd29f84e56c710952812e0090551335f05c Mon Sep 17 00:00:00 2001 From: Peter Koomen Date: Sun, 28 Dec 2025 01:54:07 -0800 Subject: [PATCH 22/37] more docs progress --- pages/components/TodoList.tsx | 2 +- pages/docs.mdx | 364 +++++++++++++++++++++++++++++++++- pages/index.mdx | 83 +------- 3 files changed, 369 insertions(+), 80 deletions(-) diff --git a/pages/components/TodoList.tsx b/pages/components/TodoList.tsx index 3a031df..0b3ffdd 100644 --- a/pages/components/TodoList.tsx +++ b/pages/components/TodoList.tsx @@ -79,7 +79,7 @@ function useTodos() { }; const reset = () => { - updateTodos([]); + updateTodos([...DEFAULT_TODOS]); }; return { todos, addTodo, toggleTodo, deleteTodo, reset }; diff --git a/pages/docs.mdx b/pages/docs.mdx index 5076ad8..85d55d6 100644 --- a/pages/docs.mdx +++ b/pages/docs.mdx @@ -1,6 +1,366 @@ --- -title: Docs -description: Documentation for Scratch +title: Documentation +description: Complete documentation for the Scratch CLI --- # Documentation + +## Quick Start + +Scratch requires no configuration so it's easy to get started: + +```bash +# Install scratch +curl -fsSL https://scratch.dev/install.sh | bash + +# Create a new project +scratch create + +# Start the dev server +scratch dev +``` + +Now you're ready to start writing in `pages/index.mdx`. + +## Project Structure + +Scratch uses an opinionated project structure and requires **no boilerplate or configuration**: just create a project, run the dev server, and start writing. + +A simple Scratch project (created with `scratch create`) looks like this: + + +``` +my-scratch-project/ + pages/ # put markdown and components here + posts/ (collapsed) + post1.md # accessible at /posts/post1 + post2.md # /posts/post2 + blog.png # static assets in pages/ or public/ + index.mdx # accessible at root path (/) + Counter.tsx + public/ (collapsed) # static assets + favicon.svg + src/ (collapsed) # global components and css + PageWrapper.jsx # wraps every page + tailwind.css # global styles + markdown/ (collapsed) # default markdown components + index.ts + CodeBlock.tsx + Heading.tsx + Link.tsx + AGENTS.md # agent context + package.json # dependencies + .gitignore +``` + + +Use `scratch build` to compile this project into a static website, like [this one](https://github.com/scratch/scratch.dev). + +### pages/ + +Markdown files live in `pages/` and can be either MDX (`.mdx`) or vanilla Markdown (`.md`). + +Scratch compiles Markdown file into a static web page whose route is determined by your project's directory structure: +- `pages/index.mdx` will be served at the root path (`/`) +- `pages/posts/post1.md` will be served at `pages/posts/post1` + +Component files and libraries can live anywhere in `pages/` and `src/`. They are auto-detected by Scratch and don't need to be explicitly imported in your Markdown files as long as the component name (`Counter`) matches the component name (`Counter.jsx` or `Counter.tsx`). + +`pages/` can also contain static content like images. + + +Modify `src/tailwind.css` to change the styling of your document. Add headers, footers and other site-wide elements by modifying `src/PageWrapper.jsx`. + +### URL Path Resolution + +| File | URL | +|------|-----| +| `pages/index.mdx` | `/` | +| `pages/about.mdx` | `/about/` | +| `pages/posts/index.mdx` | `/posts/` | +| `pages/posts/hello.mdx` | `/posts/hello/` | + +## Commands + +### scratch create + +Create a new Scratch project. + +```bash +scratch create [path] +``` + +**Options:** +- `--no-src` - Skip the src/ template directory +- `--no-package` - Skip package.json template +- `--no-example` - Skip example content files + +### scratch dev + +Start the development server with live reload. + +```bash +scratch dev [path] +``` + +**Options:** +- `-p, --port ` - Port for dev server (default: 5173) +- `-n, --no-open` - Don't open browser automatically +- `-d, --development` - Development mode (unminified, with source maps) +- `-b, --base ` - Base path for deployment (e.g., `/mysite/`) +- `--static ` - Static file mode: `public`, `assets`, `all` +- `--strict` - Disable auto-injection of PageWrapper and imports +- `--highlight ` - Syntax highlighting: `off`, `popular`, `auto`, `all` + +### scratch build + +Build the project for production. + +```bash +scratch build [path] +``` + +**Options:** +- `-o, --out-dir ` - Output directory (default: `dist`) +- `-d, --development` - Development mode (unminified, with source maps) +- `-b, --base ` - Base path for deployment +- `--no-ssg` - Disable static site generation +- `--static ` - Static file mode: `public`, `assets`, `all` +- `--strict` - Disable auto-injection of PageWrapper and imports +- `--highlight ` - Syntax highlighting mode + +### scratch preview + +Preview the production build locally. + +```bash +scratch preview [path] +``` + +**Options:** +- `-p, --port ` - Port for preview server (default: 4173) +- `-n, --no-open` - Don't open browser automatically + +### scratch view + +Quick preview of a single file or directory. + +```bash +scratch view +``` + +**Options:** +- `-p, --port ` - Port for dev server (default: 5173) +- `-n, --no-open` - Don't open browser automatically + +Useful for viewing individual `.md` or `.mdx` files without setting up a full project. + +### scratch clean + +Remove build artifacts. + +```bash +scratch clean [path] +``` + +Removes the `dist/` and `.scratch-build-cache/` directories. + +### scratch checkout + +Restore template files from built-in templates. + +```bash +scratch checkout [file] +``` + +**Options:** +- `-l, --list` - List all available template files +- `-f, --force` - Overwrite existing files without confirmation + +### scratch update + +Update Scratch to the latest version. + +```bash +scratch update +``` + +### Global Options + +These options work with all commands: + +- `-v, --verbose` - Verbose output for debugging +- `-q, --quiet` - Quiet mode (errors only) +- `--show-bun-errors` - Show full Bun error stack traces +- `--version` - Show CLI version + +## Writing Content + +### Frontmatter + +Add YAML frontmatter to set page metadata: + +```mdx +--- +title: My Page +description: A description for SEO +image: /og-image.png +keywords: ["react", "markdown"] +author: Your Name +--- +``` + +These are automatically injected as HTML meta tags. + +### Using Components + +Components in `src/` or `pages/` are automatically available in MDX files: + +```mdx +# My Page + + + + +``` + +The component filename must match the component name (e.g., `Counter.tsx` exports `Counter`). + +### Component Patterns + +**Self-closing** (wrapped in `not-prose` div): +```mdx + +``` + +**Inline children:** +```mdx + +``` + +**Block children** (requires blank lines): +```mdx + + +## Warning + +This is **markdown** inside a component. + + +``` + +## Styling + +### Tailwind CSS + +Scratch uses Tailwind CSS. Edit `src/tailwind.css` for global styles: + +```css +@import 'tailwindcss'; +@plugin "@tailwindcss/typography"; + +/* Custom styles */ +.prose a { + @apply text-blue-600 hover:text-blue-800; +} +``` + +### Typography + +Markdown content is styled with [Tailwind Typography](https://github.com/tailwindlabs/tailwindcss-typography). Use prose modifiers in `PageWrapper.jsx`: + +```jsx +
+ {children} +
+``` + +Element modifiers: `prose-h1:`, `prose-a:`, `prose-p:`, `prose-code:`, `prose-pre:`, etc. + +### Excluding from Prose + +Use the `not-prose` class to exclude elements from typography styling: + +```jsx +
+ +
+``` + +## PageWrapper + +Create `src/PageWrapper.jsx` to wrap all pages with a layout: + +```jsx +export default function PageWrapper({ children }) { + return ( +
+ +
{children}
+
...
+
+ ); +} +``` + +## Custom Markdown Components + +Override default markdown rendering by creating components in `src/markdown/`: + +```tsx +// src/markdown/Link.tsx +export default function Link({ href, children, ...props }) { + const isExternal = href?.startsWith('http'); + return ( + + {children} + + ); +} +``` + +Export from `src/markdown/index.ts`: + +```ts +export { default as a } from './Link'; +export { default as pre } from './CodeBlock'; +export { default as h1, default as h2 } from './Heading'; +``` + +## Syntax Highlighting + +Code blocks are highlighted with [Shiki](https://shiki.style/). Control highlighting with the `--highlight` flag: + +- `off` - No syntax highlighting +- `popular` - Common languages only +- `auto` - Detect from code blocks (default) +- `all` - All languages + +## Deployment + +Build your site and deploy the `dist/` folder to any static host: + +```bash +scratch build +``` + +For subdirectory deployment, use the `--base` flag: + +```bash +scratch build --base /my-site/ +``` + +## Generated Files + +Add these to `.gitignore`: + +``` +dist/ +.scratch-build-cache/ +node_modules/ +``` diff --git a/pages/index.mdx b/pages/index.mdx index edad553..9be5ef6 100644 --- a/pages/index.mdx +++ b/pages/index.mdx @@ -20,7 +20,7 @@ Scratch was designed for collaborative writing with coding agents like [Claude C ## Quick Start -Scratch requires no configuration so it's easy to get started +Scratch requires no configuration so it's easy to get started: ```bash # Install scratch @@ -33,17 +33,19 @@ scratch create scratch dev ``` +Now you're ready to start writing in `pages/index.mdx`. + ## What can you do with Scratch? -Scratch embed interactive React components into your writing, like this counter: +Scratch lets you embed interactive React components into your writing, like this counter: -You can use React components to style text or embed fully working demos in your product specs: +You can use components to style text or embed fully working demos in your product specs: -Scratch uses [Tailwind Typography](https://github.com/tailwindlabs/tailwindcss-typography), to render your prose with a clean aesthetic. Code blocks use syntax highlighting by [Shiki](https://shiki.style/). +Scratch uses [Tailwind Typography](https://github.com/tailwindlabs/tailwindcss-typography) to render your prose with a clean aesthetic. It supports [Github-flavored Markdown](https://github.com/remarkjs/remark-gfm) out of the box. Code blocks use syntax highlighting by [Shiki](https://shiki.style/). ```python def greet(name: str) -> str: @@ -52,85 +54,12 @@ def greet(name: str) -> str: print(greet("World")) ``` -Scratch also supports Github-flavored Markdown features like checklists and tables: - -| Feature | Supported? | -|---------|-----------| -| Compiles Markdown, TS, JS & CSS | ✅ | -| Dev server with HMR | ✅ | -| Tailwind CSS styling | ✅ | -| Code syntax highlighting | ✅ | - Unlike traditional word processors, Scratch makes it easy to express any idea. If you can describe it to a coding agent, you can add it to your document: Collaborating with AI makes writing more fun. Scratch makes that easy. -## No Boilerplate - -Scratch uses an opinionated project structure and requires **no boilerplate or configuration**: just create a project, run the dev server with `scratch dev`, and start writing. - -A simple Scratch project (created with `scratch create`) looks like this: - - - - -Use `scratch build` to compile this project into a static website, like [this one](https://github.com/scratch/scratch.dev). - -Component files and libraries can live anywhere in `pages/` and `src/`. They are auto-detected by Scratch and don't need to be explicitly imported in your .mdx files as long as the filename matches the component name. - -Modify `src/tailwind.css` directory to change the styling of your document. Add headers, footers and other site-wide elements by modifying `src/PageWrapper.jsx`. - -## Commands - -```bash -# Create a new project -scratch create [path] # create project - -# Start dev server with hot module reloading -scratch dev - -# Build for production -scratch build - -# Preview production build locally -scratch preview - -# Serve target file/directory on dev server -scratch view path - -# Remove build artifacts -scratch clean - -# Revert a file to its template version -scratch checkout [file] # revert a file to its template version -scratch checkout --force [file] # overwrite without confirmation -scratch checkout --list # list available template files - -# Update scratch to latest version -scratch update -``` - ## Acknowledgements From a42ab574ea5909de656314d15e34eed24c3ba6ef Mon Sep 17 00:00:00 2001 From: Peter Koomen Date: Sun, 28 Dec 2025 02:19:00 -0800 Subject: [PATCH 23/37] finish docs --- pages/about.mdx | 6 ----- pages/docs.mdx | 65 +++++++++++++++++++++++++++++++------------------ pages/index.mdx | 1 + src/Header.jsx | 1 - src/index.css | 2 +- 5 files changed, 43 insertions(+), 32 deletions(-) delete mode 100644 pages/about.mdx diff --git a/pages/about.mdx b/pages/about.mdx deleted file mode 100644 index 1e3319e..0000000 --- a/pages/about.mdx +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: About -description: About Scratch ---- - -# About diff --git a/pages/docs.mdx b/pages/docs.mdx index 85d55d6..7787141 100644 --- a/pages/docs.mdx +++ b/pages/docs.mdx @@ -1,6 +1,11 @@ --- -title: Documentation -description: Complete documentation for the Scratch CLI +title: "Scratch - Documentation" +image: "/scratch-logo.svg" +description: "Complete documentation for the Scratch CLI" +keywords: ["MDX", "static site", "React", "Bun", "markdown"] +author: "Scratch" +type: "website" +lang: "en" --- # Documentation @@ -26,7 +31,7 @@ Now you're ready to start writing in `pages/index.mdx`. Scratch uses an opinionated project structure and requires **no boilerplate or configuration**: just create a project, run the dev server, and start writing. -A simple Scratch project (created with `scratch create`) looks like this: +A simple Scratch project (created with `scratch create my-scratch-project`) looks like this: ``` @@ -41,13 +46,13 @@ my-scratch-project/ public/ (collapsed) # static assets favicon.svg src/ (collapsed) # global components and css - PageWrapper.jsx # wraps every page - tailwind.css # global styles markdown/ (collapsed) # default markdown components index.ts CodeBlock.tsx Heading.tsx Link.tsx + PageWrapper.jsx # wraps every page + tailwind.css # global styles AGENTS.md # agent context package.json # dependencies .gitignore @@ -61,24 +66,33 @@ Use `scratch build` to compile this project into a static website, like [this on Markdown files live in `pages/` and can be either MDX (`.mdx`) or vanilla Markdown (`.md`). Scratch compiles Markdown file into a static web page whose route is determined by your project's directory structure: -- `pages/index.mdx` will be served at the root path (`/`) -- `pages/posts/post1.md` will be served at `pages/posts/post1` +| File | URL | +|------|-----| +| `pages/index.mdx` | `/` | +| `pages/about.mdx` | `/about/` | +| `pages/posts/index.mdx` | `/posts/` | +| `pages/posts/hello.mdx` | `/posts/hello/` | Component files and libraries can live anywhere in `pages/` and `src/`. They are auto-detected by Scratch and don't need to be explicitly imported in your Markdown files as long as the component name (`Counter`) matches the component name (`Counter.jsx` or `Counter.tsx`). -`pages/` can also contain static content like images. +`pages/` can also contain static content like images. These are copied directly into your output directory (`dist/`) and will be served the same way your compiled Markdown is. +### public/ -Modify `src/tailwind.css` to change the styling of your document. Add headers, footers and other site-wide elements by modifying `src/PageWrapper.jsx`. +Add static content like a favicon or `_redirects` file to the `public/` directory. These are copied directly into your output directory (`dist/`) just like static content in `pages/`. -### URL Path Resolution +### src/ -| File | URL | -|------|-----| -| `pages/index.mdx` | `/` | -| `pages/about.mdx` | `/about/` | -| `pages/posts/index.mdx` | `/posts/` | -| `pages/posts/hello.mdx` | `/posts/hello/` | +`src/` is for CSS files, components and JS/TS libraries. New Scratch projects will contain the following: + +- `src/PageWrapper.jsx` - your Markdown contents will be "wrapped" with this component. Modify it to change page headers, footers, nav bars and other global components. +- `src/tailwind.css` - global styles. Edit this to change the look and feel of your compiled Markdown. +- `src/markdown` - a directory containing default Markdown components. For example, edit `src/markdown/CodeBlock.tsx` to change how compiled code blocks + + +### package.json + +Scratch automatically installs build dependences. You can add additional dependencies by editing `package.json`. ## Commands @@ -90,7 +104,7 @@ Create a new Scratch project. scratch create [path] ``` -**Options:** +Options: - `--no-src` - Skip the src/ template directory - `--no-package` - Skip package.json template - `--no-example` - Skip example content files @@ -103,14 +117,17 @@ Start the development server with live reload. scratch dev [path] ``` -**Options:** +Options: - `-p, --port ` - Port for dev server (default: 5173) - `-n, --no-open` - Don't open browser automatically - `-d, --development` - Development mode (unminified, with source maps) - `-b, --base ` - Base path for deployment (e.g., `/mysite/`) -- `--static ` - Static file mode: `public`, `assets`, `all` +- `--static ` - Static file mode: + - `public` - ignore static assets in `pages/` + - `assets` (default) - serve assets like images, but not javascript or typescript code files like `lib.js` + - `all` - serve assets _and_ code files statically - `--strict` - Disable auto-injection of PageWrapper and imports -- `--highlight ` - Syntax highlighting: `off`, `popular`, `auto`, `all` +- `--highlight ` - Syntax highlighting supported languages: `off`, `popular`, `auto` (default), `all` ### scratch build @@ -120,7 +137,7 @@ Build the project for production. scratch build [path] ``` -**Options:** +Options: - `-o, --out-dir ` - Output directory (default: `dist`) - `-d, --development` - Development mode (unminified, with source maps) - `-b, --base ` - Base path for deployment @@ -143,13 +160,13 @@ scratch preview [path] ### scratch view -Quick preview of a single file or directory. +Quick preview of a single file or directory. Handy for e.g. reading `README.md` files. ```bash scratch view ``` -**Options:** +Options: - `-p, --port ` - Port for dev server (default: 5173) - `-n, --no-open` - Don't open browser automatically @@ -173,7 +190,7 @@ Restore template files from built-in templates. scratch checkout [file] ``` -**Options:** +Options: - `-l, --list` - List all available template files - `-f, --force` - Overwrite existing files without confirmation diff --git a/pages/index.mdx b/pages/index.mdx index 9be5ef6..f1531e1 100644 --- a/pages/index.mdx +++ b/pages/index.mdx @@ -1,5 +1,6 @@ --- title: "Scratch" +image: "/scratch-logo.svg" description: "Write with Markdown and React" keywords: ["MDX", "static site", "React", "Bun", "markdown"] author: "Scratch" diff --git a/src/Header.jsx b/src/Header.jsx index f081cf3..a2c64d9 100644 --- a/src/Header.jsx +++ b/src/Header.jsx @@ -6,7 +6,6 @@ export default function Header() {
diff --git a/src/index.css b/src/index.css index d57654e..b081768 100644 --- a/src/index.css +++ b/src/index.css @@ -23,7 +23,7 @@ /* Inline code - add background and padding, remove backticks */ .prose :where(code):not(:where([class~='not-prose'], pre *)) { - @apply text-gray-900 font-medium text-[0.9em] bg-gray-100 px-1.5 py-0.5 rounded; + @apply text-gray-700 font-medium text-[0.9em] bg-gray-50 px-1.5 py-0.5 rounded; } .prose :where(code):not(:where([class~='not-prose'], pre *))::before, From 4fd54478c816761ae20e1a25fbd7a9dc7831c1eb Mon Sep 17 00:00:00 2001 From: Peter Koomen Date: Sun, 28 Dec 2025 02:22:01 -0800 Subject: [PATCH 24/37] readme --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..38eedd1 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +

+ Scratch +

+ +# scratch.dev + +Website for the [Scratch](https://scratch.dev) project From 0ff7e7865418609391c09ae3b8c4eef438d1b060 Mon Sep 17 00:00:00 2001 From: Peter Koomen Date: Sun, 28 Dec 2025 02:35:51 -0800 Subject: [PATCH 25/37] add docs sidebar --- pages/components/DocsSidebar.tsx | 76 ++++++++++++++++++++++++++++++++ pages/docs.mdx | 61 +++++++++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 pages/components/DocsSidebar.tsx diff --git a/pages/components/DocsSidebar.tsx b/pages/components/DocsSidebar.tsx new file mode 100644 index 0000000..c7a7bed --- /dev/null +++ b/pages/components/DocsSidebar.tsx @@ -0,0 +1,76 @@ +import React, { useState, useEffect } from "react"; + +interface TocItem { + id: string; + text: string; +} + +export default function DocsSidebar() { + const [headings, setHeadings] = useState([]); + const [activeId, setActiveId] = useState(""); + + useEffect(() => { + // Find all h2 elements on the page + const h2Elements = document.querySelectorAll("h2"); + const items: TocItem[] = []; + + h2Elements.forEach((h2) => { + // Generate id from text if not present + if (!h2.id) { + h2.id = h2.textContent + ?.toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/(^-|-$)/g, "") || ""; + } + items.push({ + id: h2.id, + text: h2.textContent || "", + }); + }); + + setHeadings(items); + }, []); + + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + setActiveId(entry.target.id); + } + }); + }, + { rootMargin: "-80px 0px -80% 0px" } + ); + + headings.forEach(({ id }) => { + const el = document.getElementById(id); + if (el) observer.observe(el); + }); + + return () => observer.disconnect(); + }, [headings]); + + if (headings.length === 0) return null; + + return ( +
+ ); +} diff --git a/pages/docs.mdx b/pages/docs.mdx index 7787141..0f9ec2f 100644 --- a/pages/docs.mdx +++ b/pages/docs.mdx @@ -10,6 +10,8 @@ lang: "en" # Documentation + + ## Quick Start Scratch requires no configuration so it's easy to get started: @@ -358,6 +360,65 @@ Code blocks are highlighted with [Shiki](https://shiki.style/). Control highligh - `auto` - Detect from code blocks (default) - `all` - All languages +## Build Pipeline + +When you run `scratch build`, Scratch compiles your MDX files into a **fully static website**. There's no server-side code running in production—everything is pre-rendered HTML, CSS, and JavaScript that can be hosted on any static file server. + +Scratch supports defaults that make it easy to create beautiful & functional websites with your writing. Here's a description of the Scratch build pipeline: + +### Build Steps + +#### 1. Dependency Resolution + +Scratch auto-installs npm dependencies from your `package.json` using Bun. Dependencies are cached in `.scratch-build-cache/` for faster subsequent builds. + +#### 2. Entry Generation + +For each `.mdx` or `.md` file in `pages/`, Scratch generates client and server entry files that import your content and wire up React rendering. + +#### 3. MDX Compilation + +MDX files are compiled through a pipeline of remark and rehype plugins: + +**Remark plugins** (process Markdown AST): +- **remark-gfm** - GitHub Flavored Markdown (tables, strikethrough, task lists) +- **remark-frontmatter** - Extracts YAML frontmatter for page metadata +- **remark-auto-import** - Automatically imports components used in MDX without explicit import statements +- **remark-not-prose** - Wraps self-closing components in `not-prose` divs to prevent Tailwind Typography styles from affecting them + +**Rehype plugins** (process HTML AST): +- **rehype-raw** - Allows raw HTML in Markdown while preserving MDX nodes +- **rehype-image-paths** - Transforms relative image paths to absolute routes, handles base paths for subdirectory deployments +- **rehype-shiki** - Syntax highlighting using Shiki with configurable language detection +- **rehype-footnotes** - Moves footnotes inside PageWrapper for proper styling + +#### 4. Tailwind Compilation + +Scratch compiles your CSS using Tailwind CSS v4. Only the utilities actually used in your content are included in the final bundle. + +#### 5. Server Build + +Scratch builds a server bundle to pre-render each page. Scratch generats static website and bundle is only used during the build process--it doesn't actually run on a server. + +#### 6. Client Build + +The client JavaScript is bundled with Bun's bundler. Output is minified for production with content-hashed filenames for cache busting. + +#### 7. HTML Generation + +Each page is rendered to static HTML with: +- Pre-rendered content from the server build +- Injected CSS and JS bundle references +- Meta tags from frontmatter (title, description, og:image, etc.) + +#### 8. Static Assets + +Files from `public/` are copied to the output directory. + +#### 9. Output + +The final static site is written to `dist/`, ready for deployment to any static host. + ## Deployment Build your site and deploy the `dist/` folder to any static host: From 881b748a5f2e1e55216b6c261280e157145eaa62 Mon Sep 17 00:00:00 2001 From: Peter Koomen Date: Sun, 28 Dec 2025 02:39:56 -0800 Subject: [PATCH 26/37] fix sidebar --- pages/components/DocsSidebar.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/pages/components/DocsSidebar.tsx b/pages/components/DocsSidebar.tsx index c7a7bed..5f6cc6d 100644 --- a/pages/components/DocsSidebar.tsx +++ b/pages/components/DocsSidebar.tsx @@ -22,13 +22,25 @@ export default function DocsSidebar() { .replace(/[^a-z0-9]+/g, "-") .replace(/(^-|-$)/g, "") || ""; } + // Get text content, filtering out the # anchor + let text = h2.textContent || ""; + text = text.replace(/^#\s*/, "").trim(); items.push({ id: h2.id, - text: h2.textContent || "", + text, }); }); setHeadings(items); + + // Scroll to hash after IDs are set + if (window.location.hash) { + const id = window.location.hash.slice(1); + const el = document.getElementById(id); + if (el) { + setTimeout(() => el.scrollIntoView(), 0); + } + } }, []); useEffect(() => { @@ -54,7 +66,7 @@ export default function DocsSidebar() { if (headings.length === 0) return null; return ( -