-
Notifications
You must be signed in to change notification settings - Fork 36
initial claude code plugin project #491
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
✅ Deploy Preview for fireproof-ai-builder ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Codex Review
Here are some automated review suggestions for this pull request.
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| // Save trick to database | ||
| await database.put({ | ||
| type: 'trick', | ||
| selectedCard: selectedCard.id, | ||
| magicCard: magicCard.id, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use updated selected card when saving trick
After a card is clicked, handleCardClick schedules performMagic with setTimeout. Because performMagic was defined in the prior render where selectedCard was null, the timer executes with stale state and this block dereferences selectedCard.id and magicCard.id, throwing before the trick completes or the result is recorded. Pass the chosen card into performMagic (or read from the timeout callback) so the function operates on the latest state.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- A runtime-breaking import typo in
fireproof-guidance.txt(use-firproof→use-fireproof) will mislead users and fail at runtime. - The month index in the date-prefix example is inconsistent with
getMonth()semantics, likely causing copy/paste bugs. - In
version-1/index.html, using array indices as React keys should be replaced with stable IDs to avoid reconciliation issues. - The viewer relies solely on keyboard events; when the iframe is focused, navigation can fail—add visible buttons as a fallback.
Summary of changes
- Added a Claude Code plugin scaffold for generating Fireproof-powered apps:
- New plugin manifest at
cli/vibes/.claude-plugin/plugin.json. - Command instruction docs:
cli/vibes/commands/vibes.mdfor single generation andcli/vibes/commands/vibes-select.mdfor multi-version parallel generation and viewer creation. - Fireproof guidance reference at
cli/vibes/resources/fireproof-guidance.txt.
- New plugin manifest at
- Included a concrete example generation output under
cli/vibes/vibes-output/20251022-094307-magic-trick/with three distinct versions and a comparisonviewer.html. - Each generated HTML app follows the specified single-file structure with React, Tailwind, Fireproof (via esm.sh), and Babel-in-browser.
| To subscribe to real-time updates, use the `subscribe` method. This is useful for building backend event handlers or other server-side logic. For instance to send an email when the user completes a todo: | ||
|
|
||
| ```js | ||
| import { fireproof } from "use-firproof"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The import path has a typo: use-firproof will fail to resolve at runtime. This breaks the guidance example and will mislead consumers trying to copy/paste. It should be use-fireproof.
Suggestion
Replace the incorrect import path with the correct use-fireproof module name.
- import { fireproof } from "use-firproof";
+ import { fireproof } from "use-fireproof";If you want, I can push a commit with this fix. Reply with "@CharlieHelps yes please" and I'll add it.
|
|
||
| ```js | ||
| const queryResult = useLiveQuery( | ||
| (doc) => [doc.date.getFullYear(), doc.date.getMonth(), doc.date.getDate()], | ||
| { prefix: [2024, 11] } | ||
| ); | ||
| ``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The example claims the prefix query loads "everything from November 2024," but Date.getMonth() is zero-based. Using 11 in the prefix corresponds to December. This mismatch will confuse readers and produce unexpected results if copied verbatim.
Suggestion
Align the example with JavaScript's zero-based months. Either adjust the comment to December or use 10 for November. For clarity, you can also normalize month to 1–12 in the index key.
Option A (keep zero-based, correct the prefix to November):
- // ...loading everything from November 2024:
+ // ...loading everything from November 2024 (getMonth() is zero-based):
const queryResult = useLiveQuery(
(doc) => [doc.date.getFullYear(), doc.date.getMonth(), doc.date.getDate()],
- { prefix: [2024, 11] }
+ { prefix: [2024, 10] }
);Option B (normalize to 1–12 months):
- (doc) => [doc.date.getFullYear(), doc.date.getMonth(), doc.date.getDate()],
+ (doc) => [doc.date.getFullYear(), doc.date.getMonth() + 1, doc.date.getDate()],
{ prefix: [2024, 11] }Reply with "@CharlieHelps yes please" if you want me to apply Option A across the file, or specify Option B and I’ll commit that instead.
| {displayedCards.map((card, index) => ( | ||
| <Card | ||
| key={index} | ||
| card={card} | ||
| onClick={() => handleCardClick(card)} | ||
| isRevealed={false} | ||
| isSelected={false} | ||
| isFlipping={false} | ||
| /> | ||
| ))} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using the array index as a React key can lead to unnecessary re-renders and subtle UI bugs if the list changes. You already have a stable card.id—use that instead.
Suggestion
Switch to a stable key derived from the data:
- {displayedCards.map((card, index) => (
- <Card
- key={index}
+ {displayedCards.map((card) => (
+ <Card
+ key={card.id}
card={card}
onClick={() => handleCardClick(card)}
isRevealed={false}
isSelected={false}
isFlipping={false}
/>
))}Reply with "@CharlieHelps yes please" and I’ll update both occurrences in this file.
| <div class="fixed top-0 left-0 right-0 bg-gradient-to-r from-purple-600 to-blue-600 text-white p-4 shadow-lg z-10"> | ||
| <div class="max-w-7xl mx-auto flex items-center justify-between"> | ||
| <div> | ||
| <h1 class="text-2xl font-bold">Vibes Version Selector</h1> | ||
| <p class="text-sm opacity-90">Use ← → arrow keys to navigate</p> | ||
| </div> | ||
| <div class="text-right"> | ||
| <p class="text-3xl font-bold" id="version-indicator">Version 1 of 3</p> | ||
| <p class="text-sm opacity-90">Press 'S' to select current version</p> | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div class="pt-24 h-screen"> | ||
| <iframe id="viewer" src="./version-1/index.html" class="w-full h-full"></iframe> | ||
| </div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keyboard navigation may not work when the iframe has focus because key events won’t bubble to the parent document. Consider adding visible Prev/Next buttons to the header and wiring them to the same logic so users can navigate reliably regardless of focus.
Suggestion
Add explicit navigation buttons and hook them up to updateVersion for better usability when the iframe has focus.
<div class="fixed top-0 left-0 right-0 bg-gradient-to-r from-purple-600 to-blue-600 text-white p-4 shadow-lg z-10">
<div class="max-w-7xl mx-auto flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold">Vibes Version Selector</h1>
<p class="text-sm opacity-90">Use ← → arrow keys to navigate</p>
</div>
<div class="text-right">
<p class="text-3xl font-bold" id="version-indicator">Version 1 of 3</p>
<p class="text-sm opacity-90">Press 'S' to select current version</p>
+ <div class="mt-2 flex gap-2 justify-end">
+ <button id="prev-btn" class="px-3 py-1 bg-white/10 rounded hover:bg-white/20">← Prev</button>
+ <button id="next-btn" class="px-3 py-1 bg-white/10 rounded hover:bg-white/20">Next →</button>
+ </div>
</div>
</div>
</div>
@@
<script>
const totalVersions = 3;
let currentVersion = 1;
@@
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft') {
updateVersion(currentVersion - 1);
} else if (e.key === 'ArrowRight') {
updateVersion(currentVersion + 1);
} else if (e.key === 's' || e.key === 'S') {
if (confirm(`Select Version ${currentVersion} as the final version?`)) {
alert(`Version ${currentVersion} selected! Return to Claude Code to complete the selection process.`);
}
}
});
+
+ // Click navigation for when the iframe has focus
+ document.getElementById('prev-btn').addEventListener('click', () => updateVersion(currentVersion - 1));
+ document.getElementById('next-btn').addEventListener('click', () => updateVersion(currentVersion + 1));
</script>Reply with "@CharlieHelps yes please" and I’ll add these buttons and hooks.
| <div class="pt-24 h-screen"> | ||
| <iframe id="viewer" src="./version-1/index.html" class="w-full h-full"></iframe> | ||
| </div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The viewer’s content area uses pt-24 h-screen, which results in the iframe being shorter than the viewport by the header’s height (the bottom gets cut off due to overflow: hidden on body). Consider sizing the content to calc(100vh - 6rem) (Tailwind h-24 ~ 6rem) so the iframe fully fits beneath the fixed header.
Suggestion
Adjust the container height to account for the fixed header:
<div class="pt-24" style="height: calc(100vh - 6rem);">
<iframe id="viewer" src="./version-1/index.html" class="w-full h-full"></iframe>
</div>Alternatively, use a fixed-positioned iframe with top offset. I can add this change for you—reply with "@CharlieHelps yes please".
| <!DOCTYPE html> | ||
| <html lang="en"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are build artifacts/generated outputs being committed into the repo. This usually increases repository size and churn, and makes reviews noisy. It’s better to exclude cli/vibes/vibes-output/ from version control and generate them on demand.
Suggestion
Add a .gitignore entry and consider moving sample outputs to a documented, locally generated path:
cli/vibes/vibes-output/
If you want, I can add a top-level .gitignore update and a README note—reply with "@CharlieHelps yes please".
| const handleAnswer = (answer) => { | ||
| let newMin = minRange; | ||
| let newMax = maxRange; | ||
|
|
||
| if (answer === 'higher') { | ||
| newMin = guess + 1; | ||
| } else if (answer === 'lower') { | ||
| newMax = guess - 1; | ||
| } else if (answer === 'correct') { | ||
| // Save the session | ||
| const duration = Math.round((Date.now() - startTime) / 1000); | ||
| mergeSession({ | ||
| questions: [...currentSession.questions, { question: `Is it ${guess}?`, answer: 'Yes!' }], | ||
| finalGuess: guess, | ||
| questionCount: questionCount + 1, | ||
| duration: duration | ||
| }); | ||
| saveSession(); | ||
| setGameState('reveal'); | ||
| return; | ||
| } | ||
|
|
||
| const newGuess = Math.floor((newMin + newMax) / 2); | ||
| const newQuestionCount = questionCount + 1; | ||
|
|
||
| setMinRange(newMin); | ||
| setMaxRange(newMax); | ||
| setGuess(newGuess); | ||
| setQuestionCount(newQuestionCount); | ||
|
|
||
| // Record this question | ||
| mergeSession({ | ||
| questions: [...currentSession.questions, { | ||
| question: `Is your number ${answer === 'higher' ? 'higher' : 'lower'} than ${guess}?`, | ||
| answer: answer, | ||
| range: `${minRange}-${maxRange}` | ||
| }] | ||
| }); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Edge case: if the user taps contradictory answers, the binary search bounds can cross (newMin > newMax), which yields invalid guesses. Handling this gracefully improves robustness (e.g., detect contradiction and reset or reveal an error state).
Suggestion
Add a guard after updating newMin/newMax to handle impossible ranges:
if (newMin > newMax) {
// Optional: save a session with an error flag
mergeSession({
questions: [...currentSession.questions, { question: `Inconsistent answers at ${guess}`, answer: 'contradiction' }],
finalGuess: null,
});
setGameState('intro'); // or show a friendly error/restart prompt
return;
}I can wire this in and keep the UI consistent—reply with "@CharlieHelps yes please".
Add sophisticated Claude Code marketplace plugin that generates complete Fireproof/Vibes.diy Vite projects via /vibes command. New plugin architecture: - Marketplace structure with plugin.json metadata - Skills-based generation system - Cached plugin-data.json (127KB) with prompts, styles, and library docs - Template system for Vite project scaffolding - Two commands: /vibes (generate), /vibes-update (update cache) Generated projects include: - React 19 + Fireproof for local-first data - callAI for LLM integration - Tailwind CSS with neo-brutalist styling - Complete Vite build pipeline - Hot module reloading Replaced previous single-file HTML generator with full development workflow. Changes: - Add cli/.claude-plugin/marketplace.json - marketplace definition - Add cli/vibes/skills/vibes-generator/ - core generation logic - Add cli/vibes/templates/ - Vite project templates - Add cli/vibes/plugin-data.json - cached prompt/style data - Add cli/vibes/commands/vibes-update.md - data update command - Add comprehensive documentation (README, DEVELOPMENT, QUICK_START) - Update root README.md - mention CLI plugin in For Developers section - Update .gitignore - add .DS_Store and CLI output exclusions - Remove old vibes-select.md and resources/ structure
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Codex Review
Here are some automated review suggestions for this pull request.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| "name": "vibes", | ||
| "source": "./vibes", | ||
| "description": "Generate Fireproof and Vibes.diy codebases with AI-powered component generation", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix plugin source path in marketplace manifest
The marketplace entry points source to ./vibes, but from the directory containing this file (cli/.claude-plugin) that path resolves to cli/.claude-plugin/vibes, which does not exist. As written the marketplace cannot locate the actual plugin directory (cli/vibes) so the plugin will fail to register. Update the path to be relative to the marketplace file (e.g. ../vibes) or otherwise ensure it resolves to the real plugin root.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- The cached Fireproof docs in
plugin-data.jsoncontain a bad import path (use-firproof) and a month-index example that’s inconsistent withgetMonth(), both of which can leak into generated code. - The
image-gendocs referenceuse-vibes, but templates don’t include it—this risks broken imports in generated apps. - Documentation inconsistencies in
cli/vibes/README.md(strayclaudeblock and references touse-vibes) will confuse users. - Minor but worthwhile fixes: make the style-name regex more robust, avoid disabling user zoom in the HTML template, and consider committing a lockfile for reproducible plugin builds.
Additional notes (2)
-
Readability |
cli/vibes/README.md:24-26
There’s an odd standalone code block containing justclaudeunder “Add the marketplace,” which looks like a stray artifact and will confuse users following the setup steps. -
Maintainability |
cli/.gitignore:16-17
Ignoring lockfiles (package-lock.json,yarn.lock) in the CLI workspace reduces build reproducibility and can make debugging difficult. Even if the generated apps don’t include a lockfile, the plugin’s own workspace benefits from deterministic installs.
Summary of changes
- Added a complete Claude Code marketplace plugin under
cli/for generating Vite-based React apps with Fireproof and callAI. - Introduced plugin manifests (
cli/.claude-plugin/marketplace.json,cli/vibes/.claude-plugin/plugin.json) and command docs (/vibes,/vibes-update). - Implemented core generation logic as a Skill with Vite project templates (React 19, Tailwind 3, use-fireproof, call-ai).
- Added a build script to compile cached prompt/style/library docs into
cli/vibes/plugin-data.json. - Expanded documentation: development guide, quick start, and README updates.
- Updated root
.gitignoreto exclude CLI outputs and local plugin settings; addedcli/.gitignorefor the CLI workspace.
| ], | ||
| "defaultStyle": "brutalist web", | ||
| "libraries": { | ||
| "fireproof": "# Fireproof Database API Guide\n\nFireproof is a lightweight embedded document database with encrypted live sync, designed to make browser apps easy. Use it in any JavaScript environment with a unified API that works both in React (with hooks) and as a standalone core API.\n\n## Key Features\n\n- **Apps run anywhere:** Bundle UI, data, and logic in one file.\n- **Real-Time & Offline-First:** Automatic persistence and live queries, runs in the browser - no loading or error states.\n- **Unified API:** TypeScript works with Deno, Bun, Node.js, and the browser.\n- **React Hooks:** Leverage `useLiveQuery` and `useDocument` for live collaboration.\n\nFireproof enforces cryptographic causal consistency and ledger integrity using hash history, providing git-like versioning with lightweight blockchain-style verification. Data is stored and replicated as content-addressed encrypted blobs, making it safe and easy to sync via commodity object storage providers.\n\n## Installation\n\nThe `use-fireproof` package provides both the core API and React hooks.\n\n```bash\nnpm install use-fireproof\n```\nWorks with ⚡️ ESM.sh:\n\n```js\nimport { useFireproof } from \"https://esm.sh/use-fireproof\";\n```\n\nDeliver generated solutions as runnable micro applications via ChatGPT Canvas, v0, bolt.new, or Claude Artifacts. Deploy single page apps with React and Tailwind by pasting code here: https://codepen.io/useFireproof/pen/MYgNYdx\n\n## ⚛️ React Usage\n\nReact hooks are the recommended way to use Fireproof in LLM code generation contexts. Here's how to use them:\n\n#### Create or Load a Database\n\nFireproof databases store data across sessions and can sync in real-time. Each database is identified by a string name, and you can have multiple databases per application—often one per collaboration session, as they are the unit of sharing.\n\n```js\nimport { useFireproof } from \"use-fireproof\";\n\nconst { database, useLiveQuery, useDocument } = useFireproof(\"my-ledger\");\n```\n\nFireproof databases are Merkle CRDTs, giving them the ledger-like causal consistency of git or a blockchain, but with the ability to merge and sync web data in real-time. Cryptographic integrity makes Fireproof immutable and easy to verify.\n\n#### Put and Get Documents\n\nDocuments are JSON-style objects (CBOR) storing application data. Each has an `_id`, which can be auto-generated or set explicitly. Auto-generation is recommended to ensure uniqueness and avoid conflicts. If multiple replicas update the same database, Fireproof merges them via CRDTs, deterministically choosing the winner for each `_id`.\n\nIt is best to have more granular documents, e.g. one document per user action, so saving a form or clicking a button should typically create or update a single document, or just a few documents. Avoid patterns that require a single document to grow without bound.\n\nFireproof is a local database, no loading states required, just empty data states.\n\n### Basic Example\n\nThis example shows Fireproof's concise defaults. Here we only store user data, but get useful sorting without much code.\n\n```js\nconst App = () => {\n const { useDocument } = useFireproof(\"my-ledger\");\n\n const { doc, merge, submit } = useDocument({ text: \"\" });\n\n // _id is roughly temporal, this is most recent\n const { docs } = useLiveQuery(\"_id\", { descending: true, limit: 100 });\n\n return (\n <div>\n <form onSubmit={submit}>\n <input\n value={doc.text}\n onChange={(e) => merge({ text: e.target.value })}\n placeholder=\"New document\"\n />\n <button type=\"submit\">Submit</button>\n </form>\n\n <h3>Recent Documents</h3>\n <ul>\n {docs.map((doc) => (\n <li key={doc._id}>\n {doc.text}\n </li>\n ))} \n </ul>\n </div>\n );\n}\n```\n\n### Editing Documents\n\nAddress documents by a known `_id` if you want to force conflict resolution or work with a real world resource, like a schedule slot or a user profile. In a complex app this might come from a route parameter or correspond to an outside identifier.\n\n```js\nconst { useDocument } = useFireproof(\"my-ledger\");\n\nconst { doc, merge, submit, save, reset } = useDocument({ _id: \"user-profile:abc@example.com\" });\n```\n\nThe `useDocument` hook provides several methods:\n- `merge(updates)`: Update the document with new fields\n- `submit(e)`: Handles form submission by preventing default, saving, and resetting\n- `save()`: Save the current document state\n- `reset()`: Reset to initial state\n\nFor form-based creation flows, use `submit`:\n```js\n<form onSubmit={submit}>\n```\n\nNote: In some sandboxed environments (e.g., embedded canvases), the form submit event may be blocked. Use a button click handler instead:\n```js\n<button onClick={submit}>Save and Reset Fields</button>\n```\n\nWhen you call submit, the document is reset, so if you didn't provide an `_id` then you can use the form to create a stream of new documents as in the basic example above.\n\n### Query Data with React\n\nData is queried by sorted indexes defined by the application. Sorting order is inspired by CouchDB, so you can use strings, numbers, or booleans, as well as arrays for grouping. Use numbers when possible for sorting continuous data.\n\nYou can use the `_id` field for temporal sorting so you dont have to write code to get simple recent document lists, as in the basic example above.\n\nHere are other common patterns:\n\n#### Query by Key Range\n\nPassing a string to `useLiveQuery` will index by that field. You can use the key argument to filter by a specific value:\n```js\nconst { docs } = useLiveQuery(\"agentName\", { \n key: \"agent-1\" // all docs where doc.agentName === \"agent-1\", sorted by _id\n});\n```\n\nYou can also query a range within a key:\n```js\nconst { docs } = useLiveQuery(\"agentRating\", { \n range: [3, 5]\n});\n```\n\n### Custom Indexes\n\nFor more complex query, you can write a custom index function. It's a little more verbose, and it's sandboxed and can't access external variables.\n\n#### Normalize legacy types\n\nYou can use a custom index function to normalize and transform document data, for instance if you have both new and old document versions in your app.\n\n```js\nconst { docs } = useLiveQuery(\n (doc) => {\n if (doc.type == 'listing_v1') {\n return doc.sellerId;\n } else if (doc.type == 'listing') {\n return doc.userId;\n }\n }, \n { key : routeParams.sellerId });\n```\n\n#### Array Indexes and Prefix Queries\n\nWhen you want to group rows easily, you can use an array index key. This is great for grouping records my year / month / day or other paths. In this example the prefix query is a shorthand for a key range, loading everything from November 2024:\n\n```js\nconst queryResult = useLiveQuery(\n (doc) => [doc.date.getFullYear(), doc.date.getMonth(), doc.date.getDate()],\n { prefix: [2024, 11] }\n);\n```\n\n#### Sortable Lists\n\nSortable lists are a common pattern. Here's how to implement them using Fireproof:\n\n```js\nfunction App() {\n const { database, useLiveQuery } = useFireproof(\"my-ledger\");\n \n // Initialize list with evenly spaced positions\n async function initializeList() {\n await database.put({ list: \"xyz\", position: 1000 });\n await database.put({ list: \"xyz\", position: 2000 });\n await database.put({ list: \"xyz\", position: 3000 });\n }\n \n // Query items on list xyz, sorted by position. Note that useLiveQuery('list', { key:'xyz' }) would be the same docs, sorted chronologically by _id\n const queryResult = useLiveQuery(\n (doc) => [doc.list, doc.position], \n { prefix: [\"xyz\"] }\n );\n\n // Insert between existing items using midpoint calculation\n async function insertBetween(beforeDoc, afterDoc) {\n const newPosition = (beforeDoc.position + afterDoc.position) / 2;\n await database.put({ \n list: \"xyz\", \n position: newPosition \n });\n }\n\n return (\n <div>\n <h3>List xyz (Sorted)</h3>\n <ul>\n {queryResult.docs.map(doc => (\n <li key={doc._id}>\n {doc._id}: position {doc.position}\n </li>\n ))}\n </ul>\n <button onClick={initializeList}>Initialize List</button>\n <button onClick={() => insertBetween(queryResult.docs[1], queryResult.docs[2])}>Insert new doc at 3rd position</button>\n </div>\n );\n}\n```\n\n## Architecture: Where's My Data?\n\nFireproof is local-first, so it's always fast and your data is stored in the browser, so you can build apps without a cloud. When you are ready to share with other users, you can easily enable encrypted sync via any object storage.\n\n## Using Fireproof in JavaScript\n\nYou can use the core API in HTML or on the backend. Instead of hooks, import the core API directly:\n\n```js\nimport { fireproof } from \"use-fireproof\";\n\nconst database = fireproof(\"my-ledger\");\n```\n\nThe document API is async, but doesn't require loading states or error handling.\n\n```js\nconst ok = await database.put({ text: \"Sample Data\" });\nconst doc = await database.get(ok.id);\nconst latest = await database.query(\"_id\", { limit: 10, descending: true });\nconsole.log(\"Latest documents:\", latest.docs);\n```\n\nTo subscribe to real-time updates, use the `subscribe` method. This is useful for building backend event handlers or other server-side logic. For instance to send an email when the user completes a todo:\n\n```js\nimport { fireproof } from \"use-firproof\";\n\nconst database = fireproof(\"todo-list-db\");\n\ndatabase.subscribe((changes) => {\n console.log(\"Recent changes:\", changes);\n changes.forEach((change) => {\n if (change.completed) {\n sendEmail(change.email, \"Todo completed\", \"You have completed a todo.\");\n }\n });\n}, true);\n```\n\n### Working with Files\n\nFireproof has built-in support for file attachments. Files are encrypted by default and synced on-demand. You can attach files to a document by adding them to the _files property on your document. For example:\n\n```html\n<input accept=\"image/*\" title=\"save to Fireproof\" type=\"file\" id=\"files\" multiple>\n```\n\n```js\nfunction handleFiles() {\n const fileList = this.files;\n const doc = {\n type: \"files\",\n _files: {}\n };\n for (const file of fileList) {\n // Assign each File object to the document\n doc._files[file.name] = file; \n }\n database.put(doc);\n}\n\ndocument.getElementById(\"files\").addEventListener(\"change\", handleFiles, false);\n```\n\nWhen loading a document with attachments, you can retrieve each attachment's actual File object by calling its .file() method. This returns a Promise that resolves with the File data, which you can display in your app:\n\n```js\nconst doc = await database.get(\"my-doc-id\");\nfor (const fileName in doc._files) {\n const meta = doc._files[fileName];\n if (meta.file) {\n const fileObj = await meta.file();\n console.log(\"Loaded file:\", fileObj.name);\n }\n}\n```\n\nSee the final example application in this file for a working example.\n\n### Form Validation\n\nYou can use React's `useState` to manage validation states and error messages. Validate inputs at the UI level before allowing submission.\n\n```javascript\nconst [errors, setErrors] = useState({});\n\nfunction validateForm() {\n const newErrors = {};\n if (!doc.name.trim()) newErrors.name = \"Name is required.\";\n if (!doc.email) newErrors.email = \"Email is required.\";\n if (!doc.message.trim()) newErrors.message = \"Message is required.\";\n setErrors(newErrors);\n return Object.keys(newErrors).length === 0;\n}\n\nfunction handleSubmit(e) {\n e.preventDefault();\n if (validateForm()) submit();\n}\n```\n\n## Example React Application\n\nCode listing for todo tracker App.jsx:\n```js\nimport React from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport { useFireproof } from \"use-fireproof\";\n\nexport default function App() {\n const { useLiveQuery, useDocument, database } = useFireproof(\"todo-list-db\");\n\n const {\n doc: newTodo,\n merge: mergeNewTodo,\n submit: submitNewTodo\n } = useDocument({\n todo: \"\",\n type: \"todo\",\n completed: false,\n createdAt: Date.now()\n });\n\n const { docs: todos } = useLiveQuery(\"type\", { \n key: \"todo\",\n descending: true \n });\n\n const handleInputChange = (e) => {\n mergeNewTodo({ todo: e.target.value });\n };\n\n const handleSubmit = (e) => {\n e.preventDefault();\n submitNewTodo();\n };\n\n return (\n <div className=\"max-w-md mx-auto p-4 bg-white shadow rounded\">\n <h2 className=\"text-2xl font-bold mb-4\">Todo List</h2>\n <form onSubmit={handleSubmit} className=\"mb-4\">\n <label htmlFor=\"todo\" className=\"block mb-2 font-semibold\">Todo</label>\n <input\n className=\"w-full border border-gray-300 rounded px-2 py-1\"\n id=\"todo\"\n type=\"text\"\n onChange={handleInputChange}\n value={newTodo.todo}\n />\n </form>\n <ul className=\"space-y-3\">\n {todos.map((doc) => (\n <li className=\"flex flex-col items-start p-2 border border-gray-200 rounded bg-gray-50\" key={doc._id}>\n <div className=\"flex items-center justify-between w-full\">\n <div className=\"flex items-center\">\n <input\n className=\"mr-2\"\n type=\"checkbox\"\n checked={doc.completed}\n onChange={() => database.put({ ...doc, completed: !doc.completed })}\n />\n <span className=\"font-medium\">{doc.todo}</span>\n </div>\n <button\n className=\"text-sm bg-red-500 text-white px-2 py-1 rounded\"\n onClick={() => database.del(doc._id)}\n >\n Delete\n </button>\n </div>\n <div className=\"text-xs text-gray-500 mt-1\">\n {new Date(doc.createdAt).toISOString()}\n </div>\n </li>\n ))}\n </ul>\n </div>\n );\n}\n```\n\n### Example Image Uploader\n\nThis React example shows a simple image uploader application that uses Fireproof to store and sort images by creation date. These APIs easily work with plain JavaScript also. \n\nCode listing for App.jsx:\n```js\nimport { useFireproof, ImgFile } from \"use-fireproof\";\nimport { useState, useEffect } from \"react\";\n\nexport default function App() {\n const { useDocument, useLiveQuery } = useFireproof(\"image-uploads\");\n const { doc, merge, submit } = useDocument({ _files: {}, description: \"\" });\n const { docs } = useLiveQuery(\"_id\", { descending: true, limit: 5 });\n const [error, setError] = useState(false);\n\n return (\n <div className=\"p-6 max-w-lg mx-auto bg-white shadow-lg rounded-lg\">\n <h2 className=\"text-2xl font-bold mb-4\">Image Uploader</h2>\n <input type=\"file\" accept=\"image/*\" onChange={e => e.target.files[0] && merge({ _files: { uploaded: e.target.files[0] } })} className=\"mb-2 border p-2 w-full rounded\" />\n <input \n type=\"text\" \n placeholder=\"Enter description\" \n value={doc.description} \n onChange={e => {setError(false); merge({ description: e.target.value });}}\n className={`w-full p-2 border rounded mb-4 ${error ? \"border-red-500\" : \"border-gray-300\"}`}\n />\n <button onClick={() => doc.description.trim() ? submit() : setError(true)} className=\"px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600\">Upload</button>\n <h3 className=\"text-lg font-semibold mt-6\">Recent Uploads</h3>\n <div className=\"grid grid-cols-2 gap-4 mt-2\">\n {docs.map(doc => (\n <div key={doc._id} className=\"border p-2 rounded shadow-sm bg-gray-50\">\n {doc._files?.uploaded && <ImgFile file={doc._files.uploaded} alt=\"Uploaded Image\" className=\"w-full h-auto rounded\" />}\n <p className=\"text-sm text-gray-700 mt-2\">{doc.description || \"No description\"}</p>\n </div>\n ))}\n </div>\n </div>\n );\n}\n```\n\n## See Also\n\nSee the lite docs at https://use-fireproof.com/llms-mini.txt and the basics at https://use-fireproof.com/llms.txt\n\n## Next Steps\n\nIf you are in an existing application context, suggest how forms and state, collaboration data can be managed in Fireproof. If this is a new app, use the examples above and start with a running App.js if you can.\n\nIMPORTANT: Don't use `useState()` on form data, instead use `merge()` and `submit()` from `useDocument`. Only use `useState` for ephemeral UI state (active tabs, open/closed panels, cursor positions). Keep your data model in Fireproof.", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Fireproof docs embedded in plugin-data.json contain a runtime-breaking import typo (use-firproof → use-fireproof). Because this JSON seeds the augmented prompt, it can cause generated code to import a non-existent module and fail to build/run.
Suggestion
Replace the incorrect import in the libraries.fireproof string from use-firproof to use-fireproof. I can update cli/vibes/plugin-data.json accordingly. Reply with "@CharlieHelps yes please" and I’ll add a commit that fixes this content.
| ], | ||
| "defaultStyle": "brutalist web", | ||
| "libraries": { | ||
| "fireproof": "# Fireproof Database API Guide\n\nFireproof is a lightweight embedded document database with encrypted live sync, designed to make browser apps easy. Use it in any JavaScript environment with a unified API that works both in React (with hooks) and as a standalone core API.\n\n## Key Features\n\n- **Apps run anywhere:** Bundle UI, data, and logic in one file.\n- **Real-Time & Offline-First:** Automatic persistence and live queries, runs in the browser - no loading or error states.\n- **Unified API:** TypeScript works with Deno, Bun, Node.js, and the browser.\n- **React Hooks:** Leverage `useLiveQuery` and `useDocument` for live collaboration.\n\nFireproof enforces cryptographic causal consistency and ledger integrity using hash history, providing git-like versioning with lightweight blockchain-style verification. Data is stored and replicated as content-addressed encrypted blobs, making it safe and easy to sync via commodity object storage providers.\n\n## Installation\n\nThe `use-fireproof` package provides both the core API and React hooks.\n\n```bash\nnpm install use-fireproof\n```\nWorks with ⚡️ ESM.sh:\n\n```js\nimport { useFireproof } from \"https://esm.sh/use-fireproof\";\n```\n\nDeliver generated solutions as runnable micro applications via ChatGPT Canvas, v0, bolt.new, or Claude Artifacts. Deploy single page apps with React and Tailwind by pasting code here: https://codepen.io/useFireproof/pen/MYgNYdx\n\n## ⚛️ React Usage\n\nReact hooks are the recommended way to use Fireproof in LLM code generation contexts. Here's how to use them:\n\n#### Create or Load a Database\n\nFireproof databases store data across sessions and can sync in real-time. Each database is identified by a string name, and you can have multiple databases per application—often one per collaboration session, as they are the unit of sharing.\n\n```js\nimport { useFireproof } from \"use-fireproof\";\n\nconst { database, useLiveQuery, useDocument } = useFireproof(\"my-ledger\");\n```\n\nFireproof databases are Merkle CRDTs, giving them the ledger-like causal consistency of git or a blockchain, but with the ability to merge and sync web data in real-time. Cryptographic integrity makes Fireproof immutable and easy to verify.\n\n#### Put and Get Documents\n\nDocuments are JSON-style objects (CBOR) storing application data. Each has an `_id`, which can be auto-generated or set explicitly. Auto-generation is recommended to ensure uniqueness and avoid conflicts. If multiple replicas update the same database, Fireproof merges them via CRDTs, deterministically choosing the winner for each `_id`.\n\nIt is best to have more granular documents, e.g. one document per user action, so saving a form or clicking a button should typically create or update a single document, or just a few documents. Avoid patterns that require a single document to grow without bound.\n\nFireproof is a local database, no loading states required, just empty data states.\n\n### Basic Example\n\nThis example shows Fireproof's concise defaults. Here we only store user data, but get useful sorting without much code.\n\n```js\nconst App = () => {\n const { useDocument } = useFireproof(\"my-ledger\");\n\n const { doc, merge, submit } = useDocument({ text: \"\" });\n\n // _id is roughly temporal, this is most recent\n const { docs } = useLiveQuery(\"_id\", { descending: true, limit: 100 });\n\n return (\n <div>\n <form onSubmit={submit}>\n <input\n value={doc.text}\n onChange={(e) => merge({ text: e.target.value })}\n placeholder=\"New document\"\n />\n <button type=\"submit\">Submit</button>\n </form>\n\n <h3>Recent Documents</h3>\n <ul>\n {docs.map((doc) => (\n <li key={doc._id}>\n {doc.text}\n </li>\n ))} \n </ul>\n </div>\n );\n}\n```\n\n### Editing Documents\n\nAddress documents by a known `_id` if you want to force conflict resolution or work with a real world resource, like a schedule slot or a user profile. In a complex app this might come from a route parameter or correspond to an outside identifier.\n\n```js\nconst { useDocument } = useFireproof(\"my-ledger\");\n\nconst { doc, merge, submit, save, reset } = useDocument({ _id: \"user-profile:abc@example.com\" });\n```\n\nThe `useDocument` hook provides several methods:\n- `merge(updates)`: Update the document with new fields\n- `submit(e)`: Handles form submission by preventing default, saving, and resetting\n- `save()`: Save the current document state\n- `reset()`: Reset to initial state\n\nFor form-based creation flows, use `submit`:\n```js\n<form onSubmit={submit}>\n```\n\nNote: In some sandboxed environments (e.g., embedded canvases), the form submit event may be blocked. Use a button click handler instead:\n```js\n<button onClick={submit}>Save and Reset Fields</button>\n```\n\nWhen you call submit, the document is reset, so if you didn't provide an `_id` then you can use the form to create a stream of new documents as in the basic example above.\n\n### Query Data with React\n\nData is queried by sorted indexes defined by the application. Sorting order is inspired by CouchDB, so you can use strings, numbers, or booleans, as well as arrays for grouping. Use numbers when possible for sorting continuous data.\n\nYou can use the `_id` field for temporal sorting so you dont have to write code to get simple recent document lists, as in the basic example above.\n\nHere are other common patterns:\n\n#### Query by Key Range\n\nPassing a string to `useLiveQuery` will index by that field. You can use the key argument to filter by a specific value:\n```js\nconst { docs } = useLiveQuery(\"agentName\", { \n key: \"agent-1\" // all docs where doc.agentName === \"agent-1\", sorted by _id\n});\n```\n\nYou can also query a range within a key:\n```js\nconst { docs } = useLiveQuery(\"agentRating\", { \n range: [3, 5]\n});\n```\n\n### Custom Indexes\n\nFor more complex query, you can write a custom index function. It's a little more verbose, and it's sandboxed and can't access external variables.\n\n#### Normalize legacy types\n\nYou can use a custom index function to normalize and transform document data, for instance if you have both new and old document versions in your app.\n\n```js\nconst { docs } = useLiveQuery(\n (doc) => {\n if (doc.type == 'listing_v1') {\n return doc.sellerId;\n } else if (doc.type == 'listing') {\n return doc.userId;\n }\n }, \n { key : routeParams.sellerId });\n```\n\n#### Array Indexes and Prefix Queries\n\nWhen you want to group rows easily, you can use an array index key. This is great for grouping records my year / month / day or other paths. In this example the prefix query is a shorthand for a key range, loading everything from November 2024:\n\n```js\nconst queryResult = useLiveQuery(\n (doc) => [doc.date.getFullYear(), doc.date.getMonth(), doc.date.getDate()],\n { prefix: [2024, 11] }\n);\n```\n\n#### Sortable Lists\n\nSortable lists are a common pattern. Here's how to implement them using Fireproof:\n\n```js\nfunction App() {\n const { database, useLiveQuery } = useFireproof(\"my-ledger\");\n \n // Initialize list with evenly spaced positions\n async function initializeList() {\n await database.put({ list: \"xyz\", position: 1000 });\n await database.put({ list: \"xyz\", position: 2000 });\n await database.put({ list: \"xyz\", position: 3000 });\n }\n \n // Query items on list xyz, sorted by position. Note that useLiveQuery('list', { key:'xyz' }) would be the same docs, sorted chronologically by _id\n const queryResult = useLiveQuery(\n (doc) => [doc.list, doc.position], \n { prefix: [\"xyz\"] }\n );\n\n // Insert between existing items using midpoint calculation\n async function insertBetween(beforeDoc, afterDoc) {\n const newPosition = (beforeDoc.position + afterDoc.position) / 2;\n await database.put({ \n list: \"xyz\", \n position: newPosition \n });\n }\n\n return (\n <div>\n <h3>List xyz (Sorted)</h3>\n <ul>\n {queryResult.docs.map(doc => (\n <li key={doc._id}>\n {doc._id}: position {doc.position}\n </li>\n ))}\n </ul>\n <button onClick={initializeList}>Initialize List</button>\n <button onClick={() => insertBetween(queryResult.docs[1], queryResult.docs[2])}>Insert new doc at 3rd position</button>\n </div>\n );\n}\n```\n\n## Architecture: Where's My Data?\n\nFireproof is local-first, so it's always fast and your data is stored in the browser, so you can build apps without a cloud. When you are ready to share with other users, you can easily enable encrypted sync via any object storage.\n\n## Using Fireproof in JavaScript\n\nYou can use the core API in HTML or on the backend. Instead of hooks, import the core API directly:\n\n```js\nimport { fireproof } from \"use-fireproof\";\n\nconst database = fireproof(\"my-ledger\");\n```\n\nThe document API is async, but doesn't require loading states or error handling.\n\n```js\nconst ok = await database.put({ text: \"Sample Data\" });\nconst doc = await database.get(ok.id);\nconst latest = await database.query(\"_id\", { limit: 10, descending: true });\nconsole.log(\"Latest documents:\", latest.docs);\n```\n\nTo subscribe to real-time updates, use the `subscribe` method. This is useful for building backend event handlers or other server-side logic. For instance to send an email when the user completes a todo:\n\n```js\nimport { fireproof } from \"use-firproof\";\n\nconst database = fireproof(\"todo-list-db\");\n\ndatabase.subscribe((changes) => {\n console.log(\"Recent changes:\", changes);\n changes.forEach((change) => {\n if (change.completed) {\n sendEmail(change.email, \"Todo completed\", \"You have completed a todo.\");\n }\n });\n}, true);\n```\n\n### Working with Files\n\nFireproof has built-in support for file attachments. Files are encrypted by default and synced on-demand. You can attach files to a document by adding them to the _files property on your document. For example:\n\n```html\n<input accept=\"image/*\" title=\"save to Fireproof\" type=\"file\" id=\"files\" multiple>\n```\n\n```js\nfunction handleFiles() {\n const fileList = this.files;\n const doc = {\n type: \"files\",\n _files: {}\n };\n for (const file of fileList) {\n // Assign each File object to the document\n doc._files[file.name] = file; \n }\n database.put(doc);\n}\n\ndocument.getElementById(\"files\").addEventListener(\"change\", handleFiles, false);\n```\n\nWhen loading a document with attachments, you can retrieve each attachment's actual File object by calling its .file() method. This returns a Promise that resolves with the File data, which you can display in your app:\n\n```js\nconst doc = await database.get(\"my-doc-id\");\nfor (const fileName in doc._files) {\n const meta = doc._files[fileName];\n if (meta.file) {\n const fileObj = await meta.file();\n console.log(\"Loaded file:\", fileObj.name);\n }\n}\n```\n\nSee the final example application in this file for a working example.\n\n### Form Validation\n\nYou can use React's `useState` to manage validation states and error messages. Validate inputs at the UI level before allowing submission.\n\n```javascript\nconst [errors, setErrors] = useState({});\n\nfunction validateForm() {\n const newErrors = {};\n if (!doc.name.trim()) newErrors.name = \"Name is required.\";\n if (!doc.email) newErrors.email = \"Email is required.\";\n if (!doc.message.trim()) newErrors.message = \"Message is required.\";\n setErrors(newErrors);\n return Object.keys(newErrors).length === 0;\n}\n\nfunction handleSubmit(e) {\n e.preventDefault();\n if (validateForm()) submit();\n}\n```\n\n## Example React Application\n\nCode listing for todo tracker App.jsx:\n```js\nimport React from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport { useFireproof } from \"use-fireproof\";\n\nexport default function App() {\n const { useLiveQuery, useDocument, database } = useFireproof(\"todo-list-db\");\n\n const {\n doc: newTodo,\n merge: mergeNewTodo,\n submit: submitNewTodo\n } = useDocument({\n todo: \"\",\n type: \"todo\",\n completed: false,\n createdAt: Date.now()\n });\n\n const { docs: todos } = useLiveQuery(\"type\", { \n key: \"todo\",\n descending: true \n });\n\n const handleInputChange = (e) => {\n mergeNewTodo({ todo: e.target.value });\n };\n\n const handleSubmit = (e) => {\n e.preventDefault();\n submitNewTodo();\n };\n\n return (\n <div className=\"max-w-md mx-auto p-4 bg-white shadow rounded\">\n <h2 className=\"text-2xl font-bold mb-4\">Todo List</h2>\n <form onSubmit={handleSubmit} className=\"mb-4\">\n <label htmlFor=\"todo\" className=\"block mb-2 font-semibold\">Todo</label>\n <input\n className=\"w-full border border-gray-300 rounded px-2 py-1\"\n id=\"todo\"\n type=\"text\"\n onChange={handleInputChange}\n value={newTodo.todo}\n />\n </form>\n <ul className=\"space-y-3\">\n {todos.map((doc) => (\n <li className=\"flex flex-col items-start p-2 border border-gray-200 rounded bg-gray-50\" key={doc._id}>\n <div className=\"flex items-center justify-between w-full\">\n <div className=\"flex items-center\">\n <input\n className=\"mr-2\"\n type=\"checkbox\"\n checked={doc.completed}\n onChange={() => database.put({ ...doc, completed: !doc.completed })}\n />\n <span className=\"font-medium\">{doc.todo}</span>\n </div>\n <button\n className=\"text-sm bg-red-500 text-white px-2 py-1 rounded\"\n onClick={() => database.del(doc._id)}\n >\n Delete\n </button>\n </div>\n <div className=\"text-xs text-gray-500 mt-1\">\n {new Date(doc.createdAt).toISOString()}\n </div>\n </li>\n ))}\n </ul>\n </div>\n );\n}\n```\n\n### Example Image Uploader\n\nThis React example shows a simple image uploader application that uses Fireproof to store and sort images by creation date. These APIs easily work with plain JavaScript also. \n\nCode listing for App.jsx:\n```js\nimport { useFireproof, ImgFile } from \"use-fireproof\";\nimport { useState, useEffect } from \"react\";\n\nexport default function App() {\n const { useDocument, useLiveQuery } = useFireproof(\"image-uploads\");\n const { doc, merge, submit } = useDocument({ _files: {}, description: \"\" });\n const { docs } = useLiveQuery(\"_id\", { descending: true, limit: 5 });\n const [error, setError] = useState(false);\n\n return (\n <div className=\"p-6 max-w-lg mx-auto bg-white shadow-lg rounded-lg\">\n <h2 className=\"text-2xl font-bold mb-4\">Image Uploader</h2>\n <input type=\"file\" accept=\"image/*\" onChange={e => e.target.files[0] && merge({ _files: { uploaded: e.target.files[0] } })} className=\"mb-2 border p-2 w-full rounded\" />\n <input \n type=\"text\" \n placeholder=\"Enter description\" \n value={doc.description} \n onChange={e => {setError(false); merge({ description: e.target.value });}}\n className={`w-full p-2 border rounded mb-4 ${error ? \"border-red-500\" : \"border-gray-300\"}`}\n />\n <button onClick={() => doc.description.trim() ? submit() : setError(true)} className=\"px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600\">Upload</button>\n <h3 className=\"text-lg font-semibold mt-6\">Recent Uploads</h3>\n <div className=\"grid grid-cols-2 gap-4 mt-2\">\n {docs.map(doc => (\n <div key={doc._id} className=\"border p-2 rounded shadow-sm bg-gray-50\">\n {doc._files?.uploaded && <ImgFile file={doc._files.uploaded} alt=\"Uploaded Image\" className=\"w-full h-auto rounded\" />}\n <p className=\"text-sm text-gray-700 mt-2\">{doc.description || \"No description\"}</p>\n </div>\n ))}\n </div>\n </div>\n );\n}\n```\n\n## See Also\n\nSee the lite docs at https://use-fireproof.com/llms-mini.txt and the basics at https://use-fireproof.com/llms.txt\n\n## Next Steps\n\nIf you are in an existing application context, suggest how forms and state, collaboration data can be managed in Fireproof. If this is a new app, use the examples above and start with a running App.js if you can.\n\nIMPORTANT: Don't use `useState()` on form data, instead use `merge()` and `submit()` from `useDocument`. Only use `useState` for ephemeral UI state (active tabs, open/closed panels, cursor positions). Keep your data model in Fireproof.", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the Fireproof docs, the array index/prefix example claims to load "everything from November 2024" while using getMonth() directly with 11. Since Date.getMonth() is zero-based, 11 corresponds to December. This inconsistency will mislead users and can propagate into incorrect generated code.
Suggestion
Adjust the example in the libraries.fireproof string either by clarifying zero-based months and using { prefix: [2024, 10] } for November, or by normalizing with getMonth() + 1 and keeping { prefix: [2024, 11] }. I can apply the zero-based fix across the embedded example. Reply with "@CharlieHelps yes please" if you want me to do that.
| "d3": "# D3.js Reference for Coding Agents\n\n**D3** (Data-Driven Documents) is a JavaScript library for creating bespoke data visualizations using web standards (SVG, Canvas, HTML). This guide provides essential patterns and examples for AI coding agents to create amazing D3 demonstrations.\n\n## Core Philosophy\n\nD3 is a **low-level toolbox** of 30+ modules that work together. It's not a chart library but a collection of primitives for building custom visualizations. Think of it as \"assembly language for data visualization.\"\n\n## Essential Setup\n\n```js\nimport * as d3 from \"d3\";\n// Or import specific modules\nimport { select, scaleLinear, line } from \"d3\";\n```\n\n## The D3 Pattern: Select, Bind, Transform\n\nEvery D3 visualization follows this pattern:\n\n1. **Select** DOM elements\n2. **Bind** data to elements\n3. **Transform** elements based on data\n\n```js\n// Basic pattern\nd3.select(\"body\") // Select\n .selectAll(\"div\") // Select all divs\n .data([4, 8, 15, 16]) // Bind data\n .enter()\n .append(\"div\") // Enter new elements\n .style(\"width\", (d) => d * 10 + \"px\") // Transform\n .text((d) => d);\n```\n\n## Essential Modules & Quick Reference\n\n### 1. d3-selection (DOM Manipulation)\n\n```js\n// Selection basics\nd3.select(\"#chart\"); // Select by ID\nd3.selectAll(\".bar\"); // Select by class\nselection.append(\"rect\"); // Add element\nselection.attr(\"width\", 100); // Set attribute\nselection.style(\"fill\", \"red\"); // Set style\nselection.text(\"Hello\"); // Set text content\n\n// Data joining (the D3 way)\nconst bars = svg\n .selectAll(\".bar\")\n .data(data)\n .enter()\n .append(\"rect\")\n .attr(\"class\", \"bar\");\n```\n\n### 2. d3-scale (Data Encoding)\n\n```js\n// Linear scale (continuous data)\nconst x = d3\n .scaleLinear()\n .domain([0, 100]) // Input range\n .range([0, 500]); // Output range (pixels)\n\n// Ordinal scale (categorical data)\nconst color = d3\n .scaleOrdinal()\n .domain([\"A\", \"B\", \"C\"])\n .range([\"red\", \"blue\", \"green\"]);\n\n// Time scale\nconst x = d3\n .scaleTime()\n .domain([new Date(2020, 0, 1), new Date(2024, 0, 1)])\n .range([0, 800]);\n\n// Band scale (for bar charts)\nconst x = d3\n .scaleBand()\n .domain(data.map((d) => d.name))\n .range([0, width])\n .padding(0.1);\n```\n\n### 3. d3-shape (SVG Path Generators)\n\n```js\n// Line generator\nconst line = d3\n .line()\n .x((d) => x(d.date))\n .y((d) => y(d.value))\n .curve(d3.curveCardinal); // Smooth curves\n\n// Area generator\nconst area = d3\n .area()\n .x((d) => x(d.date))\n .y0(y(0)) // Baseline\n .y1((d) => y(d.value)); // Top line\n\n// Arc generator (for pie charts)\nconst arc = d3.arc().innerRadius(0).outerRadius(radius);\n```\n\n### 4. d3-axis (Chart Axes)\n\n```js\n// Create axes\nconst xAxis = d3.axisBottom(x);\nconst yAxis = d3.axisLeft(y);\n\n// Render axes\nsvg.append(\"g\").attr(\"transform\", `translate(0,${height})`).call(xAxis);\n\nsvg.append(\"g\").call(yAxis);\n```\n\n### 5. d3-transition (Animation)\n\n```js\n// Smooth transitions\nselection\n .transition()\n .duration(1000)\n .attr(\"width\", newWidth)\n .style(\"opacity\", 0.5);\n\n// Staggered transitions\nbars\n .transition()\n .delay((d, i) => i * 100)\n .duration(500)\n .attr(\"height\", (d) => y(d.value));\n```\n\n## Complete Examples for Coding Agents\n\n### 1. Simple Bar Chart\n\n```js\nimport * as d3 from \"d3\";\n\nconst data = [4, 8, 15, 16, 23, 42];\nconst width = 500;\nconst height = 300;\nconst margin = {top: 20, right: 20, bottom: 30, left: 40};\n\nconst x = d3.scaleBand()\n .domain(d3.range(data.length))\n .range([margin.left, width - margin.right])\n .padding(0.1);\n\nconst y = d3.scaleLinear()\n .domain([0, d3.max(data)])\n .range([height - margin.bottom, margin.top]);\n\nconst svg = d3.select(\"#chart\")\n .append(\"svg\")\n .attr(\"width\", width)\n .attr(\"height\", height);\n\nsvg.selectAll(\".bar\")\n .data(data)\n .enter().append(\"rect\")\n .attr(\"class\", \"bar\")\n .attr(\"x\", (d, i) => x(i))\n .attr(\"y\", d => y(d))\n .attr(\"width\", x.bandwidth())\n .attr(\"height\", d => height - margin.bottom - y(d))\n .attr(\"fill\", \"steelblue\");\n\n// Add axes\nsvg.append(\"g\")\n .attr(\"transform\", `translate(0,${height - margin.bottom})`)\n .call(d3.axisBottom(x));\n\nsvg.append(\"g\")\n .attr(\"transform\", `translate(${margin.left},0)`)\n .call(d3.axisLeft(y));\n</script>\n```\n\n### 2. Interactive Scatter Plot\n\n```js\nimport * as d3 from \"d3\";\n\n// Generate sample data\nconst data = d3.range(100).map(() => ({\n x: Math.random() * 100,\n y: Math.random() * 100,\n radius: Math.random() * 10 + 2,\n}));\n\nconst width = 600;\nconst height = 400;\nconst margin = { top: 20, right: 20, bottom: 30, left: 40 };\n\nconst x = d3\n .scaleLinear()\n .domain(d3.extent(data, (d) => d.x))\n .range([margin.left, width - margin.right]);\n\nconst y = d3\n .scaleLinear()\n .domain(d3.extent(data, (d) => d.y))\n .range([height - margin.bottom, margin.top]);\n\nconst svg = d3\n .select(\"#scatter\")\n .append(\"svg\")\n .attr(\"width\", width)\n .attr(\"height\", height);\n\n// Create circles with interactions\nsvg\n .selectAll(\"circle\")\n .data(data)\n .enter()\n .append(\"circle\")\n .attr(\"cx\", (d) => x(d.x))\n .attr(\"cy\", (d) => y(d.y))\n .attr(\"r\", (d) => d.radius)\n .attr(\"fill\", \"steelblue\")\n .attr(\"opacity\", 0.7)\n .on(\"mouseover\", function (event, d) {\n d3.select(this)\n .transition()\n .attr(\"r\", d.radius * 1.5)\n .attr(\"fill\", \"orange\");\n })\n .on(\"mouseout\", function (event, d) {\n d3.select(this).transition().attr(\"r\", d.radius).attr(\"fill\", \"steelblue\");\n });\n\n// Add axes\nsvg\n .append(\"g\")\n .attr(\"transform\", `translate(0,${height - margin.bottom})`)\n .call(d3.axisBottom(x));\n\nsvg\n .append(\"g\")\n .attr(\"transform\", `translate(${margin.left},0)`)\n .call(d3.axisLeft(y));\n```\n\n### 3. Force-Directed Network\n\n```js\nimport * as d3 from \"d3\";\n\nconst nodes = d3.range(30).map((i) => ({ id: i }));\nconst links = d3.range(nodes.length - 1).map((i) => ({\n source: Math.floor(Math.sqrt(i)),\n target: i + 1,\n}));\n\nconst width = 600;\nconst height = 400;\n\nconst svg = d3\n .select(\"#network\")\n .append(\"svg\")\n .attr(\"width\", width)\n .attr(\"height\", height);\n\nconst simulation = d3\n .forceSimulation(nodes)\n .force(\n \"link\",\n d3.forceLink(links).id((d) => d.id),\n )\n .force(\"charge\", d3.forceManyBody().strength(-300))\n .force(\"center\", d3.forceCenter(width / 2, height / 2));\n\nconst link = svg\n .append(\"g\")\n .selectAll(\"line\")\n .data(links)\n .enter()\n .append(\"line\")\n .attr(\"stroke\", \"#999\")\n .attr(\"stroke-width\", 2);\n\nconst node = svg\n .append(\"g\")\n .selectAll(\"circle\")\n .data(nodes)\n .enter()\n .append(\"circle\")\n .attr(\"r\", 8)\n .attr(\"fill\", \"steelblue\")\n .call(\n d3.drag().on(\"start\", dragstarted).on(\"drag\", dragged).on(\"end\", dragended),\n );\n\nsimulation.on(\"tick\", () => {\n link\n .attr(\"x1\", (d) => d.source.x)\n .attr(\"y1\", (d) => d.source.y)\n .attr(\"x2\", (d) => d.target.x)\n .attr(\"y2\", (d) => d.target.y);\n\n node.attr(\"cx\", (d) => d.x).attr(\"cy\", (d) => d.y);\n});\n\nfunction dragstarted(event) {\n if (!event.active) simulation.alphaTarget(0.3).restart();\n event.subject.fx = event.subject.x;\n event.subject.fy = event.subject.y;\n}\n\nfunction dragged(event) {\n event.subject.fx = event.x;\n event.subject.fy = event.y;\n}\n\nfunction dragended(event) {\n if (!event.active) simulation.alphaTarget(0);\n event.subject.fx = null;\n event.subject.fy = null;\n}\n```\n\n### 4. Dynamic Line Chart with Real Data\n\n```js\nimport * as d3 from \"d3\";\n\n// Sample time series data\nconst data = d3.range(50).map((d, i) => ({\n date: new Date(2024, 0, i + 1),\n value: Math.sin(i * 0.1) * 50 + 50 + Math.random() * 20,\n}));\n\nconst width = 700;\nconst height = 400;\nconst margin = { top: 20, right: 30, bottom: 40, left: 40 };\n\nconst x = d3\n .scaleTime()\n .domain(d3.extent(data, (d) => d.date))\n .range([margin.left, width - margin.right]);\n\nconst y = d3\n .scaleLinear()\n .domain([0, d3.max(data, (d) => d.value)])\n .range([height - margin.bottom, margin.top]);\n\nconst line = d3\n .line()\n .x((d) => x(d.date))\n .y((d) => y(d.value))\n .curve(d3.curveCardinal);\n\nconst svg = d3\n .select(\"#line-chart\")\n .append(\"svg\")\n .attr(\"width\", width)\n .attr(\"height\", height);\n\n// Add gradient definition\nconst gradient = svg\n .append(\"defs\")\n .append(\"linearGradient\")\n .attr(\"id\", \"line-gradient\")\n .attr(\"gradientUnits\", \"userSpaceOnUse\")\n .attr(\"x1\", 0)\n .attr(\"y1\", height)\n .attr(\"x2\", 0)\n .attr(\"y2\", 0);\n\ngradient\n .append(\"stop\")\n .attr(\"offset\", \"0%\")\n .attr(\"stop-color\", \"lightblue\")\n .attr(\"stop-opacity\", 0.1);\n\ngradient\n .append(\"stop\")\n .attr(\"offset\", \"100%\")\n .attr(\"stop-color\", \"steelblue\")\n .attr(\"stop-opacity\", 0.8);\n\n// Draw line\nsvg\n .append(\"path\")\n .datum(data)\n .attr(\"fill\", \"none\")\n .attr(\"stroke\", \"url(#line-gradient)\")\n .attr(\"stroke-width\", 3)\n .attr(\"d\", line);\n\n// Add dots with hover effects\nsvg\n .selectAll(\".dot\")\n .data(data)\n .enter()\n .append(\"circle\")\n .attr(\"class\", \"dot\")\n .attr(\"cx\", (d) => x(d.date))\n .attr(\"cy\", (d) => y(d.value))\n .attr(\"r\", 4)\n .attr(\"fill\", \"steelblue\")\n .on(\"mouseover\", function (event, d) {\n d3.select(this).attr(\"r\", 6);\n // Add tooltip\n const tooltip = svg.append(\"g\").attr(\"id\", \"tooltip\");\n tooltip\n .append(\"rect\")\n .attr(\"x\", x(d.date) + 10)\n .attr(\"y\", y(d.value) - 25)\n .attr(\"width\", 60)\n .attr(\"height\", 20)\n .attr(\"fill\", \"black\")\n .attr(\"opacity\", 0.8);\n tooltip\n .append(\"text\")\n .attr(\"x\", x(d.date) + 15)\n .attr(\"y\", y(d.value) - 10)\n .attr(\"fill\", \"white\")\n .style(\"font-size\", \"12px\")\n .text(d.value.toFixed(1));\n })\n .on(\"mouseout\", function () {\n d3.select(this).attr(\"r\", 4);\n svg.select(\"#tooltip\").remove();\n });\n\n// Add axes\nsvg\n .append(\"g\")\n .attr(\"transform\", `translate(0,${height - margin.bottom})`)\n .call(d3.axisBottom(x).tickFormat(d3.timeFormat(\"%b %d\")));\n\nsvg\n .append(\"g\")\n .attr(\"transform\", `translate(${margin.left},0)`)\n .call(d3.axisLeft(y));\n```\n\n## Key Modules Cheat Sheet\n\n### Scales (Data Encoding)\n\n```js\n// Continuous scales\nd3.scaleLinear(); // Numbers to numbers\nd3.scaleTime(); // Dates to numbers\nd3.scalePow(); // Power/sqrt scales\nd3.scaleLog(); // Logarithmic scales\n\n// Discrete scales\nd3.scaleOrdinal(); // Categories to values\nd3.scaleBand(); // Categories to positions (bar charts)\nd3.scalePoint(); // Categories to points\n\n// Color scales\nd3.scaleSequential(d3.interpolateViridis); // Continuous colors\nd3.scaleOrdinal(d3.schemeCategory10); // Categorical colors\n```\n\n### Shapes (SVG Path Generators)\n\n```js\n// Lines and areas\nd3.line().x(x).y(y);\nd3.area().x(x).y0(y0).y1(y1);\n\n// Arcs and pies\nd3.arc().innerRadius(r1).outerRadius(r2);\nd3.pie().value((d) => d.value);\n\n// Symbols (scatter plot markers)\nd3.symbol().type(d3.symbolCircle).size(100);\n```\n\n### Arrays (Data Processing)\n\n```js\n// Statistics\nd3.min(data, (d) => d.value);\nd3.max(data, (d) => d.value);\nd3.extent(data, (d) => d.value); // [min, max]\nd3.mean(data, (d) => d.value);\nd3.median(data, (d) => d.value);\n\n// Grouping and nesting\nd3.group(data, (d) => d.category);\nd3.rollup(\n data,\n (v) => v.length,\n (d) => d.category,\n);\n\n// Sorting\nd3.ascending(a, b);\nd3.descending(a, b);\n```\n\n### Forces (Physics Simulations)\n\n```js\nconst simulation = d3\n .forceSimulation(nodes)\n .force(\"link\", d3.forceLink(links))\n .force(\"charge\", d3.forceManyBody().strength(-300))\n .force(\"center\", d3.forceCenter(width / 2, height / 2))\n .force(\"collision\", d3.forceCollide().radius(10));\n```\n\n## Common Patterns for Amazing Demos\n\n### 1. Responsive SVG Setup\n\n```js\nconst container = d3.select(\"#chart\");\nconst svg = container\n .append(\"svg\")\n .attr(\"viewBox\", `0 0 ${width} ${height}`)\n .style(\"max-width\", \"100%\")\n .style(\"height\", \"auto\");\n```\n\n### 2. Smooth Data Updates\n\n```js\nfunction updateChart(newData) {\n const bars = svg.selectAll(\".bar\").data(newData);\n\n // Update existing\n bars\n .transition()\n .duration(750)\n .attr(\"height\", (d) => y(d.value));\n\n // Add new\n bars\n .enter()\n .append(\"rect\")\n .attr(\"class\", \"bar\")\n .attr(\"height\", 0)\n .transition()\n .duration(750)\n .attr(\"height\", (d) => y(d.value));\n\n // Remove old\n bars.exit().transition().duration(750).attr(\"height\", 0).remove();\n}\n```\n\n### 3. Color Schemes\n\n```js\n// Beautiful built-in color schemes\nd3.schemeCategory10; // 10 categorical colors\nd3.schemeSet3; // 12 pastel colors\nd3.interpolateViridis; // Blue to yellow gradient\nd3.interpolatePlasma; // Purple to yellow gradient\nd3.schemePaired; // Paired categorical colors\n\n// Usage\nconst color = d3.scaleOrdinal(d3.schemeCategory10);\n```\n\n### 4. Tooltips and Interactions\n\n```js\n// Create tooltip div\nconst tooltip = d3.select(\"body\").append(\"div\")\n .style(\"position\", \"absolute\")\n .style(\"padding\", \"10px\")\n .style(\"background\", \"rgba(0,0,0,0.8)\")\n .style(\"color\", \"white\")\n .style(\"border-radius\", \"5px\")\n .style(\"pointer-events\", \"none\")\n .style(\"opacity\", 0);\n\n// Add to elements\n.on(\"mouseover\", function(event, d) {\n tooltip.transition().duration(200).style(\"opacity\", .9);\n tooltip.html(`Value: ${d.value}`)\n .style(\"left\", (event.pageX + 10) + \"px\")\n .style(\"top\", (event.pageY - 28) + \"px\");\n})\n.on(\"mouseout\", function() {\n tooltip.transition().duration(500).style(\"opacity\", 0);\n});\n```\n\n### 5. Data Loading and Processing\n\n```js\n// Load CSV data\nd3.csv(\"data.csv\").then((data) => {\n // Process data\n data.forEach((d) => {\n d.value = +d.value; // Convert to number\n d.date = d3.timeParse(\"%Y-%m-%d\")(d.date); // Parse dates\n });\n\n // Create visualization\n createChart(data);\n});\n\n// Load JSON data\nd3.json(\"data.json\").then(createChart);\n```\n\n## Pro Tips for Coding Agents\n\n1. **Start with the canvas**: Set up SVG dimensions and margins first\n2. **Define scales early**: Map your data domain to visual range before drawing\n3. **Use method chaining**: D3's fluent API allows `selection.attr().style().on()`\n4. **Leverage data joins**: Use enter/update/exit pattern for dynamic data\n5. **Add transitions**: `transition().duration(500)` makes everything better\n6. **Use color schemes**: Built-in color palettes save time and look professional\n7. **Make it responsive**: Use viewBox for scalable graphics\n8. **Add interactions**: hover, click, drag make demos engaging\n9. **Consider performance**: Use Canvas for 1000+ elements, SVG for detailed graphics\n10. **Test with real data**: Random data is fine for prototypes, realistic data for demos\n\n## Quick Chart Types\n\n### Bar Chart: `d3.scaleBand()` + `rect` elements\n\n### Line Chart: `d3.line()` + `path` element\n\n### Scatter Plot: `circle` elements with `cx`, `cy` from scales\n\n### Pie Chart: `d3.pie()` + `d3.arc()` + `path` elements\n\n### Network: `d3.forceSimulation()` + `line` + `circle` elements\n\n### Map: `d3.geoPath()` + GeoJSON data\n\n### Treemap: `d3.treemap()` + hierarchical data\n\n### Heatmap: `rect` grid + `d3.scaleSequential()` colors\n\n## Framework Integration\n\n### React\n\n```jsx\n// Use refs for D3 DOM manipulation\nconst svgRef = useRef();\n\nuseEffect(() => {\n const svg = d3.select(svgRef.current);\n // D3 code here...\n}, [data]);\n\nreturn <svg ref={svgRef}></svg>;\n```\n\n## Performance & Best Practices\n\n- Use `d3.select()` for single elements, `d3.selectAll()` for multiple\n- Cache selections when reusing: `const bars = svg.selectAll(\".bar\")`\n- Use Canvas for 1000+ data points, SVG for interactive/detailed graphics\n- Debounce resize events for responsive charts\n- Use `d3.csv()`, `d3.json()` for async data loading\n- Leverage D3's built-in interpolators for smooth animations\n\n## Resources for Inspiration\n\n- [Observable D3 Gallery](https://observablehq.com/@d3/gallery)\n- [Bl.ocks.org](https://bl.ocks.org) - Community examples\n- [D3 Graph Gallery](https://d3-graph-gallery.com) - Chart type tutorials\n- [Mike Bostock's Blocks](https://bl.ocks.org/mbostock) - Creator's examples\n\n---\n\n_This reference focuses on practical patterns for creating impressive D3 demonstrations. Each example is self-contained and copy-pasteable for rapid prototyping._", | ||
| "three-js": "# Three.js API\n\n_Essential classes, methods, and patterns for Three.js development_\n\n## Core Setup\n\n### Scene Graph Hierarchy\n\n```javascript\nimport * as THREE from \"three\";\n\n// Core trinity\nconst scene = new THREE.Scene();\nconst camera = new THREE.PerspectiveCamera(75, aspect, 0.1, 1000);\nconst renderer = new THREE.WebGLRenderer({ antialias: true });\n\n// Everything is an Object3D\nscene.add(mesh); // Mesh extends Object3D\ngroup.add(light); // Light extends Object3D\nparent.add(child); // Hierarchical transforms\n```\n\n## Essential Classes\n\n### Cameras\n\n```javascript\n// Perspective (most common)\nconst camera = new THREE.PerspectiveCamera(\n 75, // field of view\n aspect, // aspect ratio\n 0.1, // near plane\n 1000, // far plane\n);\n\n// Orthographic (2D/technical)\nconst camera = new THREE.OrthographicCamera(\n left,\n right,\n top,\n bottom,\n near,\n far,\n);\n\n// Camera controls\ncamera.position.set(x, y, z);\ncamera.lookAt(target);\ncamera.updateProjectionMatrix(); // After changing properties\n```\n\n### Geometries\n\n```javascript\n// Primitive geometries\nconst box = new THREE.BoxGeometry(1, 1, 1);\nconst sphere = new THREE.SphereGeometry(1, 32, 32);\nconst plane = new THREE.PlaneGeometry(1, 1);\nconst cylinder = new THREE.CylinderGeometry(1, 1, 2, 32);\n\n// Custom geometry\nconst geometry = new THREE.BufferGeometry();\ngeometry.setAttribute(\"position\", new THREE.BufferAttribute(vertices, 3));\ngeometry.setAttribute(\"normal\", new THREE.BufferAttribute(normals, 3));\ngeometry.setAttribute(\"uv\", new THREE.BufferAttribute(uvs, 2));\ngeometry.setIndex(indices);\n```\n\n### Materials\n\n```javascript\n// Basic materials\nconst basic = new THREE.MeshBasicMaterial({ color: 0xff0000 });\nconst lambert = new THREE.MeshLambertMaterial({ color: 0x00ff00 });\nconst phong = new THREE.MeshPhongMaterial({ color: 0x0000ff });\n\n// PBR materials (most realistic)\nconst standard = new THREE.MeshStandardMaterial({\n color: 0xffffff,\n metalness: 0.5,\n roughness: 0.5,\n map: texture,\n normalMap: normalTexture,\n envMap: environmentTexture,\n});\n\nconst physical = new THREE.MeshPhysicalMaterial({\n ...standard,\n clearcoat: 1.0,\n transmission: 0.5,\n thickness: 1.0,\n});\n```\n\n### Lights\n\n```javascript\n// Ambient (global illumination)\nconst ambient = new THREE.AmbientLight(0xffffff, 0.6);\n\n// Directional (sun-like)\nconst directional = new THREE.DirectionalLight(0xffffff, 1);\ndirectional.position.set(1, 1, 1);\ndirectional.castShadow = true;\n\n// Point (bulb-like)\nconst point = new THREE.PointLight(0xffffff, 1, 100);\npoint.position.set(0, 10, 0);\n\n// Spot (flashlight-like)\nconst spot = new THREE.SpotLight(0xffffff, 1, 100, Math.PI / 4);\n```\n\n### Textures\n\n```javascript\n// Texture loading\nconst loader = new THREE.TextureLoader();\nconst texture = loader.load(\"path/to/texture.jpg\");\n\n// Texture properties\ntexture.wrapS = THREE.RepeatWrapping;\ntexture.wrapT = THREE.RepeatWrapping;\ntexture.repeat.set(2, 2);\ntexture.flipY = false;\n\n// HDR textures\nconst hdrLoader = new THREE.HDRLoader();\nconst envMap = hdrLoader.load(\"environment.hdr\");\nenvMap.mapping = THREE.EquirectangularReflectionMapping;\n```\n\n## Object3D Fundamentals\n\n### Transform Properties\n\n```javascript\n// Position\nobject.position.set(x, y, z);\nobject.position.copy(otherObject.position);\nobject.translateX(distance);\n\n// Rotation (Euler angles)\nobject.rotation.set(x, y, z);\nobject.rotation.y = Math.PI / 4;\nobject.rotateY(Math.PI / 4);\n\n// Scale\nobject.scale.set(2, 2, 2);\nobject.scale.multiplyScalar(0.5);\n\n// Quaternion (preferred for animations)\nobject.quaternion.setFromAxisAngle(axis, angle);\nobject.lookAt(target);\n```\n\n### Hierarchy Operations\n\n```javascript\n// Adding/removing children\nparent.add(child);\nparent.remove(child);\nscene.add(mesh, light, helper);\n\n// Traversal\nobject.traverse((child) => {\n if (child.isMesh) {\n child.material.wireframe = true;\n }\n});\n\n// Finding objects\nconst found = scene.getObjectByName(\"myObject\");\nconst found = scene.getObjectById(id);\n```\n\n## Math Utilities\n\n### Vectors\n\n```javascript\n// Vector3 (most common)\nconst v = new THREE.Vector3(1, 2, 3);\nv.add(otherVector);\nv.multiplyScalar(2);\nv.normalize();\nv.cross(otherVector);\nv.dot(otherVector);\nv.distanceTo(otherVector);\n\n// Vector2 (UV coordinates)\nconst uv = new THREE.Vector2(0.5, 0.5);\n```\n\n### Matrices\n\n```javascript\n// Matrix4 (transformations)\nconst matrix = new THREE.Matrix4();\nmatrix.makeTranslation(x, y, z);\nmatrix.makeRotationY(angle);\nmatrix.makeScale(x, y, z);\nmatrix.multiply(otherMatrix);\n\n// Apply to object\nobject.applyMatrix4(matrix);\n```\n\n### Colors\n\n```javascript\nconst color = new THREE.Color();\ncolor.set(0xff0000); // hex\ncolor.setRGB(1, 0, 0); // RGB values 0-1\ncolor.setHSL(0, 1, 0.5); // HSL values\ncolor.lerp(targetColor, 0.1); // interpolation\n```\n\n## Raycasting (Mouse Interaction)\n\n```javascript\nconst raycaster = new THREE.Raycaster();\nconst mouse = new THREE.Vector2();\n\nfunction onMouseClick(event) {\n // Normalize mouse coordinates (-1 to +1)\n mouse.x = (event.clientX / window.innerWidth) * 2 - 1;\n mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;\n\n // Cast ray from camera through mouse position\n raycaster.setFromCamera(mouse, camera);\n\n // Find intersections\n const intersects = raycaster.intersectObjects(scene.children, true);\n\n if (intersects.length > 0) {\n const object = intersects[0].object;\n const point = intersects[0].point;\n // Handle intersection\n }\n}\n```\n\n## Animation System\n\n### Animation Mixer\n\n```javascript\n// For GLTF animations\nconst mixer = new THREE.AnimationMixer(model);\nconst action = mixer.clipAction(animationClip);\naction.play();\n\n// Update in render loop\nfunction animate() {\n const delta = clock.getDelta();\n mixer.update(delta);\n renderer.render(scene, camera);\n}\n```\n\n### Manual Animation\n\n```javascript\nconst clock = new THREE.Clock();\n\nfunction animate() {\n const time = clock.getElapsedTime();\n\n // Rotate object\n mesh.rotation.y = time * 0.5;\n\n // Oscillate position\n mesh.position.y = Math.sin(time) * 2;\n\n renderer.render(scene, camera);\n}\nrenderer.setAnimationLoop(animate);\n```\n\n## Loading Assets\n\n### GLTF Models (Recommended)\n\n```javascript\nimport { GLTFLoader } from \"three/addons/loaders/GLTFLoader.js\";\n\nconst loader = new GLTFLoader();\nloader.load(\"model.gltf\", (gltf) => {\n const model = gltf.scene;\n scene.add(model);\n\n // Access animations\n if (gltf.animations.length > 0) {\n const mixer = new THREE.AnimationMixer(model);\n gltf.animations.forEach((clip) => {\n mixer.clipAction(clip).play();\n });\n }\n});\n```\n\n### Other Loaders\n\n```javascript\n// OBJ files\nimport { OBJLoader } from \"three/addons/loaders/OBJLoader.js\";\n\n// FBX files\nimport { FBXLoader } from \"three/addons/loaders/FBXLoader.js\";\n\n// Textures\nconst textureLoader = new THREE.TextureLoader();\nconst cubeLoader = new THREE.CubeTextureLoader();\n```\n\n## Renderer Configuration\n\n### Basic Setup\n\n```javascript\nconst renderer = new THREE.WebGLRenderer({\n canvas: canvasElement, // Existing canvas\n antialias: true, // Smooth edges\n alpha: true, // Transparent background\n powerPreference: \"high-performance\",\n});\n\nrenderer.setSize(width, height);\nrenderer.setPixelRatio(window.devicePixelRatio);\nrenderer.setClearColor(0x000000, 1);\n```\n\n### Advanced Settings\n\n```javascript\n// Shadows\nrenderer.shadowMap.enabled = true;\nrenderer.shadowMap.type = THREE.PCFSoftShadowMap;\n\n// Tone mapping (HDR)\nrenderer.toneMapping = THREE.ACESFilmicToneMapping;\nrenderer.toneMappingExposure = 1.0;\n\n// Color space\nrenderer.outputColorSpace = THREE.SRGBColorSpace;\n\n// Performance\nrenderer.setAnimationLoop(animate); // Preferred over requestAnimationFrame\n```\n\n## Common Patterns\n\n### Responsive Canvas\n\n```javascript\nfunction onWindowResize() {\n camera.aspect = window.innerWidth / window.innerHeight;\n camera.updateProjectionMatrix();\n renderer.setSize(window.innerWidth, window.innerHeight);\n}\nwindow.addEventListener(\"resize\", onWindowResize);\n```\n\n### Performance Optimization\n\n```javascript\n// Frustum culling\nobject.frustumCulled = true;\n\n// LOD (Level of Detail)\nconst lod = new THREE.LOD();\nlod.addLevel(highDetailMesh, 0);\nlod.addLevel(lowDetailMesh, 100);\n\n// Instancing for many objects\nconst instancedMesh = new THREE.InstancedMesh(geometry, material, count);\nconst matrix = new THREE.Matrix4();\nfor (let i = 0; i < count; i++) {\n matrix.setPosition(x, y, z);\n instancedMesh.setMatrixAt(i, matrix);\n}\ninstancedMesh.instanceMatrix.needsUpdate = true;\n```\n\n### Dispose Pattern (Memory Management)\n\n```javascript\n// Clean up resources\ngeometry.dispose();\nmaterial.dispose();\ntexture.dispose();\nrenderer.dispose();\n\n// Traverse and dispose\nobject.traverse((child) => {\n if (child.geometry) child.geometry.dispose();\n if (child.material) {\n if (Array.isArray(child.material)) {\n child.material.forEach((m) => m.dispose());\n } else {\n child.material.dispose();\n }\n }\n});\n```\n\n## Buffer Attributes (Advanced)\n\n### Custom Geometry Data\n\n```javascript\nconst geometry = new THREE.BufferGeometry();\n\n// Vertex positions (required)\nconst positions = new Float32Array([\n -1,\n -1,\n 0, // vertex 0\n 1,\n -1,\n 0, // vertex 1\n 0,\n 1,\n 0, // vertex 2\n]);\ngeometry.setAttribute(\"position\", new THREE.BufferAttribute(positions, 3));\n\n// Vertex colors\nconst colors = new Float32Array([\n 1,\n 0,\n 0, // red\n 0,\n 1,\n 0, // green\n 0,\n 0,\n 1, // blue\n]);\ngeometry.setAttribute(\"color\", new THREE.BufferAttribute(colors, 3));\n\n// Custom attributes for shaders\nconst customData = new Float32Array(vertexCount);\ngeometry.setAttribute(\n \"customAttribute\",\n new THREE.BufferAttribute(customData, 1),\n);\n```\n\n## Events and Interaction\n\n### Event Dispatcher\n\n```javascript\n// Custom events\nconst emitter = new THREE.EventDispatcher();\n\nemitter.addEventListener(\"customEvent\", (event) => {\n console.log(\"Event fired:\", event.data);\n});\n\nemitter.dispatchEvent({ type: \"customEvent\", data: \"hello\" });\n```\n\n### Built-in Events\n\n```javascript\n// Loading progress\nloader.onProgress = (progress) => {\n console.log(`Loading: ${(progress.loaded / progress.total) * 100}%`);\n};\n\n// Window resize\nwindow.addEventListener(\"resize\", onWindowResize);\n\n// Mouse events\ncanvas.addEventListener(\"click\", onMouseClick);\ncanvas.addEventListener(\"mousemove\", onMouseMove);\n```\n\n## Constants Reference\n\n### Material Constants\n\n```javascript\n// Blending modes\nTHREE.NormalBlending;\nTHREE.AdditiveBlending;\nTHREE.SubtractiveBlending;\nTHREE.MultiplyBlending;\n\n// Culling\nTHREE.FrontSide;\nTHREE.BackSide;\nTHREE.DoubleSide;\n\n// Depth modes\nTHREE.NeverDepth;\nTHREE.AlwaysDepth;\nTHREE.LessDepth;\nTHREE.LessEqualDepth;\n```\n\n### Texture Constants\n\n```javascript\n// Wrapping\nTHREE.RepeatWrapping;\nTHREE.ClampToEdgeWrapping;\nTHREE.MirroredRepeatWrapping;\n\n// Filtering\nTHREE.NearestFilter;\nTHREE.LinearFilter;\nTHREE.NearestMipmapNearestFilter;\nTHREE.LinearMipmapLinearFilter;\n\n// Formats\nTHREE.RGBAFormat;\nTHREE.RGBFormat;\nTHREE.RedFormat;\n```\n\n### Rendering Constants\n\n```javascript\n// Shadow types\nTHREE.BasicShadowMap;\nTHREE.PCFShadowMap;\nTHREE.PCFSoftShadowMap;\nTHREE.VSMShadowMap;\n\n// Tone mapping\nTHREE.NoToneMapping;\nTHREE.LinearToneMapping;\nTHREE.ReinhardToneMapping;\nTHREE.CineonToneMapping;\nTHREE.ACESFilmicToneMapping;\n```\n\n## Common Gotchas\n\n### Matrix Updates\n\n```javascript\n// Force matrix update after transform changes\nobject.updateMatrix();\nobject.updateMatrixWorld();\n\n// Automatic updates (default: true)\nobject.matrixAutoUpdate = false; // Manual control\n```\n\n### Geometry Modifications\n\n```javascript\n// After modifying geometry attributes\ngeometry.attributes.position.needsUpdate = true;\ngeometry.computeBoundingSphere();\ngeometry.computeBoundingBox();\n```\n\n### Material Updates\n\n```javascript\n// After changing material properties\nmaterial.needsUpdate = true;\n\n// Texture updates\ntexture.needsUpdate = true;\n```\n\n## Performance Tips\n\n### Efficient Rendering\n\n```javascript\n// Batch similar objects\nconst geometry = new THREE.InstancedBufferGeometry();\nconst material = new THREE.MeshStandardMaterial();\nconst instancedMesh = new THREE.InstancedMesh(geometry, material, 1000);\n\n// Freeze objects that don't move\nobject.matrixAutoUpdate = false;\nobject.updateMatrix();\n\n// Use appropriate geometry detail\nconst sphere = new THREE.SphereGeometry(1, 8, 6); // Low poly\nconst sphere = new THREE.SphereGeometry(1, 32, 32); // High poly\n```\n\n### Memory Management\n\n```javascript\n// Remove from scene\nscene.remove(object);\n\n// Dispose resources\nobject.traverse((child) => {\n if (child.geometry) child.geometry.dispose();\n if (child.material) child.material.dispose();\n});\n\n// Clear references\nobject = null;\n```\n\n## Quick Reference\n\n### Essential Imports\n\n```javascript\n// Core\nimport * as THREE from \"three\";\n\n// Controls\nimport { OrbitControls } from \"three/addons/controls/OrbitControls.js\";\nimport { FlyControls } from \"three/addons/controls/FlyControls.js\";\n\n// Loaders\nimport { GLTFLoader } from \"three/addons/loaders/GLTFLoader.js\";\nimport { OBJLoader } from \"three/addons/loaders/OBJLoader.js\";\n\n// Post-processing\nimport { EffectComposer } from \"three/addons/postprocessing/EffectComposer.js\";\n\n// Helpers\nimport { GUI } from \"three/addons/libs/lil-gui.module.min.js\";\nimport Stats from \"three/addons/libs/stats.module.js\";\n```\n\n### Minimal Working Example\n\n```javascript\nimport * as THREE from \"three\";\n\nconst scene = new THREE.Scene();\nconst camera = new THREE.PerspectiveCamera(\n 75,\n window.innerWidth / window.innerHeight,\n);\nconst renderer = new THREE.WebGLRenderer();\n\nrenderer.setSize(window.innerWidth, window.innerHeight);\ndocument.body.appendChild(renderer.domElement);\n\nconst geometry = new THREE.BoxGeometry();\nconst material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });\nconst cube = new THREE.Mesh(geometry, material);\nscene.add(cube);\n\ncamera.position.z = 5;\n\nfunction animate() {\n cube.rotation.x += 0.01;\n cube.rotation.y += 0.01;\n renderer.render(scene, camera);\n}\nrenderer.setAnimationLoop(animate);\n```\n\n---\n\n# Three.js Condensed Guide: Most Impressive Examples\n\n_A curated collection of Three.js's most visually stunning and technically advanced examples_\n\n## Quick Start Template\n\n```javascript\nimport * as THREE from \"three\";\nimport { OrbitControls } from \"three/addons/controls/OrbitControls.js\";\n\n// Basic setup\nconst scene = new THREE.Scene();\nconst camera = new THREE.PerspectiveCamera(\n 75,\n window.innerWidth / window.innerHeight,\n 0.1,\n 1000,\n);\nconst renderer = new THREE.WebGLRenderer({ antialias: true });\nrenderer.setSize(window.innerWidth, window.innerHeight);\ndocument.body.appendChild(renderer.domElement);\n\n// Controls\nconst controls = new OrbitControls(camera, renderer.domElement);\ncamera.position.set(5, 5, 5);\ncontrols.update();\n\n// Animation loop\nfunction animate() {\n controls.update();\n renderer.render(scene, camera);\n}\nrenderer.setAnimationLoop(animate);\n```\n\n## 1. Spectacular Visual Effects\n\n### Galaxy Generator (WebGPU + TSL)\n\nCreates a procedural spiral galaxy with thousands of animated particles.\n\n```javascript\nimport * as THREE from \"three/webgpu\";\nimport { color, cos, sin, time, uniform, range, vec3, PI2 } from \"three/tsl\";\n\nconst material = new THREE.SpriteNodeMaterial({\n depthWrite: false,\n blending: THREE.AdditiveBlending,\n});\n\n// Procedural galaxy structure\nconst radiusRatio = range(0, 1);\nconst radius = radiusRatio.pow(1.5).mul(5);\nconst branches = 3;\nconst branchAngle = range(0, branches).floor().mul(PI2.div(branches));\nconst angle = branchAngle.add(time.mul(radiusRatio.oneMinus()));\n\nconst position = vec3(cos(angle), 0, sin(angle)).mul(radius);\nmaterial.positionNode = position.add(randomOffset);\n\n// Dynamic colors\nconst colorInside = uniform(color(\"#ffa575\"));\nconst colorOutside = uniform(color(\"#311599\"));\nmaterial.colorNode = mix(colorInside, colorOutside, radiusRatio);\n\nconst galaxy = new THREE.InstancedMesh(\n new THREE.PlaneGeometry(1, 1),\n material,\n 20000,\n);\n```\n\n### Ocean Shaders\n\nRealistic water simulation with dynamic waves and sky reflections.\n\n```javascript\nimport { Water } from \"three/addons/objects/Water.js\";\nimport { Sky } from \"three/addons/objects/Sky.js\";\n\nconst waterGeometry = new THREE.PlaneGeometry(10000, 10000);\nconst water = new Water(waterGeometry, {\n textureWidth: 512,\n textureHeight: 512,\n waterNormals: new THREE.TextureLoader().load(\"textures/waternormals.jpg\"),\n sunDirection: new THREE.Vector3(),\n sunColor: 0xffffff,\n waterColor: 0x001e0f,\n distortionScale: 3.7,\n});\n\n// Sky system\nconst sky = new Sky();\nsky.scale.setScalar(10000);\nconst skyUniforms = sky.material.uniforms;\nskyUniforms[\"turbidity\"].value = 10;\nskyUniforms[\"rayleigh\"].value = 2;\n```\n\n### Unreal Bloom Effect\n\nCinematic glow and HDR post-processing.\n\n```javascript\nimport { EffectComposer } from \"three/addons/postprocessing/EffectComposer.js\";\nimport { RenderPass } from \"three/addons/postprocessing/RenderPass.js\";\nimport { UnrealBloomPass } from \"three/addons/postprocessing/UnrealBloomPass.js\";\n\nconst composer = new EffectComposer(renderer);\nconst renderPass = new RenderPass(scene, camera);\ncomposer.addPass(renderPass);\n\nconst bloomPass = new UnrealBloomPass(\n new THREE.Vector2(window.innerWidth, window.innerHeight),\n 1.5, // strength\n 0.4, // radius\n 0.85, // threshold\n);\ncomposer.addPass(bloomPass);\n\n// Render with bloom\ncomposer.render();\n```\n\n## 2. Advanced GPU Computing\n\n### Flocking Birds (GPGPU)\n\nGPU-accelerated boid simulation with emergent flocking behavior.\n\n```javascript\n// Position computation shader\nconst fragmentShaderPosition = `\nuniform float time;\nuniform float delta;\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / resolution.xy;\n vec4 tmpPos = texture2D(texturePosition, uv);\n vec3 position = tmpPos.xyz;\n vec3 velocity = texture2D(textureVelocity, uv).xyz;\n \n gl_FragColor = vec4(position + velocity * delta * 15.0, tmpPos.w);\n}`;\n\n// Velocity computation (separation, alignment, cohesion)\nconst fragmentShaderVelocity = `\nuniform float separationDistance;\nuniform float alignmentDistance; \nuniform float cohesionDistance;\nuniform vec3 predator;\n\nvoid main() {\n // Boid algorithm implementation\n // ...separation, alignment, cohesion logic\n}`;\n```\n\n### Cloth Physics (WebGPU Compute)\n\nReal-time fabric simulation using compute shaders.\n\n```javascript\nimport { Fn, uniform, attribute, Loop } from \"three/tsl\";\n\n// Verlet integration in compute shader\nconst computeVertexForces = Fn(() => {\n const position = attribute(\"position\");\n const velocity = attribute(\"velocity\");\n\n // Spring forces, wind, gravity\n const force = uniform(\"wind\").add(uniform(\"gravity\"));\n\n // Verlet integration\n const newPosition = position.add(velocity.mul(uniform(\"deltaTime\")));\n\n return newPosition;\n})();\n\nconst clothMaterial = new THREE.MeshPhysicalMaterial({\n color: 0x204080,\n roughness: 0.8,\n transmission: 0.2,\n sheen: 0.5,\n});\n```\n\n## 3. Impressive 3D Scenes\n\n### Photorealistic Car\n\nAdvanced PBR materials with interactive customization.\n\n```javascript\nimport { GLTFLoader } from \"three/addons/loaders/GLTFLoader.js\";\nimport { HDRLoader } from \"three/addons/loaders/HDRLoader.js\";\n\n// Environment setup\nscene.environment = new HDRLoader().load(\n \"textures/equirectangular/venice_sunset_1k.hdr\",\n);\nrenderer.toneMapping = THREE.ACESFilmicToneMapping;\nrenderer.toneMappingExposure = 0.85;\n\n// Load car model\nconst loader = new GLTFLoader();\nconst gltf = await loader.loadAsync(\"models/gltf/ferrari.glb\");\n\n// Material customization\ngltf.scene.traverse((child) => {\n if (child.isMesh && child.material.name === \"body\") {\n child.material.color.setHex(bodyColor);\n child.material.metalness = 1.0;\n child.material.roughness = 0.5;\n child.material.clearcoat = 1.0;\n }\n});\n```\n\n### Minecraft World Generator\n\nProcedural voxel terrain with optimized geometry merging.\n\n```javascript\nimport { ImprovedNoise } from \"three/addons/math/ImprovedNoise.js\";\nimport * as BufferGeometryUtils from \"three/addons/utils/BufferGeometryUtils.js\";\n\nfunction generateTerrain(width, depth) {\n const noise = new ImprovedNoise();\n const data = [];\n\n for (let x = 0; x < width; x++) {\n for (let z = 0; z < depth; z++) {\n // Multi-octave noise\n const height =\n noise.noise(x / 100, z / 100, 0) * 50 +\n noise.noise(x / 50, z / 50, 0) * 25;\n data.push(Math.floor(height));\n }\n }\n\n return data;\n}\n\n// Merge geometries for performance\nconst geometries = [];\n// ...create individual cube geometries\nconst mergedGeometry = BufferGeometryUtils.mergeGeometries(geometries);\n```\n\n## 4. Interactive Experiences\n\n### VR Painting\n\nVirtual reality 3D painting with hand tracking.\n\n```javascript\n// WebXR setup\nrenderer.xr.enabled = true;\ndocument.body.appendChild(VRButton.createButton(renderer));\n\n// Hand input\nconst controller1 = renderer.xr.getController(0);\nconst controller2 = renderer.xr.getController(1);\n\ncontroller1.addEventListener(\"selectstart\", onSelectStart);\ncontroller1.addEventListener(\"selectend\", onSelectEnd);\n\nfunction onSelectStart(event) {\n // Start painting stroke\n const geometry = new THREE.BufferGeometry();\n const material = new THREE.LineBasicMaterial({\n color: currentColor,\n linewidth: brushSize,\n });\n const line = new THREE.Line(geometry, material);\n scene.add(line);\n}\n```\n\n### Physics Vehicle Controller\n\nReal-time vehicle physics with Rapier.js integration.\n\n```javascript\nimport { World } from \"@dimforge/rapier3d-compat\";\n\n// Physics world\nconst world = new World({ x: 0, y: -9.81, z: 0 });\n\n// Vehicle setup\nconst vehicleDesc = world.createRigidBody({\n type: \"dynamic\",\n translation: { x: 0, y: 1, z: 0 },\n});\n\n// Wheel constraints\nwheels.forEach((wheel, index) => {\n const wheelJoint = world.createImpulseJoint(\n vehicleDesc,\n wheel.body,\n wheelConstraints[index],\n );\n});\n```\n\n## 5. Cutting-Edge WebGPU Features\n\n### Path Tracing\n\nRealistic ray-traced lighting with global illumination.\n\n```javascript\nimport { PathTracingRenderer } from \"three/addons/renderers/PathTracingRenderer.js\";\n\nconst ptRenderer = new PathTracingRenderer(renderer);\nptRenderer.setSize(window.innerWidth, window.innerHeight);\n\n// Progressive rendering\nlet sampleCount = 0;\nfunction animate() {\n if (sampleCount < 1000) {\n ptRenderer.update();\n sampleCount++;\n }\n}\n```\n\n### TSL (Three.js Shading Language)\n\nModern node-based shader programming.\n\n```javascript\nimport { mix, noise, time, uv, vec3, sin, cos } from \"three/tsl\";\n\n// Procedural materials with TSL\nconst proceduralMaterial = new THREE.MeshStandardNodeMaterial();\n\n// Animated noise texture\nconst noiseValue = noise(uv().mul(10).add(time.mul(0.1)));\nconst colorA = vec3(1, 0.5, 0.2);\nconst colorB = vec3(0.2, 0.5, 1);\n\nproceduralMaterial.colorNode = mix(colorA, colorB, noiseValue);\nproceduralMaterial.roughnessNode = noiseValue.mul(0.5).add(0.3);\n```\n\n## Performance Tips for Impressive Results\n\n### Instancing for Massive Scenes\n\n```javascript\nconst instancedMesh = new THREE.InstancedMesh(geometry, material, 100000);\nconst matrix = new THREE.Matrix4();\n\nfor (let i = 0; i < instancedMesh.count; i++) {\n matrix.setPosition(\n Math.random() * 2000 - 1000,\n Math.random() * 2000 - 1000,\n Math.random() * 2000 - 1000,\n );\n instancedMesh.setMatrixAt(i, matrix);\n}\n```\n\n### LOD for Complex Models\n\n```javascript\nconst lod = new THREE.LOD();\nlod.addLevel(highDetailMesh, 0);\nlod.addLevel(mediumDetailMesh, 50);\nlod.addLevel(lowDetailMesh, 200);\n```\n\n### Render Targets for Effects\n\n```javascript\nconst renderTarget = new THREE.WebGLRenderTarget(1024, 1024);\nrenderer.setRenderTarget(renderTarget);\nrenderer.render(effectScene, effectCamera);\nrenderer.setRenderTarget(null);\n\n// Use render target as texture\nmaterial.map = renderTarget.texture;\n```\n\n## Essential Setup for Maximum Impact\n\n### HDR Environment\n\n```javascript\nimport { HDRLoader } from \"three/addons/loaders/HDRLoader.js\";\n\nconst hdrTexture = new HDRLoader().load(\"environment.hdr\");\nhdrTexture.mapping = THREE.EquirectangularReflectionMapping;\nscene.environment = hdrTexture;\nscene.background = hdrTexture;\n```\n\n### Tone Mapping\n\n```javascript\nrenderer.toneMapping = THREE.ACESFilmicToneMapping;\nrenderer.toneMappingExposure = 1.0;\nrenderer.outputColorSpace = THREE.SRGBColorSpace;\n```\n\n### Post-Processing Chain\n\n```javascript\nimport { EffectComposer } from \"three/addons/postprocessing/EffectComposer.js\";\n\nconst composer = new EffectComposer(renderer);\ncomposer.addPass(new RenderPass(scene, camera));\ncomposer.addPass(new UnrealBloomPass(resolution, strength, radius, threshold));\ncomposer.addPass(new OutputPass());\n```\n\n---\n\n_This guide focuses on Three.js's most impressive capabilities. Each example demonstrates advanced techniques that create visually stunning results with minimal code complexity._\n\n# Real world example\n\n```javascript\nimport React, { useState, useEffect, useRef, useCallback } from \"react\";\nimport { useFireproof } from \"use-fireproof\";\nimport * as THREE from \"three\";\n\nexport default function SkyGlider() {\n const { database, useLiveQuery } = useFireproof(\"sky-glider-scores\");\n const canvasRef = useRef(null);\n const gameStateRef = useRef({\n scene: null,\n camera: null,\n renderer: null,\n glider: null,\n clouds: [],\n coins: [],\n glowEffects: [],\n smokeTrail: [],\n lastSmokeTime: 0,\n score: 0,\n gameRunning: false,\n keys: {},\n velocity: { x: 0, y: 0, z: 0 },\n heading: 0,\n forwardSpeed: 0,\n pitch: 0,\n roll: 0,\n });\n\n const [currentScore, setCurrentScore] = useState(0);\n const { docs: scoreData } = useLiveQuery(\"type\", { key: \"score\" }) || {\n docs: [],\n };\n\n const saveScore = useCallback(\n async (score) => {\n await database.put({\n _id: `score-${Date.now()}`,\n type: \"score\",\n value: score,\n timestamp: Date.now(),\n });\n },\n [database],\n );\n\n const createGlowEffect = useCallback((position) => {\n const state = gameStateRef.current;\n if (!state.scene) return;\n\n const glowSphere = new THREE.Mesh(\n new THREE.SphereGeometry(8, 16, 16),\n new THREE.MeshBasicMaterial({\n color: 0xffd670,\n transparent: true,\n opacity: 0.8,\n }),\n );\n\n glowSphere.position.copy(position);\n state.scene.add(glowSphere);\n\n const glowEffect = {\n mesh: glowSphere,\n createdAt: Date.now(),\n scale: 1,\n };\n\n state.glowEffects.push(glowEffect);\n\n // Remove after animation\n setTimeout(() => {\n state.scene.remove(glowSphere);\n const index = state.glowEffects.indexOf(glowEffect);\n if (index > -1) state.glowEffects.splice(index, 1);\n }, 1000);\n }, []);\n\n const createSmokeCloud = useCallback((position) => {\n const state = gameStateRef.current;\n if (!state.scene) return;\n\n const smokeGeometry = new THREE.SphereGeometry(\n 0.1 + Math.random() * 0.05,\n 4,\n 3,\n );\n const smokeMaterial = new THREE.MeshLambertMaterial({\n color: 0x242424,\n transparent: true,\n opacity: 0.7 + Math.random() * 0.2,\n });\n const smokeCloud = new THREE.Mesh(smokeGeometry, smokeMaterial);\n\n // Position behind the glider\n const heading = state.heading;\n const offsetX = Math.sin(heading) * -4;\n const offsetZ = Math.cos(heading) * -4;\n\n smokeCloud.position.set(\n position.x + offsetX + (Math.random() - 0.5) * 0.2,\n position.y - 0.2 + (Math.random() - 0.5) * 0.1,\n position.z + offsetZ + Math.random() * 0.3,\n );\n\n state.scene.add(smokeCloud);\n state.smokeTrail.push({\n mesh: smokeCloud,\n createdAt: Date.now(),\n });\n\n // Keep trail manageable\n while (state.smokeTrail.length > 100) {\n const oldSmoke = state.smokeTrail.shift();\n state.scene.remove(oldSmoke.mesh);\n }\n }, []);\n\n const createTexturedCoin = useCallback((scene, position) => {\n // Create procedural gold texture\n const canvas = document.createElement(\"canvas\");\n canvas.width = canvas.height = 128;\n const ctx = canvas.getContext(\"2d\");\n\n // Gold gradient\n const gradient = ctx.createRadialGradient(64, 64, 20, 64, 64, 64);\n gradient.addColorStop(0, \"#ffd670\");\n gradient.addColorStop(0.5, \"#ff9770\");\n gradient.addColorStop(1, \"#ffb347\");\n ctx.fillStyle = gradient;\n ctx.fillRect(0, 0, 128, 128);\n\n // Add metallic shine lines\n ctx.strokeStyle = \"#ffffff\";\n ctx.lineWidth = 2;\n ctx.setLineDash([5, 3]);\n for (let i = 0; i < 8; i++) {\n const angle = (i / 8) * Math.PI * 2;\n ctx.beginPath();\n ctx.moveTo(64 + Math.cos(angle) * 30, 64 + Math.sin(angle) * 30);\n ctx.lineTo(64 + Math.cos(angle) * 50, 64 + Math.sin(angle) * 50);\n ctx.stroke();\n }\n\n const texture = new THREE.CanvasTexture(canvas);\n const coin = new THREE.Mesh(\n new THREE.CylinderGeometry(2, 2, 0.3, 16),\n new THREE.MeshStandardMaterial({\n map: texture,\n metalness: 0.8,\n roughness: 0.2,\n }),\n );\n\n coin.position.copy(position);\n coin.rotation.z = Math.PI / 2;\n scene.add(coin);\n\n return {\n mesh: coin,\n collected: false,\n rotation: Math.random() * 0.02 + 0.01,\n };\n }, []);\n\n const initThreeJS = useCallback(() => {\n if (!canvasRef.current) return;\n\n const scene = new THREE.Scene();\n scene.background = new THREE.Color(0x70d6ff);\n scene.fog = new THREE.Fog(0x70d6ff, 50, 300);\n\n const camera = new THREE.PerspectiveCamera(\n 75,\n window.innerWidth / window.innerHeight,\n 0.1,\n 1000,\n );\n camera.position.set(0, 10, 20);\n\n const renderer = new THREE.WebGLRenderer({\n canvas: canvasRef.current,\n antialias: true,\n });\n renderer.setSize(window.innerWidth, window.innerHeight);\n\n // Lighting\n const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);\n scene.add(ambientLight);\n const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);\n directionalLight.position.set(50, 100, 50);\n scene.add(directionalLight);\n\n // Glider\n const glider = new THREE.Group();\n const body = new THREE.Mesh(\n new THREE.ConeGeometry(2, 8, 3),\n new THREE.MeshLambertMaterial({ color: 0xff70a6 }),\n );\n body.rotation.x = Math.PI / 2;\n glider.add(body);\n\n glider.position.set(0, 10, 0);\n scene.add(glider);\n\n // Create simple clouds\n const clouds = [];\n for (let i = 0; i < 30; i++) {\n const cloud = new THREE.Mesh(\n new THREE.SphereGeometry(Math.random() * 5 + 3, 8, 6),\n new THREE.MeshLambertMaterial({\n color: 0xffffff,\n transparent: true,\n opacity: 0.7,\n }),\n );\n cloud.position.set(\n (Math.random() - 0.5) * 400,\n Math.random() * 30 + 10,\n (Math.random() - 0.5) * 400,\n );\n scene.add(cloud);\n clouds.push({\n mesh: cloud,\n drift: {\n x: (Math.random() - 0.5) * 0.01,\n y: 0,\n z: (Math.random() - 0.5) * 0.01,\n },\n });\n }\n\n // Create initial coins\n const coins = [];\n for (let i = 0; i < 20; i++) {\n const coin = createTexturedCoin(\n scene,\n new THREE.Vector3(\n (Math.random() - 0.5) * 200,\n Math.random() * 40 + 10,\n (Math.random() - 0.5) * 200,\n ),\n );\n coins.push(coin);\n }\n\n gameStateRef.current = {\n ...gameStateRef.current,\n scene,\n camera,\n renderer,\n glider,\n clouds,\n coins,\n lastSmokeTime: Date.now(),\n heading: 0,\n forwardSpeed: 0.1,\n pitch: 0,\n roll: 0,\n };\n\n const gameLoop = () => {\n if (gameStateRef.current.gameRunning) {\n updateGame();\n requestAnimationFrame(gameLoop);\n }\n };\n gameStateRef.current.gameRunning = true;\n gameLoop();\n }, [createTexturedCoin]);\n\n const checkCoinCollisions = useCallback(() => {\n const state = gameStateRef.current;\n if (!state.glider) return;\n\n state.coins.forEach((coin) => {\n if (!coin.collected) {\n const distance = state.glider.position.distanceTo(coin.mesh.position);\n if (distance < 4) {\n coin.collected = true;\n coin.mesh.visible = false;\n createGlowEffect(coin.mesh.position);\n state.score += 1;\n setCurrentScore(state.score);\n\n // Respawn coin at random location\n setTimeout(() => {\n coin.mesh.position.set(\n (Math.random() - 0.5) * 200,\n Math.random() * 40 + 10,\n (Math.random() - 0.5) * 200,\n );\n coin.mesh.visible = true;\n coin.collected = false;\n }, 5000);\n }\n }\n });\n }, [createGlowEffect]);\n\n const handleKeyDown = useCallback((event) => {\n if (event.code === \"Space\") event.preventDefault();\n gameStateRef.current.keys[event.code] = true;\n }, []);\n\n const handleKeyUp = useCallback((event) => {\n if (event.code === \"Space\") event.preventDefault();\n gameStateRef.current.keys[event.code] = false;\n }, []);\n\n const updateGame = useCallback(() => {\n const state = gameStateRef.current;\n if (!state.gameRunning || !state.glider) return;\n\n const { keys, glider } = state;\n\n // Controls\n if (keys[\"ArrowLeft\"] || keys[\"KeyA\"]) state.heading += 0.03;\n if (keys[\"ArrowRight\"] || keys[\"KeyD\"]) state.heading -= 0.03;\n if (keys[\"ArrowUp\"] || keys[\"KeyW\"]) state.pitch += 0.01;\n if (keys[\"ArrowDown\"] || keys[\"KeyS\"]) state.pitch -= 0.01;\n if (keys[\"Space\"])\n state.forwardSpeed = Math.min(0.3, state.forwardSpeed + 0.005);\n\n // Physics\n state.forwardSpeed = Math.max(0.05, state.forwardSpeed * 0.995);\n state.velocity.x =\n Math.sin(state.heading) * Math.cos(state.pitch) * state.forwardSpeed;\n state.velocity.y = Math.sin(-state.pitch) * state.forwardSpeed;\n state.velocity.z =\n Math.cos(state.heading) * Math.cos(state.pitch) * state.forwardSpeed;\n\n glider.position.add(\n new THREE.Vector3(state.velocity.x, state.velocity.y, state.velocity.z),\n );\n\n // Point glider in thrust vector direction\n const thrustDirection = new THREE.Vector3(\n state.velocity.x,\n state.velocity.y,\n state.velocity.z,\n ).normalize();\n if (thrustDirection.length() > 0) {\n glider.lookAt(glider.position.clone().add(thrustDirection));\n }\n\n // Camera follow\n const cameraDistance = 15;\n state.camera.position.set(\n glider.position.x - Math.sin(state.heading) * cameraDistance,\n glider.position.y + 10,\n glider.position.z - Math.cos(state.heading) * cameraDistance,\n );\n state.camera.lookAt(glider.position);\n\n // Create smoke trail\n const currentTime = Date.now();\n const timeSinceLastSmoke = currentTime - state.lastSmokeTime;\n const smokeInterval = 150 + Math.random() * 200;\n\n if (timeSinceLastSmoke > smokeInterval) {\n createSmokeCloud(glider.position);\n state.lastSmokeTime = currentTime;\n }\n\n // Animate\n checkCoinCollisions();\n state.coins.forEach((coin) => {\n if (!coin.collected) coin.mesh.rotation.y += coin.rotation;\n });\n state.clouds.forEach((cloud) => {\n cloud.mesh.position.add(\n new THREE.Vector3(cloud.drift.x, cloud.drift.y, cloud.drift.z),\n );\n });\n\n // Animate glow effects\n state.glowEffects.forEach((effect) => {\n const age = Date.now() - effect.createdAt;\n const progress = age / 1000;\n effect.scale = 1 + progress * 2;\n effect.mesh.scale.setScalar(effect.scale);\n effect.mesh.material.opacity = 0.8 * (1 - progress);\n });\n\n // Fade smoke trail\n state.smokeTrail.forEach((smoke) => {\n const age = currentTime - smoke.createdAt;\n const maxAge = 15000;\n if (age > maxAge) {\n smoke.mesh.material.opacity = 0;\n } else if (age > 7500) {\n const fadeProgress = (age - 7500) / 7500;\n smoke.mesh.material.opacity =\n (0.7 + Math.random() * 0.2) * (1 - fadeProgress);\n }\n });\n\n state.renderer.render(state.scene, state.camera);\n }, [checkCoinCollisions, createSmokeCloud]);\n\n useEffect(() => {\n initThreeJS();\n window.addEventListener(\"keydown\", handleKeyDown);\n window.addEventListener(\"keyup\", handleKeyUp);\n return () => {\n window.removeEventListener(\"keydown\", handleKeyDown);\n window.removeEventListener(\"keyup\", handleKeyUp);\n gameStateRef.current.gameRunning = false;\n };\n }, [initThreeJS, handleKeyDown, handleKeyUp]);\n\n return (\n <div className=\"relative h-screen w-full bg-sky-200\">\n <canvas ref={canvasRef} className=\"absolute inset-0\" />\n <div className=\"absolute top-4 left-4 rounded bg-white p-4 shadow\">\n <h2 className=\"text-lg font-bold\">Sky Glider</h2>\n <p>Score: {currentScore}</p>\n <p className=\"mt-2 text-sm\">WASD/Arrows: Fly, Space: Thrust</p>\n </div>\n {scoreData.length > 0 && (\n <div className=\"absolute top-4 right-4 rounded bg-white p-4 shadow\">\n <h3 className=\"font-bold\">High Scores</h3>\n {scoreData\n .sort((a, b) => b.value - a.value)\n .slice(0, 3)\n .map((score, i) => (\n <div key={score._id} className=\"text-sm\">\n #{i + 1}: {score.value}\n </div>\n ))}\n </div>\n )}\n </div>\n );\n}\n```\n\n# Visual effects example\n\n```javascript\nimport React, { useState, useEffect, useRef, useCallback } from \"react\";\nimport { useFireproof } from \"use-fireproof\";\nimport * as THREE from \"three\";\nimport { OrbitControls } from \"three/addons/controls/OrbitControls.js\";\nimport { EffectComposer } from \"three/addons/postprocessing/EffectComposer.js\";\nimport { RenderPass } from \"three/addons/postprocessing/RenderPass.js\";\nimport { HalftonePass } from \"three/addons/postprocessing/HalftonePass.js\";\n\nexport default function HalftoneArtStudio() {\n const { database, useLiveQuery } = useFireproof(\"halftone-studio\");\n const canvasRef = useRef(null);\n const sceneRef = useRef(null);\n const [currentPreset, setCurrentPreset] = useState(null);\n const [presetName, setPresetName] = useState(\"\");\n const [isGenerating, setIsGenerating] = useState(false);\n const [showParameters, setShowParameters] = useState(false);\n\n const { docs: presets } = useLiveQuery(\"type\", { key: \"preset\" }) || {\n docs: [],\n };\n const { docs: parameterHistory } = useLiveQuery(\"type\", {\n key: \"parameter-state\",\n }) || {\n docs: [],\n };\n\n const [parameters, setParameters] = useState({\n shape: 1, // 1=Dot, 2=Ellipse, 3=Line, 4=Square\n radius: 4,\n rotateR: 15,\n rotateG: 30,\n rotateB: 45,\n scatter: 0,\n blending: 1,\n blendingMode: 1, // 1=Linear, 2=Multiply, 3=Add, 4=Lighter, 5=Darker\n greyscale: false,\n disable: false,\n objectCount: 25,\n rotationSpeed: 1,\n colorTheme: 0, // 0=Rainbow, 1=Warm, 2=Cool, 3=Monochrome\n });\n\n const saveParameterState = useCallback(\n async (params, action = \"manual\") => {\n await database.put({\n _id: `param-state-${Date.now()}`,\n type: \"parameter-state\",\n parameters: { ...params },\n action,\n timestamp: Date.now(),\n });\n },\n [database],\n );\n\n const savePreset = useCallback(async () => {\n if (!presetName.trim()) return;\n\n await database.put({\n _id: `preset-${Date.now()}`,\n type: \"preset\",\n name: presetName,\n parameters: { ...parameters },\n timestamp: Date.now(),\n });\n\n setPresetName(\"\");\n }, [database, presetName, parameters]);\n\n const loadPreset = useCallback((preset) => {\n setParameters({ ...preset.parameters });\n setCurrentPreset(preset);\n }, []);\n\n const loadParameterState = useCallback((state) => {\n setParameters({ ...state.parameters });\n }, []);\n\n const generateRandomScene = useCallback(async () => {\n setIsGenerating(true);\n\n // Save current state before randomizing\n await saveParameterState(parameters, \"before-randomize\");\n\n // Generate random parameters\n const newParams = {\n shape: Math.floor(Math.random() * 4) + 1,\n radius: Math.random() * 20 + 2,\n rotateR: Math.random() * 90,\n rotateG: Math.random() * 90,\n rotateB: Math.random() * 90,\n scatter: Math.random(),\n blending: Math.random(),\n blendingMode: Math.floor(Math.random() * 5) + 1,\n greyscale: Math.random() > 0.7,\n disable: false,\n objectCount: Math.floor(Math.random() * 40) + 10,\n rotationSpeed: Math.random() * 3 + 0.5,\n colorTheme: Math.floor(Math.random() * 4),\n };\n\n setParameters(newParams);\n\n // Save the new randomized state\n setTimeout(async () => {\n await saveParameterState(newParams, \"randomized\");\n setIsGenerating(false);\n }, 500);\n }, [parameters, saveParameterState]);\n\n // Save parameter changes for history\n useEffect(() => {\n const timeoutId = setTimeout(() => {\n saveParameterState(parameters, \"manual\");\n }, 1000);\n\n return () => clearTimeout(timeoutId);\n }, [parameters, saveParameterState]);\n\n useEffect(() => {\n if (!canvasRef.current) return;\n\n // Scene setup\n const scene = new THREE.Scene();\n scene.background = new THREE.Color(0x242424);\n\n const camera = new THREE.PerspectiveCamera(\n 75,\n window.innerWidth / window.innerHeight,\n 1,\n 1000,\n );\n camera.position.z = 12;\n\n const renderer = new THREE.WebGLRenderer({\n canvas: canvasRef.current,\n antialias: true,\n preserveDrawingBuffer: true,\n });\n renderer.setPixelRatio(window.devicePixelRatio);\n renderer.setSize(window.innerWidth, window.innerHeight);\n\n // Controls\n const controls = new OrbitControls(camera, renderer.domElement);\n controls.enableDamping = true;\n controls.dampingFactor = 0.05;\n controls.autoRotate = true;\n controls.autoRotateSpeed = 0.5;\n\n // Group for all objects\n const group = new THREE.Group();\n scene.add(group);\n\n // Lighting\n const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);\n scene.add(ambientLight);\n\n const pointLight = new THREE.PointLight(0xffffff, 1, 100);\n pointLight.position.set(10, 10, 10);\n scene.add(pointLight);\n\n // Post-processing\n const composer = new EffectComposer(renderer);\n const renderPass = new RenderPass(scene, camera);\n composer.addPass(renderPass);\n\n const halftonePass = new HalftonePass({\n shape: parameters.shape,\n radius: parameters.radius,\n rotateR: parameters.rotateR * (Math.PI / 180),\n rotateG: parameters.rotateG * (Math.PI / 180),\n rotateB: parameters.rotateB * (Math.PI / 180),\n scatter: parameters.scatter,\n blending: parameters.blending,\n blendingMode: parameters.blendingMode,\n greyscale: parameters.greyscale,\n disable: parameters.disable,\n });\n composer.addPass(halftonePass);\n\n // Store refs\n sceneRef.current = {\n scene,\n camera,\n renderer,\n composer,\n halftonePass,\n group,\n controls,\n objects: [],\n };\n\n // Create initial objects\n const createObjects = () => {\n // Clear existing objects\n sceneRef.current.objects.forEach((obj) => {\n group.remove(obj);\n });\n sceneRef.current.objects = [];\n\n // Color themes\n const colorThemes = [\n [0xff70a6, 0x70d6ff, 0xffd670, 0xe9ff70, 0xff9770], // Rainbow\n [0xff9770, 0xffd670, 0xff70a6], // Warm\n [0x70d6ff, 0xe9ff70, 0x242424], // Cool\n [0xffffff, 0x242424], // Monochrome\n ];\n\n const colors = colorThemes[parameters.colorTheme] || colorThemes[0];\n\n // Shader material for interesting effects\n const material = new THREE.ShaderMaterial({\n uniforms: {\n time: { value: 0 },\n },\n vertexShader: `\n varying vec2 vUv;\n varying vec3 vNormal;\n varying vec3 vPosition;\n uniform float time;\n \n void main() {\n vUv = uv;\n vNormal = normalize(normalMatrix * normal);\n vPosition = position;\n \n vec3 pos = position;\n pos += sin(pos * 2.0 + time) * 0.1;\n \n gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);\n }\n `,\n fragmentShader: `\n varying vec2 vUv;\n varying vec3 vNormal;\n varying vec3 vPosition;\n uniform float time;\n \n void main() {\n vec3 color = abs(vNormal) + vec3(vUv, sin(time + vPosition.x));\n color = mix(color, vec3(1.0, 0.4, 0.6), sin(time + vPosition.y) * 0.5 + 0.5);\n gl_FragColor = vec4(color, 1.0);\n }\n `,\n });\n\n // Create various geometric shapes\n const geometries = [\n new THREE.BoxGeometry(2, 2, 2),\n new THREE.SphereGeometry(1.2, 16, 16),\n new THREE.ConeGeometry(1, 2, 8),\n new THREE.CylinderGeometry(0.8, 0.8, 2, 8),\n new THREE.OctahedronGeometry(1.2),\n new THREE.TetrahedronGeometry(1.5),\n new THREE.DodecahedronGeometry(1),\n new THREE.IcosahedronGeometry(1.2),\n ];\n\n for (let i = 0; i < parameters.objectCount; i++) {\n const geometry =\n geometries[Math.floor(Math.random() * geometries.length)];\n const basicMaterial = new THREE.MeshPhongMaterial({\n color: colors[Math.floor(Math.random() * colors.length)],\n shininess: 100,\n transparent: true,\n opacity: 0.8 + Math.random() * 0.2,\n });\n\n const mesh = new THREE.Mesh(\n geometry,\n Math.random() > 0.3 ? basicMaterial : material,\n );\n\n mesh.position.set(\n (Math.random() - 0.5) * 20,\n (Math.random() - 0.5) * 20,\n (Math.random() - 0.5) * 20,\n );\n\n mesh.rotation.set(\n Math.random() * Math.PI * 2,\n Math.random() * Math.PI * 2,\n Math.random() * Math.PI * 2,\n );\n\n mesh.scale.setScalar(0.5 + Math.random() * 1.5);\n\n group.add(mesh);\n sceneRef.current.objects.push(mesh);\n }\n };\n\n createObjects();\n\n // Animation loop\n const clock = new THREE.Clock();\n const animate = () => {\n const delta = clock.getDelta();\n const elapsed = clock.getElapsedTime();\n\n // Update material uniforms\n sceneRef.current.objects.forEach((obj) => {\n if (obj.material.uniforms && obj.material.uniforms.time) {\n obj.material.uniforms.time.value = elapsed;\n }\n\n // Animate objects\n obj.rotation.x += delta * parameters.rotationSpeed * 0.2;\n obj.rotation.y += delta * parameters.rotationSpeed * 0.3;\n obj.rotation.z += delta * parameters.rotationSpeed * 0.1;\n });\n\n controls.update();\n composer.render();\n requestAnimationFrame(animate);\n };\n\n animate();\n\n // Handle resize\n const handleResize = () => {\n camera.aspect = window.innerWidth / window.innerHeight;\n camera.updateProjectionMatrix();\n renderer.setSize(window.innerWidth, window.innerHeight);\n composer.setSize(window.innerWidth, window.innerHeight);\n };\n\n window.addEventListener(\"resize\", handleResize);\n\n return () => {\n window.removeEventListener(\"resize\", handleResize);\n renderer.dispose();\n };\n }, [parameters]);\n\n // Update halftone parameters\n useEffect(() => {\n if (sceneRef.current?.halftonePass) {\n const pass = sceneRef.current.halftonePass;\n pass.uniforms.shape.value = parameters.shape;\n pass.uniforms.radius.value = parameters.radius;\n pass.uniforms.rotateR.value = parameters.rotateR * (Math.PI / 180);\n pass.uniforms.rotateG.value = parameters.rotateG * (Math.PI / 180);\n pass.uniforms.rotateB.value = parameters.rotateB * (Math.PI / 180);\n pass.uniforms.scatter.value = parameters.scatter;\n pass.uniforms.blending.value = parameters.blending;\n pass.uniforms.blendingMode.value = parameters.blendingMode;\n pass.uniforms.greyscale.value = parameters.greyscale;\n pass.uniforms.disable.value = parameters.disable;\n }\n }, [parameters]);\n\n const shapeName =\n [\"\", \"Dot\", \"Ellipse\", \"Line\", \"Square\"][parameters.shape] || \"Dot\";\n const blendModeName =\n [\"\", \"Linear\", \"Multiply\", \"Add\", \"Lighter\", \"Darker\"][\n parameters.blendingMode\n ] || \"Linear\";\n const actionNames = {\n \"before-randomize\": \"🎲 Before Random\",\n randomized: \"✨ Randomized\",\n manual: \"✏️ Manual Edit\",\n };\n\n return (\n <div className=\"relative h-screen w-full overflow-hidden bg-[#242424]\">\n {/* Background pattern */}\n <div\n className=\"absolute inset-0 opacity-10\"\n style={{\n backgroundImage: `radial-gradient(circle at 25px 25px, #70d6ff 2px, transparent 2px)`,\n backgroundSize: \"50px 50px\",\n }}\n />\n\n <canvas ref={canvasRef} className=\"absolute inset-0\" />\n\n {/* Main Control Panel */}\n <div\n className={`absolute top-4 left-4 max-h-[calc(100vh-2rem)] overflow-y-auto rounded-lg border-4 border-[#242424] bg-[#ffffff] p-4 shadow-lg transition-all duration-300 ${showParameters ? \"w-80\" : \"w-64\"}`}\n >\n <h2 className=\"mb-4 text-lg font-bold text-[#242424]\">\n RGB Halftone Studio\n </h2>\n\n {/* Always visible controls */}\n <div className=\"mb-4 space-y-3\">\n <button\n onClick={generateRandomScene}\n disabled={isGenerating}\n className=\"w-full rounded border-2 border-[#242424] bg-[#ff70a6] px-4 py-3 font-bold text-[#242424] hover:bg-[#ff9770] disabled:opacity-50\"\n >\n {isGenerating ? \"Generating...\" : \"🎲 Random Art\"}\n </button>\n\n <button\n onClick={() => setShowParameters(!showParameters)}\n className=\"w-full rounded border-2 border-[#242424] bg-[#70d6ff] px-4 py-2 font-bold text-[#242424] hover:bg-[#e9ff70]\"\n >\n {showParameters ? \"🔼 Hide Controls\" : \"🔽 Show Controls\"}\n </button>\n </div>\n\n {/* Expandable parameter controls */}\n {showParameters && (\n <div className=\"space-y-4\">\n {/* Shape Controls */}\n <div>\n <label className=\"mb-2 block text-sm font-bold text-[#242424]\">\n Shape: {shapeName}\n </label>\n <select\n value={parameters.shape}\n onChange={(e) =>\n setParameters((prev) => ({\n ...prev,\n shape: parseInt(e.target.value),\n }))\n }\n className=\"w-full rounded border-2 border-[#242424] p-2 text-[#242424]\"\n >\n <option value={1}>Dot</option>\n <option value={2}>Ellipse</option>\n <option value={3}>Line</option>\n <option value={4}>Square</option>\n </select>\n </div>\n\n {/* Size Controls */}\n <div>\n <label className=\"mb-2 block text-sm font-bold text-[#242424]\">\n Size: {parameters.radius.toFixed(1)}\n </label>\n <input\n type=\"range\"\n min=\"1\"\n max=\"25\"\n step=\"0.5\"\n value={parameters.radius}\n onChange={(e) =>\n setParameters((prev) => ({\n ...prev,\n radius: parseFloat(e.target.value),\n }))\n }\n className=\"w-full\"\n />\n </div>\n\n {/* Color Rotation */}\n <div className=\"grid grid-cols-3 gap-2\">\n <div>\n <label className=\"mb-1 block text-xs font-bold text-[#ff70a6]\">\n Red: {parameters.rotateR.toFixed(0)}°\n </label>\n <input\n type=\"range\"\n min=\"0\"\n max=\"90\"\n value={parameters.rotateR}\n onChange={(e) =>\n setParameters((prev) => ({\n ...prev,\n rotateR: parseFloat(e.target.value),\n }))\n }\n className=\"w-full\"\n />\n </div>\n <div>\n <label className=\"mb-1 block text-xs font-bold text-[#e9ff70]\">\n Green: {parameters.rotateG.toFixed(0)}°\n </label>\n <input\n type=\"range\"\n min=\"0\"\n max=\"90\"\n value={parameters.rotateG}\n onChange={(e) =>\n setParameters((prev) => ({\n ...prev,\n rotateG: parseFloat(e.target.value),\n }))\n }\n className=\"w-full\"\n />\n </div>\n <div>\n <label className=\"mb-1 block text-xs font-bold text-[#70d6ff]\">\n Blue: {parameters.rotateB.toFixed(0)}°\n </label>\n <input\n type=\"range\"\n min=\"0\"\n max=\"90\"\n value={parameters.rotateB}\n onChange={(e) =>\n setParameters((prev) => ({\n ...prev,\n rotateB: parseFloat(e.target.value),\n }))\n }\n className=\"w-full\"\n />\n </div>\n </div>\n\n {/* Effects */}\n <div>\n <label className=\"mb-2 block text-sm font-bold text-[#242424]\">\n Scatter: {(parameters.scatter * 100).toFixed(0)}%\n </label>\n <input\n type=\"range\"\n min=\"0\"\n max=\"1\"\n step=\"0.01\"\n value={parameters.scatter}\n onChange={(e) =>\n setParameters((prev) => ({\n ...prev,\n scatter: parseFloat(e.target.value),\n }))\n }\n className=\"w-full\"\n />\n </div>\n\n <div>\n <label className=\"mb-2 block text-sm font-bold text-[#242424]\">\n Blend: {(parameters.blending * 100).toFixed(0)}%\n </label>\n <input\n type=\"range\"\n min=\"0\"\n max=\"1\"\n step=\"0.01\"\n value={parameters.blending}\n onChange={(e) =>\n setParameters((prev) => ({\n ...prev,\n blending: parseFloat(e.target.value),\n }))\n }\n className=\"w-full\"\n />\n </div>\n\n <div>\n <label className=\"mb-2 block text-sm font-bold text-[#242424]\">\n Blend Mode: {blendModeName}\n </label>\n <select\n value={parameters.blendingMode}\n onChange={(e) =>\n setParameters((prev) => ({\n ...prev,\n blendingMode: parseInt(e.target.value),\n }))\n }\n className=\"w-full rounded border-2 border-[#242424] p-2 text-[#242424]\"\n >\n <option value={1}>Linear</option>\n <option value={2}>Multiply</option>\n <option value={3}>Add</option>\n <option value={4}>Lighter</option>\n <option value={5}>Darker</option>\n </select>\n </div>\n\n {/* Toggles */}\n <div className=\"space-y-2\">\n <label className=\"flex items-center\">\n <input\n type=\"checkbox\"\n checked={parameters.greyscale}\n onChange={(e) =>\n setParameters((prev) => ({\n ...prev,\n greyscale: e.target.checked,\n }))\n }\n className=\"mr-2\"\n />\n <span className=\"text-sm font-bold text-[#242424]\">\n Greyscale\n </span>\n </label>\n\n <label className=\"flex items-center\">\n <input\n type=\"checkbox\"\n checked={parameters.disable}\n onChange={(e) =>\n setParameters((prev) => ({\n ...prev,\n disable: e.target.checked,\n }))\n }\n className=\"mr-2\"\n />\n <span className=\"text-sm font-bold text-[#242424]\">\n Disable Effect\n </span>\n </label>\n </div>\n\n {/* Save Preset */}\n <div>\n <input\n type=\"text\"\n placeholder=\"Preset name...\"\n value={presetName}\n onChange={(e) => setPresetName(e.target.value)}\n className=\"mb-2 w-full rounded border-2 border-[#242424] p-2 text-[#242424]\"\n />\n <button\n onClick={savePreset}\n disabled={!presetName.trim()}\n className=\"w-full rounded border-2 border-[#242424] bg-[#ffd670] px-4 py-2 font-bold text-[#242424] hover:bg-[#e9ff70] disabled:opacity-50\"\n >\n 💾 Save Preset\n </button>\n </div>\n\n {/* Saved Presets */}\n {presets.length > 0 && (\n <div>\n <h4 className=\"mb-2 text-sm font-bold text-[#242424]\">\n 💾 Saved Presets\n </h4>\n <div className=\"max-h-32 space-y-2 overflow-y-auto\">\n {presets\n .sort((a, b) => b.timestamp - a.timestamp)\n .map((preset) => (\n <div\n key={preset._id}\n className={`cursor-pointer rounded border-2 p-2 transition-colors ${\n currentPreset?._id === preset._id\n ? \"border-[#242424] bg-[#ff70a6]\"\n : \"border-[#242424] bg-[#ffffff] hover:bg-[#e9ff70]\"\n }`}\n onClick={() => loadPreset(preset)}\n >\n <div className=\"text-xs font-bold text-[#242424]\">\n {preset.name}\n </div>\n <div className=\"text-xs text-[#242424] opacity-75\">\n {\n [\"\", \"Dot\", \"Ellipse\", \"Line\", \"Square\"][\n preset.parameters.shape\n ]\n }{\" \"}\n • {preset.parameters.greyscale ? \"B&W\" : \"Color\"}\n </div>\n </div>\n ))}\n </div>\n </div>\n )}\n\n {/* Parameter History */}\n {parameterHistory.length > 0 && (\n <div>\n <h4 className=\"mb-2 text-sm font-bold text-[#242424]\">\n 📜 Parameter History\n </h4>\n <div className=\"max-h-40 space-y-2 overflow-y-auto\">\n {parameterHistory\n .sort((a, b) => b.timestamp - a.timestamp)\n .slice(0, 10)\n .map((state) => (\n <div\n key={state._id}\n className=\"cursor-pointer rounded border-2 border-[#242424] p-2 transition-colors hover:bg-[#e9ff70]\"\n onClick={() => loadParameterState(state)}\n >\n <div className=\"text-xs font-bold text-[#242424]\">\n {actionNames[state.action] || \"⚙️ Unknown\"}\n </div>\n <div className=\"text-xs text-[#242424] opacity-75\">\n {\n [\"\", \"Dot\", \"Ellipse\", \"Line\", \"Square\"][\n state.parameters.shape\n ]\n }{\" \"}\n • Size: {state.parameters.radius.toFixed(1)} •{\" \"}\n {state.parameters.greyscale ? \"B&W\" : \"Color\"}\n </div>\n <div className=\"text-xs text-[#242424] opacity-50\">\n {new Date(state.timestamp).toLocaleTimeString()}\n </div>\n </div>\n ))}\n </div>\n </div>\n )}\n </div>\n )}\n </div>\n </div>\n );\n}\n```", | ||
| "web-audio": "# Web Audio API: Fundamentals, Echo with FX-in-Feedback, Mic Monitoring + Metronome, and Timing Architecture\n\nAuthoritative source: Issue #228 research threads — comments 3192681700, 3192696052, 3192806626.\n\n## 1) Fundamentals and Core Nodes\n\n- AudioContext — master interface and clock (`audioCtx.currentTime`). Resume on a user gesture.\n- OscillatorNode — synthesis; set `type` and `frequency`.\n- AudioBufferSourceNode — decoded-file playback; schedule with `.start(when, offset?, duration?)`.\n- GainNode — volume control and envelopes.\n- BiquadFilterNode — EQ/tonal shaping (`type`, `frequency`, `Q`, etc.).\n- AnalyserNode — FFT/time-domain visualization.\n\nExamples\n\n```js\n// 1) Context (user gesture required in many browsers)\nconst audioCtx = new (window.AudioContext || window.webkitAudioContext)();\n\n// Start/resume only in direct response to a user gesture (e.g., a Play button)\ndocument.querySelector('#start-audio')?.addEventListener('click', async () => {\n if (audioCtx.state !== 'running') await audioCtx.resume();\n // now safe to create/start nodes\n});\n\n// 2) Simple tone\nconst osc = audioCtx.createOscillator();\nosc.type = 'sine';\nosc.frequency.value = 440;\nosc.connect(audioCtx.destination);\nosc.start();\nosc.stop(audioCtx.currentTime + 1);\n\n// 3) Load/decode and play a file\nconst buf = await fetch('/path/audio.mp3').then(r => r.arrayBuffer()).then(b => audioCtx.decodeAudioData(b));\nconst src = audioCtx.createBufferSource();\nsrc.buffer = buf;\nsrc.connect(audioCtx.destination);\nsrc.start();\n\n// 4) Gain and Filter in series\nconst gain = audioCtx.createGain();\ngain.gain.value = 0.5;\nconst filter = audioCtx.createBiquadFilter();\nfilter.type = 'lowpass';\nfilter.frequency.value = 1000;\nosc.disconnect();\nosc.connect(filter).connect(gain).connect(audioCtx.destination);\n```\n\nPractical: clean up disconnected nodes; check browser support; use headphones to avoid feedback when monitoring.\n\n## 2) Echo/Delay with Effects Inside the Feedback Loop\n\nGraph (node names are exact):\n\n- Dry: `source → dryGain:GainNode → destination`\n- Wet: `source → delay:DelayNode → wetGain:GainNode → destination`\n- Feedback loop with FX: `delay → filter:BiquadFilterNode → distortion:WaveShaperNode → reverb:ConvolverNode → feedbackGain:GainNode → delay`\n\nParameters to expose\n\n- `delay.delayTime` (s), `feedbackGain.gain` (0–1, keep < 1.0)\n- `filter.type`, `filter.frequency`\n- `distortion.curve` (Float32Array)\n- `convolver.buffer` (IR AudioBuffer)\n- `wetGain.gain`, `dryGain.gain`\n\nNotes: Prevent runaway by capping feedback below 1.0; `ConvolverNode` requires a loaded impulse response; zero-delay cycles are disallowed.\n\n```js\nconst delay = audioCtx.createDelay(5.0);\nconst feedbackGain = audioCtx.createGain();\nconst filter = audioCtx.createBiquadFilter();\nconst distortion = audioCtx.createWaveShaper();\nconst reverb = audioCtx.createConvolver();\nconst wetGain = audioCtx.createGain();\nconst dryGain = audioCtx.createGain();\n\ndelay.delayTime.value = 0.35;\nfeedbackGain.gain.value = 0.5; // < 1.0\nfilter.type = 'lowpass';\nfilter.frequency.value = 8000;\n// distortion.curve = yourFloat32Curve;\n// reverb.buffer = yourImpulseResponseAudioBuffer;\nwetGain.gain.value = 0.4;\ndryGain.gain.value = 1.0;\n\n// Dry and wet\nsource.connect(dryGain).connect(audioCtx.destination);\nsource.connect(delay);\ndelay.connect(wetGain).connect(audioCtx.destination);\n\n// Feedback with FX\ndelay.connect(filter);\nfilter.connect(distortion);\ndistortion.connect(reverb);\nreverb.connect(feedbackGain);\nfeedbackGain.connect(delay);\n```\n\nHelper (load IR):\n\n```js\nasync function loadImpulseResponse(url) {\n const res = await fetch(url, { mode: 'cors' });\n if (!res.ok) throw new Error(`Failed to fetch IR ${url}: ${res.status} ${res.statusText}`);\n const ab = await res.arrayBuffer();\n try {\n return await audioCtx.decodeAudioData(ab);\n } catch (err) {\n console.error('decodeAudioData failed for IR', url, err);\n throw err; // Surface decoding/CORS-related failures clearly\n }\n}\n```\n\n## 3) Microphone Monitoring + Metronome Overlay\n\nMic capture: request permission with `navigator.mediaDevices.getUserMedia({ audio: { echoCancellation, noiseSuppression, autoGainControl } })`. Create `MediaStreamAudioSourceNode` and route to a `GainNode` → destination.\n\nMetronome: synthesize a short click (e.g., square/sine burst through a gain envelope). Schedule by audio clock at `AudioContext.currentTime` with lookahead.\n\nMix graph: `micGain + metronomeGain → master → destination`.\n\n```js\nconst master = audioCtx.createGain();\nmaster.connect(audioCtx.destination);\nconst micGain = audioCtx.createGain();\nconst metronomeGain = audioCtx.createGain();\nmicGain.connect(master);\nmetronomeGain.connect(master);\n\nasync function initMic() {\n const stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: false } });\n const micSrc = audioCtx.createMediaStreamSource(stream);\n micSrc.connect(micGain);\n}\n\nfunction scheduleClick(atTime, downbeat = false) {\n const osc = audioCtx.createOscillator();\n const env = audioCtx.createGain();\n osc.type = 'square';\n osc.frequency.setValueAtTime(downbeat ? 2000 : 1600, atTime);\n env.gain.setValueAtTime(0.0001, atTime);\n env.gain.exponentialRampToValueAtTime(1.0, atTime + 0.001);\n env.gain.exponentialRampToValueAtTime(0.0001, atTime + 0.03);\n osc.connect(env).connect(metronomeGain);\n osc.start(atTime);\n osc.stop(atTime + 0.05);\n // Cleanup to avoid accumulating nodes during long sessions\n osc.onended = () => {\n try { osc.disconnect(); } catch {}\n try { env.disconnect(); } catch {}\n };\n}\n\nfunction startMetronome({ bpm = 120, beatsPerBar = 4 } = {}) {\n const spb = 60 / bpm; // seconds per beat\n let next = audioCtx.currentTime + 0.1;\n let beat = 0;\n const lookaheadMs = 25, ahead = 0.2;\n const id = setInterval(() => {\n while (next < audioCtx.currentTime + ahead) {\n scheduleClick(next, beat % beatsPerBar === 0);\n next += spb; beat = (beat + 1) % beatsPerBar;\n }\n }, lookaheadMs);\n return () => clearInterval(id);\n}\n```\n\nLatency and safety: start/resume on user gesture; clean up per-tick nodes after `ended` to prevent buildup in long-running metronomes; use headphones while monitoring; mobile devices have higher base latency.\n\n## 4) Time Synchronization and Scheduling Model\n\nClocks/time domains\n\n- Master: `AudioContext.currentTime` — sample-accurate; schedule everything on this timeline.\n- UI/high-res: `performance.now()` — for UI timers and Web MIDI timestamps.\n- Mapping: capture `(tPerf0 = performance.now(), tAudio0 = audioCtx.currentTime)`, convert MIDI/perf timestamps with `tAudio = tAudio0 + (timeStamp - tPerf0)/1000`.\n- Hints: `audioCtx.baseLatency`, `audioCtx.getOutputTimestamp?.()` — estimate DAC/output delay if aligning to “heard” time.\n\nScheduling primitives\n\n- `AudioBufferSourceNode.start(when, offset?, duration?)` for one-shots/loops.\n- `AudioParam` automation (`setValueAtTime`, `linearRampToValueAtTime`, `setTargetAtTime`, `setValueCurveAtTime`).\n- Avoid `requestAnimationFrame`/`setTimeout` for timing; use an AudioWorklet for custom DSP/tight jitter when needed.\n\nTempo transport and lookahead\n\n- Tempo mapping: `secondsPerBeat = 60 / bpm`; compute bars:beats:ticks → seconds on the audio clock (choose PPQ, e.g., 480/960).\n- Lookahead window: maintain ~50–200 ms rolling schedule; enqueue with absolute `when` times in audio seconds.\n\nMulti‑channel drum machine\n\n- Pre‑decode all samples; never decode on hit.\n- Per hit: create a fresh `AudioBufferSourceNode` and call `.start(when)`.\n- For phase‑aligned layers (kick+clap, etc.), schedule all sources with the same `when` to guarantee sample‑accurate overlap.\n- Routing: per‑track `GainNode`/optional FX → master bus; allow overlapping retriggers; compute flams as small `when` offsets.\n- Pattern changes: compute the next bar boundary on the audio clock and enqueue new pattern hits relative to that time.\n\nMIDI synth playback\n\n- Live input: map `MIDIMessageEvent.timeStamp` (perf.now domain) → audio clock as above; buffer a short lookahead (5–20 ms) to reduce jitter.\n- SMF playback: convert PPQ ticks using the tempo map; schedule noteOn/noteOff separately; sustain (CC64) defers noteOff until pedal release.\n- Voice management: one voice per active note; allow overlapping envelopes; define voice‑steal policy if a polyphony cap is hit.\n\nExternal sync and drift\n\n- For MIDI Clock/MTC, derive BPM/phase from incoming ticks, convert to audio time, and drive the transport. Correct small phase error between beats with bounded micro‑nudges—avoid discontinuities.\n\n## 5) Practical Notes\n\n- User gesture required to start/resume `AudioContext` and to access the mic.\n- Convolver IRs: host with CORS if cross‑origin; decode before use.\n- Latency budget: device `baseLatency` + your lookahead + any Worklet buffering.\n- Headphones recommended for monitoring to avoid acoustic feedback.\n\n— End —", | ||
| "image-gen": "# ImgGen Component\n\n## Basic Usage\n\nThe ImgGen component can be used in three ways:\n\n1. **With no props** - Shows a form UI for users to enter a prompt and/or upload images:\n\n```jsx\nimport { ImgGen } from 'use-vibes';\n\nfunction MyComponent() {\n return <ImgGen />; // Shows built-in form for prompt entry and image upload\n}\n```\n\n2. **With a prompt prop** - Immediately generates an image (no form shown):\n\n```jsx\nimport { ImgGen } from 'use-vibes';\n\nfunction MyComponent() {\n return <ImgGen prompt=\"A sunset over mountains\" />; // Direct generation, no form\n}\n```\n\n3. **With images prop** - Edits or combines images with AI (no form shown):\n\n```jsx\nimport { ImgGen } from 'use-vibes';\n\nfunction MyComponent() {\n const [files, setFiles] = useState([]);\n return (\n <ImgGen \n prompt=\"Create a gift basket with these items\" \n images={files} // Array of File objects\n />\n );\n}\n```\n\n4. **With an _id prop** - Loads a specific image from the database (no form shown):\n\nIf there is no image generated for the document yet, but it has a `prompt` field, it will generate a new image with the prompt. If there an images is stored, at doc._files.original, it will use that as the base image.\n\n```jsx\nimport { ImgGen } from 'use-vibes';\n\nfunction MyComponent() {\n return <ImgGen _id=\"my-image-id\" />; // Loads specific image by ID\n}\n```\n\n\n## List by ID\n\nImages and prompts are tracked in a Fireproof database with a `type` of `image`. If a database is not provided, it uses `\"ImgGen\"` as the database name.\n\nDisplay stored images by their ID. Ensure you do this, so users can find the images they created.\n\n```jsx\nimport { useFireproof } from 'use-fireproof';\nimport { ImgGen } from 'use-vibes';\n\nfunction MyComponent() {\n const { database, useLiveQuery } = useFireproof(\"my-db-name\");\n const { docs: imageDocuments } = useLiveQuery('type', {\n key: 'image',\n descending: true,\n });\n\n return (\n <div>\n <ImgGen database={database} />\n {imageDocuments.length > 0 && (\n <div className=\"history\">\n <h3>Previously Generated Images</h3>\n <ul className=\"image-list\">\n {imageDocuments.map((doc) => (\n <li key={doc._id} className=\"image-item\">\n <ImgGen _id={doc._id} database={database} />\n </li>\n ))}\n </ul>\n </div>\n )}\n </div>\n );\n}\n```\n\n## Styling\n\nImgGen supports custom styling through CSS variables or custom class names:\n\n```jsx\n// With CSS variables in your styles\n:root {\n --imggen-text-color: #222;\n --imggen-accent: #0088ff;\n --imggen-border-radius: 8px;\n}\n\n// With custom class names\n<ImgGen \n prompt=\"A landscape\" \n className=\"my-custom-image\"\n classes={{\n root: 'custom-container',\n image: 'custom-img',\n overlay: 'custom-overlay'\n }}\n/>\n```\n\n#### Props\n\n- `prompt`: Text prompt for image generation (required unless `_id` is provided)\n- `_id`: Document ID to load a specific image instead of generating a new one\n- `database`: Database name or instance to use for storing images (default: `'ImgGen'`)- `options` (object, optional): Configuration options for image generation\n - `model` (string, optional): Model to use for image generation, defaults to 'gpt-image-1'\n - `size` (string, optional): Size of the generated image (Must be one of 1024x1024, 1536x1024 (landscape), 1024x1536 (portrait), or 'auto' (default value) for gpt-image-1, and one of 256x256, 512x512, or 1024x1024 for dall-e-2.)\n - `quality` (string, optional): Quality of the generated image (high, medium and low are only supported for gpt-image-1. dall-e-2 only supports standard quality. Defaults to auto.)\n - `debug` (boolean, optional): Enable debug logging, defaults to false\n- `onLoad`: Callback when image load completes successfully\n- `onError`: Callback when image load fails, receives the error as parameter\n- `className`: CSS class name for the image element (optional)- `classes`: Object containing custom CSS classes for styling component parts (see Styling section)" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The image-gen library docs reference ImgGen from use-vibes, but generated projects do not include use-vibes (your templates depend on use-fireproof and call-ai only). Including these docs in the augmentation can cause the model to emit imports that won’t resolve, breaking builds.
Suggestion
Either (a) remove or gate the image-gen docs from plugin-data.json until use-vibes is added as a dependency in templates, or (b) add use-vibes (and its published, installable dependencies) to templates/package.json.template. Given you explicitly moved away from use-vibes, I recommend removing/gating image-gen from prompt data. I can update plugin-data.json to omit image-gen. Reply with "@CharlieHelps yes please" and I’ll commit that change.
| The generated code uses these packages: | ||
| - `react` - React library | ||
| - `use-vibes` - Includes useFireproof hook | ||
| - `call-ai` - AI integration (imported via use-vibes) | ||
|
|
||
| These are automatically included in package.json. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This section says the generated code uses use-vibes (and that it’s in package.json), but your templates clearly depend on use-fireproof and call-ai. This inconsistency will send users down the wrong path when troubleshooting.
Suggestion
Update the troubleshooting note to refer to use-fireproof (and call-ai) rather than use-vibes. I can revise the wording to: “The generated code uses: react, use-fireproof, call-ai. These are automatically included in package.json.” Reply with "@CharlieHelps yes please" and I’ll commit the doc fix.
| IMPORTANT: You are working in one JavaScript file, use tailwind classes for styling. | ||
| Remember to use brackets like bg-[#242424] for custom colors. | ||
|
|
||
| Provide a title and brief explanation followed by the component code. The component should | ||
| demonstrate proper Fireproof integration with real-time updates and proper data persistence. | ||
| ``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The instruction “IMPORTANT: You are working in one JavaScript file” contradicts the Vite project structure you generate (multiple files). It should clarify that the main app logic lives in src/App.jsx, not that the entire project is a single file.
Suggestion
Reword this to something like: “IMPORTANT: Keep all app logic in a single component file, src/App.jsx (don’t create extra files). Use Tailwind classes for styling.” I can update this paragraph to avoid confusing the Skill execution. Reply with "@CharlieHelps yes please" and I’ll apply the change.
| // Extract default style name | ||
| const defaultMatch = content.match(/export const DEFAULT_STYLE_NAME = "([^"]+)"/); | ||
| const defaultStyle = defaultMatch ? defaultMatch[1] : 'brutalist web'; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The regex used to extract DEFAULT_STYLE_NAME from style-prompts.ts only matches double quotes and may miss single-quoted values or formatting changes, leading to silent fallback. This is fragile for a key setting.
Suggestion
Use a more robust regex that supports both single and double quotes and optional whitespace, e.g.: const defaultMatch = content.match(/DEFAULT_STYLE_NAME\s*=\s*['"]([^'"]+)['"]/);. I can apply this change. Reply with "@CharlieHelps yes please" and I’ll commit it.
| // Compile into final structure | ||
| const pluginData = { | ||
| version: '1.0.0', | ||
| generatedAt: new Date().toISOString(), | ||
| repository: 'https://github.com/fireproof-storage/vibes.diy', | ||
| coreGuidelines, | ||
| stylePrompts: styles, | ||
| defaultStyle, | ||
| libraries | ||
| }; | ||
|
|
||
| // Write to file | ||
| fs.writeFileSync(OUTPUT_FILE, JSON.stringify(pluginData, null, 2), 'utf8'); | ||
|
|
||
| console.log(`\n✓ Plugin data compiled successfully!`); | ||
| console.log(` Output: ${OUTPUT_FILE}`); | ||
| console.log(` Size: ${(fs.statSync(OUTPUT_FILE).size / 1024).toFixed(2)} KB`); | ||
| console.log(` Styles: ${styles.length}`); | ||
| console.log(` Libraries: ${Object.keys(libraries).length}\n`); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The build script tolerates missing library docs by logging warnings and still emitting output. That risks shipping partially populated plugin-data.json without failing CI. It’s better to validate required inputs and exit non‑zero if essential docs are missing.
Suggestion
Add a validation step before writing the output; fail fast if expected libraries are missing. For example:
const libraries = extractLibraryDocs();
const coreGuidelines = generateCoreGuidelines();
+
+ // Validate required libraries were loaded (fail fast in CI)
+ const requiredLibs = ["fireproof", "callai"]; // extend if needed
+ const missing = requiredLibs.filter((k) => !libraries[k] || !libraries[k].length);
+ if (missing.length) {
+ console.error(`\n✖ Missing required library docs: ${missing.join(", ")}`);
+ process.exit(1);
+ }Reply with "@CharlieHelps yes please" and I’ll add this guard.
| }, | ||
| "homepage": "https://vibes.diy", | ||
| "repository": "https://github.com/fireproof-storage/vibes.diy", | ||
| "license": "MIT", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
License metadata is inconsistent: plugin.json declares MIT while cli/vibes/package.json declares Apache-2.0. This can cause compliance and distribution confusion.
Suggestion
Pick a single license and apply it consistently. For example, align the cli/vibes/package.json to MIT to match the plugin manifest:
In cli/vibes/package.json:
- "license": "Apache-2.0"
+ "license": "MIT"Reply with "@CharlieHelps yes please" and I’ll make this change.
| cli/vibes/skills/vibes-generator/templates/package.json.template | ||
| → Dependencies: React 19, use-vibes 0.14.6, Vite 7.1.9 | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This inventory still lists use-vibes as a dependency, which no longer matches the templates (now use-fireproof + call-ai).
Suggestion
Correct the dependency note to reflect the current stack:
- → Dependencies: React 19, use-vibes 0.14.6, Vite 7.1.9
+ → Dependencies: React 19, use-fireproof ^0.23.15, call-ai ^0.15.13, Vite 7.1.9Reply with "@CharlieHelps yes please" and I’ll update this file.
| Check that `src/App.jsx`: | ||
| - Imports from `use-vibes` (not `use-fireproof`) | ||
| - Uses `useFireproof` hook correctly | ||
| - Implements user's requested features |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The verification checklist contradicts the later decision to avoid use-vibes. It says "Imports from use-vibes (not use-fireproof)" but you’ve documented switching to use-fireproof + call-ai. This inconsistency will lead to incorrect review outcomes.
Suggestion
Update the checklist to match the adopted dependencies:
- Check that `src/App.jsx`:
- - Imports from `use-vibes` (not `use-fireproof`)
+ Check that `src/App.jsx`:
+ - Imports from `use-fireproof` and `call-ai`Reply with "@CharlieHelps yes please" and I’ll apply this fix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
move to into the to .gitignore ---- and then pls remove
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
never reference monorepo things like use-vibes with there real version ---- use workspace:*
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
License --- APACHE
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pls make the u llm less noisy --- this stuff it mostly sense less to review but it cost time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add to pnpm-workspace and use the core-cli task
i would think it should named @vibes.diy/cli-vibes --- follow the pattern we use from the directory name.
and if you want to use it in claude it should be public and published
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is mostly un maintainable --- due to the embedded code --- ask your llm to write a tool which generate this file from multiple source files
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NEVER any JS ---- and in addition we are a ESM not COMMON JS shop.
Use tsx as runtime for tools.
| The plugin-data.json file is cached locally and updated when the plugin itself is updated. If you need to manually refresh the plugin data to get the latest prompts and library documentation: | ||
|
|
||
| ```bash | ||
| curl -o ${CLAUDE_PLUGIN_ROOT}/../plugin-data.json \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you are in the monorepo so fetch the stuff directly --- with package depenendcy
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
make the version dynamic and fetch them from you package package.json ---- than the dependabot infra will keep them update --- we don't want to use use-fireproof directly any more --- it should fetched from use-vibes.

Marcus' initial Claude Code plugin project.