From 5e4fec400e10e21e5ace36c083bb2bb2903c3d26 Mon Sep 17 00:00:00 2001 From: Shaheen Date: Mon, 25 May 2026 14:34:47 +0530 Subject: [PATCH 1/4] docs(rollups-v2): add Build with AI section and copy-page plugin Introduce Cartesi Rollups v2.0 docs for AI-assisted development: overview, MCP server setup, skills catalog, and prompting guide with essential app templates. Register sidebar category, enable copy-as-markdown via local plugin wrapper, and fix duplicate package.json engines field. Co-authored-by: Cursor --- .../version-2.0/build-with-ai/mcp-server.mdx | 162 ++++ .../version-2.0/build-with-ai/overview.md | 110 +++ .../version-2.0/build-with-ai/prompting.md | 195 +++++ .../version-2.0/build-with-ai/skills.md | 65 ++ .../version-2.0-sidebars.json | 11 + docusaurus.config.js | 1 + package.json | 8 +- plugins/copy-page-button/CopyPageButton.js | 378 +++++++++ plugins/copy-page-button/client.js | 534 +++++++++++++ plugins/copy-page-button/htmlToMarkdown.js | 754 ++++++++++++++++++ plugins/copy-page-button/index.js | 18 + plugins/copy-page-button/styles.module.css | 274 +++++++ yarn.lock | 139 +++- 13 files changed, 2610 insertions(+), 39 deletions(-) create mode 100644 cartesi-rollups_versioned_docs/version-2.0/build-with-ai/mcp-server.mdx create mode 100644 cartesi-rollups_versioned_docs/version-2.0/build-with-ai/overview.md create mode 100644 cartesi-rollups_versioned_docs/version-2.0/build-with-ai/prompting.md create mode 100644 cartesi-rollups_versioned_docs/version-2.0/build-with-ai/skills.md create mode 100644 plugins/copy-page-button/CopyPageButton.js create mode 100644 plugins/copy-page-button/client.js create mode 100644 plugins/copy-page-button/htmlToMarkdown.js create mode 100644 plugins/copy-page-button/index.js create mode 100644 plugins/copy-page-button/styles.module.css diff --git a/cartesi-rollups_versioned_docs/version-2.0/build-with-ai/mcp-server.mdx b/cartesi-rollups_versioned_docs/version-2.0/build-with-ai/mcp-server.mdx new file mode 100644 index 00000000..661b7dc8 --- /dev/null +++ b/cartesi-rollups_versioned_docs/version-2.0/build-with-ai/mcp-server.mdx @@ -0,0 +1,162 @@ +--- +title: MCP Server +resources: + - url: https://modelcontextprotocol.io/docs/getting-started/intro + title: MCP Server Standard +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +The Cartesi MCP server is a read-only knowledge service your AI assistant connects to for Cartesi-specific context. Instead of guessing CLI flags or hallucinating APIs, your assistant can query curated documentation routes, repositories, articles, and skills. Many of the resources are returned inline without fetching external URLs. + +The server runs at `https://server.mcp.mugen.builders/mcp`. No API key or local install is required; add the URL to your MCP client and connect. + +Once connected, your assistant can: + +- Look up **CLI commands** matched to the Cartesi CLI version you have installed +- Pull **step-by-step skills** for local dev bootstrapping, frontend and backend development, asset deposits, L1 contract interactions, and on-chain deployment +- **Search** documentation routes, repositories, and articles by topic for deeper context +- Prepare **workflow commands** (`cartesi create`, `cartesi build`, `cartesi run`, deposits, and inputs) as instructions to run on your machine; the MCP server does not execute the CLI for you + +## Connect your client + + + + + +1. Open **Settings**. +2. In the sidebar, click **Tools and MCPs**, then select **New MCP Server**. +3. Cursor opens your MCP config file (`~/.cursor/mcp.json` globally, or `.cursor/mcp.json` in your project). Add: + +```json +{ + "mcpServers": { + "cartesi-mcp": { + "transport": "http", + "url": "https://server.mcp.mugen.builders/mcp" + } + } +} +``` + +4. Restart Cursor, then verify under **Settings → Tools and MCPs** that `cartesi-mcp` is connected. + + + + + +1. Install the [Claude CLI](https://docs.anthropic.com/en/docs/claude-code), then add the server: + +```shell +claude mcp add --transport http cartesi https://server.mcp.mugen.builders/mcp +``` + +2. Restart Claude Code (or start a new conversation). +3. Run `/mcp` and confirm `cartesi` appears in the list of active MCP servers. + + + + + +1. Add the server: + +```shell +codex mcp add cartesi-mcp --url https://server.mcp.mugen.builders/mcp +``` + +2. Verify it was added: + +```shell +codex mcp list +``` + + + + + +Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows): + +```json +{ + "mcpServers": { + "cartesi-mcp": { + "type": "streamable-http", + "url": "https://server.mcp.mugen.builders/mcp" + } + } +} +``` + +Restart Claude Desktop after saving. + + + + + +Add to `.vscode/mcp.json` in your project: + +```json +{ + "servers": { + "cartesi-mcp": { + "type": "http", + "url": "https://server.mcp.mugen.builders/mcp" + } + } +} +``` + +Restart VS Code after saving. Requires GitHub Copilot with MCP support enabled. + + + + + +For any other MCP-compatible client, point it at `https://server.mcp.mugen.builders/mcp` using HTTP transport. + +## Getting started + +Once connected, a typical workflow looks like this: + +1. **Scaffold or extend an app**, e.g. *"Create a Cartesi Rollups v2 JavaScript echo app using cartesi-scaffold"* +2. **Run locally**, e.g. *"Give me the exact cartesi build and cartesi run commands for this project"* +3. **Interact and debug**, e.g. *"How do I send an ERC-20 deposit to my app?"* or *"My advance handler is rejecting inputs; help me debug"* + +:::tip Demo +See the MCP server in action: [demo video](https://drive.google.com/file/d/1a0Ad4qgXY8ebjYtJXt0ycq8vtG95VlsM/view?usp=drive_link). Claude scaffolds a Cartesi app from a single prompt, builds and runs it locally, interacts via text input and token deposits, and deploys to Base Sepolia. +::: + +## Available tools + +The server exposes developer-facing tools in three categories: + +**Knowledge search**: find curated Cartesi resources and documentation routes: + +- `search_knowledge_resources`: search repos, articles, docs, and skills by topic +- `search_documentation_routes`: find docs pages by keyword +- `get_resource_detail`: fetch metadata and related doc routes for a resource +- `list_resources_for_tag` / `list_resources_for_source`: browse by tag or source +- `list_resource_doc_routes`: list documentation routes linked to a resource +- `get_knowledge_taxonomy` / `summarize_knowledge_base`: explore what the knowledge base covers + +**Workflow helpers**: generate step-by-step instructions to run on your machine: + +- `prepare_cartesi_create_command`: scaffold a new app with the correct CLI flags for your CLI version +- `prepare_cartesi_build_command` / `prepare_cartesi_run_command`: build and run locally +- `send_input_to_application`: send generic inputs or deposits via the CLI +- `prepare_erc20_deposit_instructions` / `prepare_erc721_deposit_instructions` / `prepare_erc1155_deposit_instructions`: portal deposit workflows +- `get_cartesi_app_logic_guidance`: guidance for advance/inspect handlers, address-book usage, and outputs +- `build_debugging_context`: assemble docs and resources for a debugging query + +**Prompts**: reusable prompt templates your assistant can invoke: + +- `find_cartesi_docs`: locate the best documentation for a topic +- `debug_cartesi_issue`: investigate an error using curated knowledge +- `explain_repository_context`: understand a Cartesi repo before making changes diff --git a/cartesi-rollups_versioned_docs/version-2.0/build-with-ai/overview.md b/cartesi-rollups_versioned_docs/version-2.0/build-with-ai/overview.md new file mode 100644 index 00000000..237ecad6 --- /dev/null +++ b/cartesi-rollups_versioned_docs/version-2.0/build-with-ai/overview.md @@ -0,0 +1,110 @@ +--- +title: Overview +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +AI coding assistants can scaffold, implement, and debug Cartesi applications quickly, but without Cartesi-specific context they often guess CLI flags, mix up v1 and v2 APIs, or invent workflows. + +Using the right tools, you get faster iteration-handlers, contracts, and frontends from natural-language prompts-and less context switching, with version-aware CLI commands and doc links inline instead of hunting through tabs. This section gives your assistant structured knowledge and tools to build and ship applications quickly and reliably. + +## How it works + +Multiple pieces work together to learn and build with AIs: + +1. **[Documentaion](#documentation-indexing-and-usage)**: Usage of [llms.txt](https://docs.cartesi.io/llms.txt) gives your assistant a machine-readable index of all Cartesi docs. +2. **[MCP server](./mcp-server.mdx)**: Connects your editor to curated Cartesi docs, repos, articles, and skills. +3. **[Skills](./skills.md)**: Loads focused instructions (scaffold, backend, frontend, deploy, debug) +4. **[Prompting](./prompting.md)**: Interactive way to put the knowledge and skills of the AI assistant in practice. + +## Documentation indexing and usage + +Cartesi docs publish machine-readable indexes so AI assistants can discover pages and fetch raw Markdown without scraping HTML. Use these files when your client does not have MCP connected, or when you want a lightweight doc dump in context. + +### Documentation index + +Fetch the complete documentation index at: [https://docs.cartesi.io/llms.txt](https://docs.cartesi.io/llms.txt) + +`llms.txt` lists every indexed page with links to raw Markdown sources, version-priority notes (default to Rollups v2.0), and a documentation map. Agents should read this file first to discover which pages to fetch for a given task. + +### Per-page Markdown + +Any docs page can be fetched as Markdown by appending `.md` to its URL. For example: + +- Page: `https://docs.cartesi.io/cartesi-rollups/2.0/build-with-ai/overview` +- Source: `https://docs.cartesi.io/cartesi-rollups/2.0/build-with-ai/overview.md` + +On any docs page, use the **Copy page** widget in the table of contents sidebar to copy the page as Markdown, open the `.md` URL directly, or send the link to ChatGPT, Claude, or Gemini. Use this when you need a single page in context instead of the full corpus. + +### Full documentation file + +If your AI tool does not support MCP yet, you can use a static documentation file instead. This gives your assistant the entire Cartesi documentation corpus as one text file. + +Download or reference: [https://docs.cartesi.io/llms-full.txt](https://docs.cartesi.io/llms-full.txt) + +### Setup Static Documentation + + + + + +[Cursor](https://cursor.com/) can index external documentation for `@docs` references in chat. + +1. Open **Cursor Settings** → **Indexing & Docs** → **Docs**. +2. Click **Add new doc** and paste: `https://docs.cartesi.io/llms-full.txt` +3. In chat, reference the docs source (for example `@docs` → your Cartesi entry) when you want the assistant to ground answers in official documentation. + +For live Cartesi-specific tools (CLI commands, skills, repo search), also connect the [MCP server](./mcp-server.mdx). + + + + + +[Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview) can include static doc files in a session. + +1. Download the static documentation file from [https://docs.cartesi.io/llms-full.txt](https://docs.cartesi.io/llms-full.txt), or fetch the index at [https://docs.cartesi.io/llms.txt](https://docs.cartesi.io/llms.txt) and pull individual `.md` pages as needed. +2. Save the file in your project directory or another known location. +3. Reference it in chat with `/read` or by attaching the file path so Claude Code has Cartesi documentation for that session. + +For ongoing work, add the Cartesi MCP server to your project's `.mcp.json` (see [MCP server](./mcp-server.mdx)) so lookups stay current without re-downloading `llms-full.txt`. + + + + + +Alternatively, you can use the [Cartesi MCP server](./mcp-server.mdx) to get the latest documentation and skills. + +## Best Practices + +AI-assisted development is powerful, but it is not a substitute for good engineering judgment. Treat every generated command, dependency, and deployment step as untrusted until you understand and verify it. + +### Security + +- **Never paste private keys, mnemonics, or production secrets into prompts.** Use testnet keys, local dev accounts, and environment variables your assistant never sees. +- **Review before you run.** Agents can propose shell commands, config changes, or contract deployments that look correct but are wrong or harmful. Read diffs and commands before approving them. +- **Audit generated code.** Especially for Solidity, wallet flows, and anything that moves funds. AI can miss edge cases, use deprecated APIs, or introduce subtle bugs. +- **Trust your toolchain.** Only install MCP servers, skills, and editor plugins from sources you recognize. A malicious plugin or MCP server could exfiltrate files, env vars, or keys from your machine. +- **Limit blast radius.** Prefer testnets and disposable wallets for AI-assisted deployment. Do not point agents at mainnet credentials or production infrastructure. + +### Costs and model quality + +- **Free or lightweight models** are fine for boilerplate and docs lookup, but they hallucinate more often and struggle with multi-step Cartesi workflows. +- **Frontier models** (paid tiers) are usually better at following skills, chaining CLI steps, and debugging but usage-based billing adds up quickly on long agent sessions. +- **Token usage grows fast** when you attach large repos, full doc dumps, or long chat histories. Scope context to what the task needs. + +### Agent access and sandboxing + +- **Agents may read and write files, run terminals, and call MCP tools** depending on your client settings. Understand what your editor allows before enabling auto-run or broad file access. +- **Use sandboxing where your client supports it**: restrict network access, require approval for terminal commands, and avoid giving an agent unrestricted access to your home directory or `.env` files. +- **The Cartesi MCP server is read-only**, but other MCP servers or built-in tools in your client may not be. Review every MCP server you connect. +- **Separate dev from production.** Do not run AI agents in directories that contain production keys, customer data, or unreleased IP you cannot afford to leak. + +### Other limitations + +- **Models can still guess.** Even with the Cartesi MCP server and skills, assistants may mix API versions, invent flags, or skip steps. Cross-check against official docs. +- **Early-release tooling.** Cartesi Skills and the MCP server are evolving; expect gaps, breaking changes, and incomplete coverage. +- **You own the outcome.** AI speeds up scaffolding and iteration; shipping safely still requires tests, manual review, and your own deployment discipline. \ No newline at end of file diff --git a/cartesi-rollups_versioned_docs/version-2.0/build-with-ai/prompting.md b/cartesi-rollups_versioned_docs/version-2.0/build-with-ai/prompting.md new file mode 100644 index 00000000..e4f37993 --- /dev/null +++ b/cartesi-rollups_versioned_docs/version-2.0/build-with-ai/prompting.md @@ -0,0 +1,195 @@ +--- +title: Prompting +--- + +Effective prompting helps your AI assistant use Cartesi docs, skills, and the MCP server reliably. Clear prompts tell the assistant what to generate and which skill or doc source to follow. + +## Anatomy of a strong prompt + +The best prompts are as specific as possible. They pin the version, name the stack, define module boundaries, list deliverables, and state constraints. Your prompts should mirror that shape. Vague prompts make the assistant guess while risky prompts let it guess in places where guesses cost money, leak keys, or push bad code. + +Use these three tiers as a quick check before sending a prompt. + +### Effective: specific, scoped, version-aware + +An effective prompt typically: + +- Pins **Cartesi Rollups v2** and any relevant chain or network. +- Names the **skill(s)** to load and the **stack** (language, framework, package versions). +- Lists **deliverables** (folder structure, CLI commands, tests, inspect routes). +- States **constraints** (deterministic execution, module boundaries, reproducible simulation seeds where needed). +- Tells the assistant to **prepare commands for you to run**, not to execute them. + +```text +Build a Cartesi Rollups v2 JavaScript app called "order-book". + +Use cartesi-scaffold, cartesi-backend-core, cartesi-backend-js-ts, and +cartesi-contracts. Stack: JS template, Foundry for any L1 contracts, vanilla CSS +for any harness. Pin Cartesi alpha packages explicitly. + +Deliverables: +- Folder structure: handlers/, validation/, inspect/, assets/. +- advance_state for new/cancel order; inspect routes for /book and /trades. +- README with exact cartesi build / run / send commands. +- Unit tests for handler validation and double-cancel prevention. + +Constraints: +- Do not execute commands; print them for me to run. +- Use only v2 APIs (/cartesi-rollups/2.0/...). +- Reject malformed advances; never silently accept. +``` + +### Vague: missing version, stack, or deliverables + +Vague prompts force the assistant to guess. It will often pick the wrong API version, the wrong template, or invent CLI flags. + +Common symptoms: no v2 pin, no skill name, no stack, no description of what the answer should look like. + +```text +Make me a Cartesi app for an order book. Add a frontend too. +``` + +Likely failure modes: + +- Mixes v1 and v2 CLI commands. +- Picks a random template (Python? Rust? JS?) without asking. +- Skips inspect routes, validation, or tests. +- Ships a frontend pointed at the wrong port or chain ID. + +### Risky: invites the agent into places it shouldn't go + +A prompt is risky when it grants the agent power, secrets, or production scope without guardrails. These prompts can produce code or commands that move funds, leak keys, or silently deploy to the wrong network. + +Watch for: + +- Asking the agent to **execute** rather than **prepare** commands. +- Pasting **private keys**, **mnemonics**, or `.env` contents into the chat. +- Pointing at **mainnet** RPCs or keys "just for a quick test". +- Skipping **review** ("just commit and push it"). +- Asking for **production deployment** from skills that are explicitly testnet-style. + +```text +Deploy my Cartesi app to mainnet now. Here's my private key: 0xabc... +Use forge script and broadcast everything. Don't ask me to confirm anything. +Also commit and push to main when you're done. +``` + +What to do instead: + +- Stay on **testnets** with **dev keys** while iterating. +- Ask the assistant to **print** CLI / `cast` / `forge` commands; you run them. +- Have it **diff and explain** changes before any commit. +- Read [Overview → Exercise caution](./overview.md#exercise-caution) before widening the agent's access. + +## Prompt patterns + +### Scaffold a new app + +```text +Build a Cartesi Rollups v2 JavaScript app called "echo-app". + +Use the cartesi-scaffold and cartesi-backend-js-ts skills. Scaffold with +cartesi create, implement advance/inspect handlers, and give me the exact +commands to build and run locally. +``` + +### Debug an issue + +```text +My Cartesi advance handler rejects every input with status reject. +Use the cartesi-debug skill and Cartesi MCP to find relevant docs. +Show me what to check in my /finish loop and handler validation. +``` + +### Add a frontend + +```text +Add a React frontend to my Cartesi Rollups v2 app using cartesi-frontend. +Wire wallet connect, send inputs via InputBox, and read state via JSON-RPC. +Use @cartesi/wagmi and @cartesi/viem. +``` + +### Deploy + +```text +Walk me through self-hosted deployment for this Cartesi Rollups v2 app. +Use the cartesi-deploy skill and point me to the exact CLI and Docker steps. +``` + +## Application prompts + +Use these prompts when you want practical, production-oriented building blocks instead of toy examples. + +### Build a simple Cartesi wallet (multi-asset) + +```text +Build a Cartesi Rollups v2 app called "cartesi-wallet" that behaves like a simple +custodial wallet inside the Cartesi machine. + +Use cartesi-scaffold, cartesi-backend-core, and cartesi-backend-js-ts. Follow +Cartesi Rollups v2 asset handling for ETH, ERC-721, and ERC-1155 (single and +batch) portal deposits and withdrawals. + +Requirements: +- Support deposits, internal balances, transfers, and withdrawals for: + 1) ETH + 2) ERC-721 + 3) ERC-1155 single transfer + 4) ERC-1155 batch transfer +- Define canonical payload schemas for every action (deposit, transfer, withdraw). +- Implement advance handlers with strict validation and deterministic state updates. +- Expose inspect endpoints to query balances, owned NFTs, and transfer history. +- Include replay-safe idempotency rules for repeated messages. +- Add tests for happy path + invalid payloads + double-withdraw prevention. + +Implementation guidance: +- Use Cartesi Rollups v2 docs only. +- Scaffold with Cartesi CLI and show exact commands. +- Keep code modular: assets/, ledger/, handlers/, validation/, serialization/. +- For token standards, follow OpenZeppelin interface semantics. +- Return a final checklist: local run, test execution, and next hardening steps. +``` + +### Build bonding curve math in Cartesi + +```text +Create a Cartesi Rollups v2 module named "bonding-curve-engine" for pricing and +mint/burn settlement. + +Use cartesi-scaffold, cartesi-backend-core, cartesi-backend-js-ts, and +cartesi-local-dev. + +Requirements: +- Implement at least two curve types: + 1) Linear: P(s) = a + b*s (closed-form integral for buy/sell quotes) + 2) Non-linear (e.g. exponential): approximate buy/sell cost with Monte Carlo + simulation inside the Cartesi machine (sample price paths or Riemann sums; + document sample count, integration bounds, and convergence tolerance) +- Add quoteBuy(amount), quoteSell(amount), executeBuy, executeSell. +- Floating-point math is acceptable inside the Cartesi machine for simulation and + curve evaluation; use a fixed RNG seed per input so results are reproducible + across runs. Serialize on-chain-facing amounts (notices, vouchers, reports) in + a deterministic integer encoding and document rounding toward the protocol. +- Define rounding policy (always round against trader) and document it. +- Enforce invariants: reserve solvency, monotonic price, non-negative supply. +- Add tests: closed-form checks for linear curves, Monte Carlo stability tests + (same seed => same quote), and edge cases (zero amount, max supply). + +Cartesi-specific constraints: +- Deterministic execution across nodes for a given input payload and machine state. +- Explicit serialization format for inputs/outputs. +- Inspect endpoints for quotes, reserves, supply, curve parameters, and last + simulation metadata (samples used, seed, tolerance). + +Output: +- Folder structure, implementation plan, and complete code. +- CLI commands to build, run, send sample inputs, and inspect state. +``` + +### Other DeFi essentials to prompt next + +- **AMM core (x*y=k)**: swaps, LP mint/burn math, fee accounting, slippage checks +- **Lending risk engine**: collateral factors, health factor, liquidation thresholds +- **Perps/funding module**: mark/index price handling, funding-rate accrual, margin checks +- **Staking + rewards distributor**: epoch accounting, reward debt math, emergency withdraw +- **Treasury + timelock governance**: queued actions, execution delays, role-based controls diff --git a/cartesi-rollups_versioned_docs/version-2.0/build-with-ai/skills.md b/cartesi-rollups_versioned_docs/version-2.0/build-with-ai/skills.md new file mode 100644 index 00000000..a4a13c2f --- /dev/null +++ b/cartesi-rollups_versioned_docs/version-2.0/build-with-ai/skills.md @@ -0,0 +1,65 @@ +--- +title: Skills +resources: + - url: https://agentskills.io/home + title: Agent Skills standard + - url: https://skills.mugen.builders/ + title: Browse and copy Cartesi skills + - url: https://youtu.be/wep_ZEPKt8s + title: Cartesi skills setup walkthrough +--- + +**Cartesi Skills** are lightweight, specialized instructions for AI agents. Each skill covers a focused task: scaffolding a new app, building backend logic, wiring a frontend, interacting with L1 contracts, or deploying to a self-hosted node. + +Cartesi Skills follow the open [Agent Skills](https://agentskills.io/home) format. For a similar pattern in the broader Ethereum ecosystem, see [eth-skills](https://www.ethskills.com/). + +## What's included + +The current release ships **10 skills plus a workflow skill** that routes you to the right skill for each phase: + +- **`cartesi-workflow`**: map a full-stack application build and pick the next skill +- **`cartesi-scaffold`**: create a new Rollups v2 app with `cartesi create` +- **`cartesi-backend-core`**: implement advance/inspect handlers and the `/finish` loop +- **`cartesi-backend-js-ts`**: build a JavaScript or TypeScript backend +- **`cartesi-backend-py`**: build a Python backend +- **`cartesi-frontend`**: wire wallet, InputBox, JSON-RPC, and inspect in a UI +- **`cartesi-contracts`**: write Solidity/Foundry contracts that call InputBox and portals +- **`cartesi-local-dev`**: run `cartesi build` / `cartesi run`, test locally or on a fork +- **`cartesi-jsonrpc`**: query a running node over JSON-RPC +- **`cartesi-deploy`**: deploy and operate a self-hosted rollups node +- **`cartesi-debug`**: diagnose errors across the stack + +## Get started + +### Install into your project + +The recommended way to use Cartesi Skills is to install them into your project so your assistant loads them from `.agents/skills/` automatically. + +From your project root, add the Cartesi skills package: + +```shell +npx skills add Mugen-Builders/cartesi-skills +``` + +Confirm the skills appear under `.agents/skills/` (one folder per skill, each with a `SKILL.md`). Restart your editor or start a new agent session so the client picks up the new files. In your next prompt, name the skill you need (for example `cartesi-scaffold` or `cartesi-backend-js-ts`); see [Prompting](./prompting.md) for examples. + +Watch the **[skills setup walkthrough](https://youtu.be/wep_ZEPKt8s)** for a full install-and-verify flow in Cursor or another Agent Skills-compatible client. + +### Use skills with MCP or on their own + +Once installed, you can still use skills in two ways: + +- **Through the [Cartesi MCP server](./mcp-server.mdx)**: skills are bundled in the knowledge base and returned inline when your assistant queries the server, so you do not have to paste skill text into every chat. +- **On their own**: your assistant reads skill files directly from `.agents/skills/` when you name them in a prompt. You do not need to connect to the MCP server in this case. + +### Browse and copy (optional) + +If you prefer to inspect or copy a single skill without installing the full set, use [skills.mugen.builders](https://skills.mugen.builders/) to browse each skill and paste the content into your agent context. + +### Source and contributions + +The canonical source lives in the [cartesi-skills](https://github.com/Mugen-Builders/cartesi-skills) repository on GitHub. Open an issue or pull request there if you spot gaps, want a new skill, or have fixes to existing instructions. Contributions are welcome. + +:::caution Early release +Cartesi Skills v0.1.0 is an early release for testing and feedback. Be cautious when using private keys or mainnet credentials with AI-assisted workflows. +::: diff --git a/cartesi-rollups_versioned_sidebars/version-2.0-sidebars.json b/cartesi-rollups_versioned_sidebars/version-2.0-sidebars.json index 99facf74..0e5bf915 100644 --- a/cartesi-rollups_versioned_sidebars/version-2.0-sidebars.json +++ b/cartesi-rollups_versioned_sidebars/version-2.0-sidebars.json @@ -160,6 +160,17 @@ "development/asset-handling" ] }, + { + "type": "category", + "label": "Build with AI", + "collapsed": true, + "items": [ + "build-with-ai/overview", + "build-with-ai/mcp-server", + "build-with-ai/skills", + "build-with-ai/prompting" + ] + }, { "type": "category", "label": "Deployment", diff --git a/docusaurus.config.js b/docusaurus.config.js index adba332d..0ffcc7af 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -351,6 +351,7 @@ const config = { }), plugins: [ require.resolve("./plugins/serve-markdown.js"), + require.resolve("./plugins/copy-page-button"), [ "@docusaurus/plugin-content-docs", { diff --git a/package.json b/package.json index 69330f6b..711b5cae 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "autoprefixer": "^10.4.20", "classnames": "^2.5.1", "clsx": "^2.1.1", + "docusaurus-plugin-copy-page-button": "^0.5.2", "docusaurus-plugin-hotjar": "^0.0.2", "docusaurus-plugin-openapi-docs": "^4.2.0", "docusaurus-plugin-sass": "^0.2.5", @@ -69,11 +70,8 @@ "prettier": "^3.4.0", "typescript": "^5.7.2" }, - "engines": { - "node": ">=18.0" - }, - "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e", "engines": { "node": ">=20.0.0" - } + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/plugins/copy-page-button/CopyPageButton.js b/plugins/copy-page-button/CopyPageButton.js new file mode 100644 index 00000000..bfd472cb --- /dev/null +++ b/plugins/copy-page-button/CopyPageButton.js @@ -0,0 +1,378 @@ +import React, { useState, useEffect, useRef } from "react"; +import IconCopy from "@theme/Icon/Copy"; +import IconSuccess from "@theme/Icon/Success"; +import styles from "./styles.module.css"; +const { + extractPageMarkdownFromDocument, + getMarkdownRouteUrl, +} = require("./htmlToMarkdown"); + +// Utility function to merge custom styles with default classes +const mergeStyles = (defaultClassName, customStyleConfig = {}) => { + const { className: customClassName, style: customStyle } = customStyleConfig; + + const finalClassName = customClassName + ? `${defaultClassName} ${customClassName}` + : defaultClassName; + + return { + className: finalClassName, + style: customStyle || {} + }; +}; + +// Utility function to separate positioning styles from other styles +const separatePositioningStyles = (styleObject = {}) => { + const positioningProps = ['position', 'top', 'right', 'bottom', 'left', 'zIndex', 'transform']; + const positioning = {}; + const nonPositioning = {}; + + Object.entries(styleObject).forEach(([key, value]) => { + if (positioningProps.includes(key)) { + positioning[key] = value; + } else { + nonPositioning[key] = value; + } + }); + + return { positioning, nonPositioning }; +}; + +export default function CopyPageButton({ + customStyles = {}, + enabledActions = ['copy', 'view', 'chatgpt', 'claude', 'gemini'], + generateMarkdownRoutes = false +}) { + const [isOpen, setIsOpen] = useState(false); + const [copied, setCopied] = useState(false); + const [pageContent, setPageContent] = useState(""); + const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 }); + const dropdownRef = useRef(null); + const buttonRef = useRef(null); + const copyTimeoutRef = useRef(undefined); + + // Extract custom style configurations + const containerStyleConfig = customStyles.container || {}; + const buttonStyleConfig = customStyles.button || {}; + const dropdownStyleConfig = customStyles.dropdown || {}; + const dropdownItemStyleConfig = customStyles.dropdownItem || {}; + + useEffect(() => { + const handleClickOutside = (event) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target) && + buttonRef.current && + !buttonRef.current.contains(event.target) + ) { + setIsOpen(false); + } + }; + + if (isOpen) { + document.addEventListener("mousedown", handleClickOutside); + } + + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [isOpen]); + + useEffect(() => { + if (isOpen && buttonRef.current) { + const rect = buttonRef.current.getBoundingClientRect(); + setDropdownPosition({ + top: rect.bottom + 8, + left: rect.right - 300, // Align dropdown right edge with button + }); + } + }, [isOpen]); + + useEffect(() => { + if (typeof window === 'undefined') return; + + const content = extractPageContent(); + if (content) { + setPageContent(content); + } + }, []); + + useEffect(() => () => window.clearTimeout(copyTimeoutRef.current), []); + + const extractPageContent = () => { + return extractPageMarkdownFromDocument(document, window.location.href); + }; + + const copyToClipboard = async (text) => { + // If no content, try to extract it now + if (!text || text.trim() === '') { + const extractedContent = extractPageContent(); + if (extractedContent) { + setPageContent(extractedContent); + text = extractedContent; + } else { + return false; + } + } + + try { + if (navigator.clipboard && navigator.clipboard.writeText) { + await navigator.clipboard.writeText(text); + } else { + // Fallback for older browsers + const textArea = document.createElement('textarea'); + textArea.value = text; + document.body.appendChild(textArea); + textArea.select(); + document.execCommand('copy'); + document.body.removeChild(textArea); + } + return true; + } catch (err) { + return false; + } + }; + + const handleCopyPage = async () => { + const ok = await copyToClipboard(pageContent); + if (!ok) return; + setIsOpen(false); + setCopied(true); + window.clearTimeout(copyTimeoutRef.current); + copyTimeoutRef.current = window.setTimeout(() => setCopied(false), 1000); + }; + + const openInAI = (baseUrl, queryParam = 'q', extraParams = {}) => { + try { + const currentUrl = getMarkdownRouteUrl(window.location.href); + const prompt = `Please read and explain this documentation page: ${currentUrl} + +Please provide a clear summary and help me understand the key concepts covered in this documentation.`; + const params = new URLSearchParams({ [queryParam]: prompt, ...extraParams }); + window.open(`${baseUrl}?${params.toString()}`, "_blank"); + } catch (err) { + // Silently fail + } + }; + + const viewAsMarkdown = () => { + try { + const mdUrl = getMarkdownRouteUrl(window.location.href); + window.open(mdUrl, "_blank"); + } catch (err) { + // Silently fail + } + }; + + const allDropdownItems = [ + { + id: "copy", + title: "Copy page", + description: "Copy the page as Markdown for LLMs", + icon: ( + + + + + ), + action: handleCopyPage, + }, + { + id: "view", + title: "View as Markdown", + description: "View this page as plain text", + icon: ( + + + + + + + ), + action: viewAsMarkdown, + }, + { + id: "chatgpt", + title: "Open in ChatGPT", + description: "Ask questions about this page", + icon: ( + + + + ), + action: () => openInAI("https://chatgpt.com/"), + }, + { + id: "claude", + title: "Open in Claude", + description: "Ask questions about this page", + icon: ( + + + + ), + action: () => openInAI("https://claude.ai/new"), + }, + { + id: "gemini", + title: "Open in Gemini", + description: "Ask questions about this page", + icon: ( + + + + ), + action: () => openInAI("https://www.google.com/search", "q", { udm: "50" }), + }, + ]; + + // Filter dropdown items based on enabled actions + const dropdownItems = allDropdownItems.filter(item => + enabledActions.includes(item.id) + ); + + // Handle positioning styles - if button config has positioning, move it to container + const { positioning: buttonPositioning, nonPositioning: buttonNonPositioning } = + separatePositioningStyles(buttonStyleConfig.style); + + // Create final style configs + const finalContainerConfig = { + ...containerStyleConfig, + style: { + ...containerStyleConfig.style, + ...buttonPositioning, // Apply button positioning to container + } + }; + + const finalButtonConfig = { + ...buttonStyleConfig, + style: buttonNonPositioning, // Apply only non-positioning styles to button + }; + + // Merge custom styles with default styles + const containerProps = mergeStyles(styles.copyPageContainer, finalContainerConfig); + const buttonProps = mergeStyles( + copied ? `${styles.copyPageButton} ${styles.copyPageButtonCopied}` : styles.copyPageButton, + finalButtonConfig + ); + const dropdownProps = mergeStyles(styles.copyPageDropdown, dropdownStyleConfig); + const dropdownItemProps = mergeStyles(styles.dropdownItem, dropdownItemStyleConfig); + + return ( + <> +
+ +
+ + {isOpen && ( +
+ {dropdownItems.map((item) => ( + + ))} +
+ )} + + ); +} diff --git a/plugins/copy-page-button/client.js b/plugins/copy-page-button/client.js new file mode 100644 index 00000000..66f433df --- /dev/null +++ b/plugins/copy-page-button/client.js @@ -0,0 +1,534 @@ +import React from "react"; +import { createRoot } from "react-dom/client"; +import CopyPageButton from "./CopyPageButton"; +import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; + +// Only run in browser environment +if (ExecutionEnvironment.canUseDOM) { + let root = null; + let lastUrl = location.href; + let recheckInterval = null; + let injectionAttempts = 0; + + const getPluginOptions = () => { + return (typeof window !== "undefined" && window.__COPY_PAGE_BUTTON_OPTIONS__) || {}; + }; + + // Fallback injection for pages without TOC. + // Inject the button inline at the top of the article (right after the + // breadcrumbs if present, otherwise as the article's first child). Keeps the + // button in normal document flow — fixed-viewport placement is brittle + // because it overlaps navbars/edit-this-page widgets. + const injectToFallbackLocation = () => { + // Prefer the actual
element since that's where breadcrumbs/h1 live. + const article = document.querySelector("article"); + const articleContent = + article || + document.querySelector(".theme-doc-markdown") || + document.querySelector(".markdown") || + document.querySelector('[class*="docItemContainer"]') || + document.querySelector('main'); + + if (!articleContent) { + return; // No suitable container found + } + + let container = document.getElementById("copy-page-button-container"); + if (container && articleContent.contains(container)) { + return; // Already properly attached + } + + if (container) { + cleanup(); + } + + container = document.createElement("div"); + container.id = "copy-page-button-container"; + container.dataset.fallback = "true"; + + const pluginOptions = getPluginOptions(); + const customStyles = pluginOptions.customStyles || {}; + const buttonStyles = customStyles.button?.style || {}; + + // If the user explicitly set positioning props on the button config, honor them. + const positioningProps = ['position', 'top', 'right', 'bottom', 'left', 'zIndex', 'transform']; + let hasCustomPositioning = false; + positioningProps.forEach(prop => { + if (buttonStyles[prop] !== undefined) { + container.style[prop] = buttonStyles[prop]; + hasCustomPositioning = true; + } + }); + + // Default fallback: inline, right-aligned within the article column. Sits + // in the normal flow above the H1 so it doesn't fight with anything else. + if (!hasCustomPositioning) { + container.style.display = 'flex'; + container.style.justifyContent = 'flex-end'; + container.style.margin = '0 0 12px 0'; + } + + const containerStyles = customStyles.container?.style || {}; + Object.assign(container.style, containerStyles); + + // Place after breadcrumbs if present (typical Docusaurus docs layout), otherwise prepend. + const breadcrumbs = articleContent.querySelector(".theme-doc-breadcrumbs"); + if (breadcrumbs && breadcrumbs.parentElement === articleContent) { + breadcrumbs.insertAdjacentElement("afterend", container); + } else { + articleContent.insertBefore(container, articleContent.firstChild); + } + + if (root) { + try { + root.unmount(); + } catch (e) { + // Silent cleanup + } + } + root = createRoot(container); + + const renderOptions = getPluginOptions(); + root.render(React.createElement(CopyPageButton, { + customStyles: renderOptions.customStyles, + enabledActions: renderOptions.enabledActions, + generateMarkdownRoutes: renderOptions.generateMarkdownRoutes + })); + }; + + // Fast injection for navigation (when sidebar already exists) + const fastInjectCopyPageButton = () => { + const sidebar = + document.querySelector(".theme-doc-toc-desktop") || + document.querySelector(".table-of-contents") || + document.querySelector('[class*="tableOfContents"]') || + document.querySelector('[class*="toc"]'); + + if (!sidebar) { + // If no sidebar, try fallback injection to main content area + injectToFallbackLocation(); + return; + } + + let container = document.getElementById("copy-page-button-container"); + if (container && sidebar.contains(container)) { + return; // Already properly attached + } + + if (container) { + cleanup(); + } + + container = document.createElement("div"); + container.id = "copy-page-button-container"; + + // Apply custom positioning styles to the container if provided + const pluginOptions = getPluginOptions(); + const customStyles = pluginOptions.customStyles || {}; + const buttonStyles = customStyles.button?.style || {}; + + // Check if button config has positioning styles that should be applied to container + const positioningProps = ['position', 'top', 'right', 'bottom', 'left', 'zIndex', 'transform']; + positioningProps.forEach(prop => { + if (buttonStyles[prop] !== undefined) { + container.style[prop] = buttonStyles[prop]; + } + }); + + // Also apply container-specific styles + const containerStyles = customStyles.container?.style || {}; + Object.assign(container.style, containerStyles); + + sidebar.insertBefore(container, sidebar.firstChild); + + if (root) { + try { + root.unmount(); + } catch (e) { + // Silent cleanup + } + } + root = createRoot(container); + + const renderOptions = getPluginOptions(); + root.render(React.createElement(CopyPageButton, { + customStyles: renderOptions.customStyles, + enabledActions: renderOptions.enabledActions, + generateMarkdownRoutes: renderOptions.generateMarkdownRoutes + })); + }; + + // Reliable injection for page refresh/initial load (when DOM might not be ready) + const reliableInjectCopyPageButton = () => { + injectionAttempts++; + + const sidebar = + document.querySelector(".theme-doc-toc-desktop") || + document.querySelector(".table-of-contents") || + document.querySelector('[class*="tableOfContents"]') || + document.querySelector('[class*="toc"]'); + + if (!sidebar) { + // Try fallback injection to main content area + const articleContent = + document.querySelector("article") || + document.querySelector(".markdown") || + document.querySelector('[class*="docItemContainer"]') || + document.querySelector('.theme-doc-markdown') || + document.querySelector('main'); + + if (articleContent) { + injectToFallbackLocation(); + injectionAttempts = 0; // Reset counter on success + return; + } else if (injectionAttempts < 30) { // Try for 3 seconds max + setTimeout(reliableInjectCopyPageButton, 100); + } + return; + } + + let container = document.getElementById("copy-page-button-container"); + if (container && sidebar.contains(container)) { + injectionAttempts = 0; // Reset counter on success + return; // Already properly attached + } + + if (container) { + cleanup(); + } + + container = document.createElement("div"); + container.id = "copy-page-button-container"; + + // Apply custom positioning styles to the container if provided + const pluginOptions = getPluginOptions(); + const customStyles = pluginOptions.customStyles || {}; + const buttonStyles = customStyles.button?.style || {}; + + // Check if button config has positioning styles that should be applied to container + const positioningProps = ['position', 'top', 'right', 'bottom', 'left', 'zIndex', 'transform']; + positioningProps.forEach(prop => { + if (buttonStyles[prop] !== undefined) { + container.style[prop] = buttonStyles[prop]; + } + }); + + // Also apply container-specific styles + const containerStyles = customStyles.container?.style || {}; + Object.assign(container.style, containerStyles); + + sidebar.insertBefore(container, sidebar.firstChild); + + if (root) { + try { + root.unmount(); + } catch (e) { + // Silent cleanup + } + } + root = createRoot(container); + + const renderOptions = getPluginOptions(); + root.render(React.createElement(CopyPageButton, { + customStyles: renderOptions.customStyles, + enabledActions: renderOptions.enabledActions, + generateMarkdownRoutes: renderOptions.generateMarkdownRoutes + })); + + // Reset injection attempts on successful injection + injectionAttempts = 0; + + // Clear any existing recheck interval since button is now injected + if (recheckInterval) { + clearInterval(recheckInterval); + recheckInterval = null; + } + }; + + const cleanup = () => { + const container = document.getElementById("copy-page-button-container"); + if (container) { + if (root) { + try { + root.unmount(); + } catch (e) { + // Silent cleanup + } + } + container.remove(); + } + }; + + // Fast route change handler (navigation within SPA) + const handleRouteChange = () => { + // Check if button is properly attached before cleaning up + const container = document.getElementById("copy-page-button-container"); + const sidebar = + document.querySelector(".theme-doc-toc-desktop") || + document.querySelector(".table-of-contents") || + document.querySelector('[class*="tableOfContents"]') || + document.querySelector('[class*="toc"]'); + + // Check if button is attached to sidebar or fallback location + const articleContent = + document.querySelector("article") || + document.querySelector(".markdown") || + document.querySelector('[class*="docItemContainer"]') || + document.querySelector('.theme-doc-markdown') || + document.querySelector('main'); + + const buttonProperlyAttached = container && ( + (sidebar && sidebar.contains(container)) || + (articleContent && articleContent.contains(container)) + ); + + // Only cleanup and re-inject if button is not properly attached + if (!buttonProperlyAttached) { + cleanup(); + + // Clear any existing recheck interval + if (recheckInterval) { + clearInterval(recheckInterval); + recheckInterval = null; + } + + // Use fast injection for navigation since DOM is already stable + if (window.innerWidth <= 996) { + // Mobile/tablet: small delay for sidebar re-rendering + setTimeout(fastInjectCopyPageButton, 50); + } else { + // Desktop: immediate injection + fastInjectCopyPageButton(); + } + } + }; + + let mountObserver = null; + + // Find the ToC sidebar element using all known selectors. + const findSidebar = () => + document.querySelector(".theme-doc-toc-desktop") || + document.querySelector(".table-of-contents") || + document.querySelector('[class*="tableOfContents"]') || + document.querySelector('[class*="toc"]'); + + // Find the article content element (used for the no-ToC fallback). + const findArticleContent = () => + document.querySelector("article") || + document.querySelector(".theme-doc-markdown") || + document.querySelector(".markdown") || + document.querySelector('[class*="docItemContainer"]') || + document.querySelector('main'); + + // Reliable initialization for page refresh/initial load. + // Uses MutationObserver as the primary detection mechanism — fires the + // moment the ToC or article mounts, without waiting for a setTimeout poll + // cycle. Falls back to periodic polling as a safety net for edge cases + // where the observer misses the event (e.g. async theme hydration). + const initializeButton = () => { + injectionAttempts = 0; + + const stopMountObserver = () => { + if (mountObserver) { + mountObserver.disconnect(); + mountObserver = null; + } + }; + + const tryInject = () => { + const sidebar = findSidebar(); + const articleContent = findArticleContent(); + if (!sidebar && !articleContent) { + return false; + } + reliableInjectCopyPageButton(); + return true; + }; + + // Strategy 1: synchronous attempt if DOM is already there. + if (tryInject()) { + return; + } + + // Strategy 2: MutationObserver watches for the ToC or article to appear. + // This is the primary mechanism — it fires immediately on mount instead + // of waiting for the next poll tick. + stopMountObserver(); + mountObserver = new MutationObserver(() => { + if (tryInject()) { + stopMountObserver(); + } + }); + mountObserver.observe(document.body, { childList: true, subtree: true }); + + // Strategy 3: backup periodic check. Some Docusaurus hydration patterns + // (especially with @docusaurus/faster) re-render the ToC after initial + // mount, which the observer may have already disconnected from. + startPeriodicCheck(); + + // Hard stop after 15s so we don't keep an observer alive forever on + // pages that genuinely have no article + no ToC (404s, custom layouts). + setTimeout(stopMountObserver, 15000); + }; + + // Periodic check - only for initial page load issues + const startPeriodicCheck = () => { + let recheckCount = 0; + const maxRechecks = 30; // 15 seconds total - increased for slower loading pages + + // Clear any existing interval + if (recheckInterval) { + clearInterval(recheckInterval); + } + + recheckInterval = setInterval(() => { + recheckCount++; + const container = document.getElementById("copy-page-button-container"); + const sidebar = document.querySelector(".theme-doc-toc-desktop") || + document.querySelector(".table-of-contents") || + document.querySelector('[class*="tableOfContents"]') || + document.querySelector('[class*="toc"]'); + + const articleContent = + document.querySelector("article") || + document.querySelector(".markdown") || + document.querySelector('[class*="docItemContainer"]') || + document.querySelector('.theme-doc-markdown') || + document.querySelector('main'); + + const needsInjection = (sidebar || articleContent) && (!container || + (!sidebar || !sidebar.contains(container)) && + (!articleContent || !articleContent.contains(container))); + + if (needsInjection) { + reliableInjectCopyPageButton(); + } + + const buttonProperlyAttached = container && ( + (sidebar && sidebar.contains(container)) || + (articleContent && articleContent.contains(container)) + ); + + if (recheckCount >= maxRechecks || buttonProperlyAttached) { + clearInterval(recheckInterval); + recheckInterval = null; + } + }, 500); + }; + + // Initialize when DOM is ready (only for page refresh/initial load) + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => { + setTimeout(initializeButton, 100); + }); + } else { + setTimeout(initializeButton, 100); + } + + // Force re-injection on page visibility change (helps with tab switching and refreshes) + document.addEventListener('visibilitychange', () => { + if (!document.hidden) { + setTimeout(() => { + const container = document.getElementById("copy-page-button-container"); + const sidebar = document.querySelector(".theme-doc-toc-desktop") || + document.querySelector(".table-of-contents") || + document.querySelector('[class*="tableOfContents"]') || + document.querySelector('[class*="toc"]'); + + const articleContent = + document.querySelector("article") || + document.querySelector(".markdown") || + document.querySelector('[class*="docItemContainer"]') || + document.querySelector('.theme-doc-markdown') || + document.querySelector('main'); + + const needsInjection = (sidebar || articleContent) && (!container || + (!sidebar || !sidebar.contains(container)) && + (!articleContent || !articleContent.contains(container))); + + if (needsInjection) { + reliableInjectCopyPageButton(); + } + }, 200); + } + }); + + // Handle responsive layout changes + window.addEventListener("resize", () => { + setTimeout(() => { + const container = document.getElementById("copy-page-button-container"); + const sidebar = + document.querySelector(".theme-doc-toc-desktop") || + document.querySelector(".table-of-contents") || + document.querySelector('[class*="tableOfContents"]') || + document.querySelector('[class*="toc"]'); + + const articleContent = + document.querySelector("article") || + document.querySelector(".markdown") || + document.querySelector('[class*="docItemContainer"]') || + document.querySelector('.theme-doc-markdown') || + document.querySelector('main'); + + const sidebarVisible = + sidebar && + sidebar.offsetWidth > 0 && + sidebar.offsetHeight > 0 && + window.getComputedStyle(sidebar).display !== "none"; + + const buttonProperlyAttached = container && ( + (sidebar && sidebar.contains(container)) || + (articleContent && articleContent.contains(container)) + ); + + if ((sidebarVisible || articleContent) && !buttonProperlyAttached) { + cleanup(); + fastInjectCopyPageButton(); // Use fast injection for resize + } else if (!sidebarVisible && !articleContent && buttonProperlyAttached) { + cleanup(); + } + }, 300); + }); + + // Handle browser navigation + window.addEventListener("popstate", handleRouteChange); + + // Handle Docusaurus route changes + if (typeof window !== "undefined" && window.docusaurus) { + document.addEventListener("docusaurus-route-update", handleRouteChange); + } + + // Targeted URL change detection for SPA routing + const checkUrlChange = () => { + if (location.href !== lastUrl) { + const currentPathname = location.pathname; + const lastPathname = new URL(lastUrl).pathname; + + // Only trigger route change for actual page changes, not hash/query changes + if (currentPathname !== lastPathname) { + lastUrl = location.href; + handleRouteChange(); // Use fast route change handler + } else { + // Just update the URL without triggering re-injection + lastUrl = location.href; + } + } + }; + + // Check for URL changes - keep this for SPA navigation + setInterval(checkUrlChange, 100); + + // Also intercept pushState/replaceState for immediate detection + const originalPushState = history.pushState; + const originalReplaceState = history.replaceState; + + history.pushState = function (...args) { + originalPushState.apply(this, args); + setTimeout(checkUrlChange, 0); + }; + + history.replaceState = function (...args) { + originalReplaceState.apply(this, args); + setTimeout(checkUrlChange, 0); + }; +} diff --git a/plugins/copy-page-button/htmlToMarkdown.js b/plugins/copy-page-button/htmlToMarkdown.js new file mode 100644 index 00000000..a362abf4 --- /dev/null +++ b/plugins/copy-page-button/htmlToMarkdown.js @@ -0,0 +1,754 @@ +const TEXT_NODE = 3; +const ELEMENT_NODE = 1; +const DOCUMENT_NODE = 9; +const DOCUMENT_FRAGMENT_NODE = 11; + +const SELECTORS_TO_REMOVE = [ + ".theme-edit-this-page", + ".theme-last-updated", + ".pagination-nav", + ".theme-doc-breadcrumbs", + ".theme-doc-footer", + "button", + ".copy-code-button", + ".buttonGroup", + ".clean-btn", + ".theme-code-block-title", + ".line-number", +]; + +const RAW_TEXT_TAGS = new Set(["script", "style", "textarea", "title"]); +const VOID_TAGS = new Set([ + "area", + "base", + "br", + "col", + "embed", + "hr", + "img", + "input", + "link", + "meta", + "param", + "source", + "track", + "wbr", +]); + +const cleanText = (text = "") => { + return text + .replace(/[\u200B-\u200D\uFEFF]/g, "") + .replace(/\u00A0/g, " ") + .replace(/[\u2018\u2019]/g, "'") + .replace(/[\u201C\u201D]/g, '"') + .replace(/\u00e2\u20ac\u2039/g, "") + .replace(/\s+/g, " ") + .trim(); +}; + +const decodeHtmlEntities = (value = "") => { + return String(value).replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (entity, code) => { + const lowerCode = code.toLowerCase(); + if (lowerCode === "amp") return "&"; + if (lowerCode === "lt") return "<"; + if (lowerCode === "gt") return ">"; + if (lowerCode === "quot") return '"'; + if (lowerCode === "apos") return "'"; + if (lowerCode === "nbsp") return " "; + + if (lowerCode.startsWith("#x")) { + const codePoint = parseInt(lowerCode.slice(2), 16); + try { + return Number.isNaN(codePoint) + ? entity + : String.fromCodePoint(codePoint); + } catch (error) { + return entity; + } + } + if (lowerCode.startsWith("#")) { + const codePoint = parseInt(lowerCode.slice(1), 10); + try { + return Number.isNaN(codePoint) + ? entity + : String.fromCodePoint(codePoint); + } catch (error) { + return entity; + } + } + return entity; + }); +}; + +const appendTextNode = (parent, data) => { + if (!data) return; + parent.children.push({ + type: "text", + data: decodeHtmlEntities(data), + parent, + }); +}; + +const parseAttributes = (source = "") => { + const attrs = {}; + const attrPattern = + /([^\s=/>]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g; + let match; + + while ((match = attrPattern.exec(source))) { + attrs[match[1]] = decodeHtmlEntities( + match[2] ?? match[3] ?? match[4] ?? "" + ); + } + + return attrs; +}; + +const parseTag = (source) => { + const match = source.match(/^<\/?\s*([a-zA-Z][\w:-]*)\s*([\s\S]*?)\/?\s*>$/); + if (!match) return null; + + const tagName = match[1].toLowerCase(); + const attrSource = match[2] || ""; + return { + tagName, + attrs: parseAttributes(attrSource), + selfClosing: /\/\s*>$/.test(source) || VOID_TAGS.has(tagName), + }; +}; + +const parseHtmlToTree = (html) => { + const root = { + type: "root", + children: [], + parent: null, + }; + const stack = [root]; + let index = 0; + + while (index < html.length) { + const openIndex = html.indexOf("<", index); + if (openIndex === -1) { + appendTextNode(stack[stack.length - 1], html.slice(index)); + break; + } + + appendTextNode(stack[stack.length - 1], html.slice(index, openIndex)); + + if (html.startsWith("", openIndex + 4); + index = closeIndex === -1 ? html.length : closeIndex + 3; + continue; + } + + const closeIndex = html.indexOf(">", openIndex + 1); + if (closeIndex === -1) { + appendTextNode(stack[stack.length - 1], html.slice(openIndex)); + break; + } + + const tagSource = html.slice(openIndex, closeIndex + 1); + if (/^<\s*[!?]/.test(tagSource)) { + index = closeIndex + 1; + continue; + } + + if (/^<\s*\//.test(tagSource)) { + const closingTag = tagSource.match(/^<\s*\/\s*([a-zA-Z][\w:-]*)/); + if (closingTag) { + const tagName = closingTag[1].toLowerCase(); + for (let i = stack.length - 1; i > 0; i--) { + if (stack[i].name === tagName) { + stack.length = i; + break; + } + } + } + index = closeIndex + 1; + continue; + } + + const parsedTag = parseTag(tagSource); + if (!parsedTag) { + appendTextNode(stack[stack.length - 1], tagSource); + index = closeIndex + 1; + continue; + } + + const parent = stack[stack.length - 1]; + const node = { + type: "tag", + name: parsedTag.tagName, + attribs: parsedTag.attrs, + children: [], + parent, + }; + parent.children.push(node); + + if (RAW_TEXT_TAGS.has(parsedTag.tagName)) { + const closingTag = ``; + const rawCloseIndex = html + .toLowerCase() + .indexOf(closingTag, closeIndex + 1); + if (rawCloseIndex === -1) { + appendTextNode(node, html.slice(closeIndex + 1)); + break; + } + appendTextNode(node, html.slice(closeIndex + 1, rawCloseIndex)); + index = rawCloseIndex + closingTag.length; + continue; + } + + if (!parsedTag.selfClosing) { + stack.push(node); + } + index = closeIndex + 1; + } + + return root; +}; + +const parseHtml = (html) => { + if (typeof DOMParser !== "undefined") { + return { + root: new DOMParser().parseFromString(html, "text/html"), + context: {}, + }; + } + + return { + root: parseHtmlToTree(html), + context: {}, + }; +}; + +const normalizeInput = (input) => { + if (typeof input === "string") { + return parseHtml(input); + } + + return { + root: input, + context: {}, + }; +}; + +const isTextNode = (node) => { + return node?.nodeType === TEXT_NODE || node?.type === "text"; +}; + +const isElementNode = (node) => { + return node?.nodeType === ELEMENT_NODE || node?.type === "tag"; +}; + +const isContainerNode = (node) => { + return ( + node?.nodeType === DOCUMENT_NODE || + node?.nodeType === DOCUMENT_FRAGMENT_NODE || + node?.type === "root" + ); +}; + +const getChildNodes = (node) => { + return Array.from(node?.childNodes || node?.children || []); +}; + +const getTagName = (node) => { + return (node?.tagName || node?.name || "").toLowerCase(); +}; + +const getAttribute = (node, name) => { + if (!node) return null; + if (typeof node.getAttribute === "function") { + return node.getAttribute(name); + } + return node.attribs?.[name] || null; +}; + +const getClassName = (node) => { + const className = node?.className; + if (typeof className === "string") { + return className; + } + if (typeof className?.baseVal === "string") { + return className.baseVal; + } + return getAttribute(node, "class") || ""; +}; + +const textContentOf = (node) => { + if (!node) return ""; + if (typeof node.textContent === "string") { + return node.textContent; + } + if (isTextNode(node)) { + return node.data || ""; + } + return getChildNodes(node).map(textContentOf).join(""); +}; + +const queryAll = (node, selector, context = {}) => { + if (!node) return []; + if (typeof node.querySelectorAll === "function") { + return Array.from(node.querySelectorAll(selector)); + } + if (context.$) { + return context.$(node).find(selector).toArray(); + } + return queryTree(node, selector); +}; + +const queryOne = (node, selector, context = {}) => { + if (!node) return null; + if (typeof node.querySelector === "function") { + return node.querySelector(selector); + } + if (context.$) { + return context.$(node).find(selector).get(0) || null; + } + return queryTree(node, selector)[0] || null; +}; + +const matchesSimpleSelector = (node, selector) => { + if (!isElementNode(node)) return false; + + const attrMatch = selector.match(/^([a-zA-Z][\w:-]*)?\[([^\]=]+)(?:=([^\]]+))?\]$/); + if (attrMatch) { + const tagName = attrMatch[1]; + const attrName = attrMatch[2]; + if (tagName && getTagName(node) !== tagName.toLowerCase()) { + return false; + } + return getAttribute(node, attrName) !== null; + } + + const classMatch = selector.match(/^([a-zA-Z][\w:-]*)?\.([\w-]+)$/); + if (classMatch) { + const tagName = classMatch[1]; + const className = classMatch[2]; + if (tagName && getTagName(node) !== tagName.toLowerCase()) { + return false; + } + return hasClass(node, className); + } + + if (selector.startsWith(".")) { + return hasClass(node, selector.slice(1)); + } + + return getTagName(node) === selector.toLowerCase(); +}; + +const matchesSelectorChain = (node, selector) => { + const parts = selector.split(/\s+/).filter(Boolean); + let current = node; + + for (let i = parts.length - 1; i >= 0; i--) { + if (i === parts.length - 1) { + if (!matchesSimpleSelector(current, parts[i])) { + return false; + } + current = current.parent; + continue; + } + + while (current && !matchesSimpleSelector(current, parts[i])) { + current = current.parent; + } + if (!current) { + return false; + } + current = current.parent; + } + + return true; +}; + +const queryTree = (node, selector) => { + const selectors = selector + .split(",") + .map((item) => item.trim()) + .filter(Boolean); + const results = []; + + const visit = (current) => { + getChildNodes(current).forEach((child) => { + if ( + isElementNode(child) && + selectors.some((item) => matchesSelectorChain(child, item)) + ) { + results.push(child); + } + visit(child); + }); + }; + + visit(node); + return results; +}; + +const cloneTree = (node, parent = null) => { + if (isTextNode(node)) { + return { + type: "text", + data: node.data || "", + parent, + }; + } + + const clone = { + type: node.type, + name: node.name, + attribs: { ...(node.attribs || {}) }, + children: [], + parent, + }; + clone.children = getChildNodes(node).map((child) => cloneTree(child, clone)); + return clone; +}; + +const cloneNode = (node, context = {}) => { + if (!node) return null; + if (typeof node.cloneNode === "function") { + return node.cloneNode(true); + } + if (context.$) { + return context.$(node).clone().get(0) || null; + } + return cloneTree(node); +}; + +const removeNode = (node, context = {}) => { + if (!node) return; + if (typeof node.remove === "function") { + node.remove(); + return; + } + if (context.$) { + context.$(node).remove(); + return; + } + const siblings = node.parent?.children; + if (Array.isArray(siblings)) { + const index = siblings.indexOf(node); + if (index !== -1) { + siblings.splice(index, 1); + } + } +}; + +const hasClass = (node, className) => { + if (node?.classList?.contains(className)) { + return true; + } + return getClassName(node).split(/\s+/).includes(className); +}; + +const hasUserSelectNone = (node) => { + if (node?.style?.userSelect === "none") { + return true; + } + return /(?:^|;)\s*user-select\s*:\s*none\s*(?:;|$)/i.test( + getAttribute(node, "style") || "" + ); +}; + +const joinChildren = (childResults) => { + let children = ""; + for (let i = 0; i < childResults.length; i++) { + const current = childResults[i]; + const previous = i > 0 ? childResults[i - 1] : ""; + + if (current) { + if ( + previous && + !previous.match(/[\s\n]$/) && + !current.match(/^[\s\n]/) && + previous.trim() && + current.trim() + ) { + children += " "; + } + children += current; + } + } + + return children; +}; + +const processNode = (node, context = {}) => { + if (isTextNode(node)) { + return cleanText(textContentOf(node)); + } + + if (isContainerNode(node)) { + return joinChildren(getChildNodes(node).map((child) => processNode(child, context))); + } + + if (isElementNode(node)) { + const tag = getTagName(node); + const childResults = getChildNodes(node).map((child) => + processNode(child, context) + ); + const children = joinChildren(childResults); + + switch (tag) { + case "h1": + return `\n# ${children.trim()}\n\n`; + case "h2": + return `\n## ${children.trim()}\n\n`; + case "h3": + return `\n### ${children.trim()}\n\n`; + case "h4": + return `\n#### ${children.trim()}\n\n`; + case "h5": + return `\n##### ${children.trim()}\n\n`; + case "h6": + return `\n###### ${children.trim()}\n\n`; + case "p": + return children.trim() ? `${children.trim()}\n\n` : "\n"; + case "strong": + case "b": + return `**${children}**`; + case "em": + case "i": + return `*${children}*`; + case "code": + if (getTagName(node.parentElement || node.parent) === "pre") { + return children; + } + return `\`${children + .replace(/[\u200B-\u200D\uFEFF]/g, "") + .replace(/\u00A0/g, " ") + .trim()}\``; + case "pre": { + const codeElement = queryOne(node, "code", context); + if (codeElement) { + const codeClassName = getClassName(codeElement); + const preClassName = getClassName(node); + const language = + (codeClassName.match(/language-(\w+)/) || + preClassName.match(/language-(\w+)/) || + codeClassName.match(/hljs-(\w+)/) || + codeClassName.match(/prism-(\w+)/) || + [])[1] || ""; + + let codeContent = ""; + + try { + const originalContent = + getAttribute(codeElement, "data-code") || + getAttribute(node, "data-code") || + getAttribute(codeElement, "data-raw"); + + if (originalContent) { + codeContent = originalContent; + } else { + const codeLines = queryAll( + codeElement, + "span[data-line], .token-line, .code-line, .highlight-line", + context + ); + if (codeLines.length > 0) { + codeContent = codeLines.map(textContentOf).join("\n"); + } else { + const codeLineDivs = queryAll(codeElement, "div", context); + if (codeLineDivs.length > 0) { + codeContent = codeLineDivs + .map((lineDiv) => { + const lineClassName = getClassName(lineDiv); + if ( + lineClassName.includes("codeLineNumber") || + lineClassName.includes("LineNumber") || + lineClassName.includes("line-number") || + hasUserSelectNone(lineDiv) + ) { + return null; + } + return textContentOf(lineDiv); + }) + .filter((line) => line !== null) + .join("\n"); + } else { + let rawText = textContentOf(codeElement); + rawText = rawText.replace(/^\d+\s+/gm, ""); + rawText = rawText.replace(/^Copy$/gm, ""); + rawText = rawText.replace(/^Copied!$/gm, ""); + rawText = rawText.replace(/^\s*Copy to clipboard\s*$/gm, ""); + + codeContent = rawText; + } + } + } + + codeContent = codeContent + .replace(/[\u200B-\u200D\uFEFF]/g, "") + .replace(/\u00A0/g, " ") + .trim(); + codeContent = codeContent.replace(/^\n+|\n+$/g, ""); + } catch (error) { + codeContent = textContentOf(codeElement); + } + + return `\n\`\`\`${language}\n${codeContent}\n\`\`\`\n\n`; + } + return `\n\`\`\`\n${children}\n\`\`\`\n\n`; + } + case "ul": + return `\n${children}`; + case "ol": { + const items = queryAll(node, "li", context); + return ( + "\n" + + items + .map( + (item, index) => + `${index + 1}. ${processNode(item, context) + .replace(/^- /, "") + .trim()}\n` + ) + .join("") + ); + } + case "li": + return `- ${children.trim()}\n`; + case "a": { + const href = getAttribute(node, "href"); + if (href && !href.startsWith("#") && children.trim()) { + return `[${children.trim()}](${href})`; + } + return children; + } + case "br": + return "\n"; + case "blockquote": + return `\n> ${children.trim()}\n\n`; + case "table": + return `\n${children}\n`; + case "tr": + return `${children}\n`; + case "th": + case "td": + return `| ${children.trim()} `; + case "img": { + const src = getAttribute(node, "src"); + const alt = getAttribute(node, "alt") || ""; + return src ? `![${alt}](${src})` : ""; + } + case "div": + case "section": + case "article": + if (hasClass(node, "admonition")) { + const type = + getClassName(node) + .split(/\s+/) + .find((cls) => cls.startsWith("alert--")) + ?.replace("alert--", "") || "note"; + return `\n> **${type.toUpperCase()}**: ${children.trim()}\n\n`; + } + return `${children}\n`; + default: + return children; + } + } + + return ""; +}; + +const convertToMarkdown = (input) => { + const { root, context } = normalizeInput(input); + if (!root) return ""; + + return processNode(root, context) + .replace(/\n{3,}/g, "\n\n") + .replace(/^\n+|\n+$/g, "") + .trim(); +}; + +const extractPageMarkdownFromRoot = ( + root, + pageUrl, + context = {}, + options = {} +) => { + const mainContent = + queryOne(root, "main article", context) || + queryOne(root, "main .markdown", context); + if (options.requireDocContent && !mainContent) { + return ""; + } + + const targetElement = + mainContent || + queryOne(root, "main", context) || + queryOne(root, "article", context) || + queryOne(root, ".main-wrapper", context); + + if (!targetElement) return ""; + + const clone = cloneNode(targetElement, context); + if (!clone) return ""; + + SELECTORS_TO_REMOVE.forEach((selector) => { + queryAll(clone, selector, context).forEach((el) => removeNode(el, context)); + }); + + const firstH1 = queryOne(clone, "h1", context); + const title = firstH1 ? cleanText(textContentOf(firstH1)) : "Documentation Page"; + removeNode(firstH1, context); + + const content = convertToMarkdown(clone); + return `# ${title}\n\nURL: ${pageUrl}\n\n${content}`; +}; + +const extractPageMarkdownFromDocument = (documentLike, pageUrl) => { + return extractPageMarkdownFromRoot(documentLike, pageUrl); +}; + +const extractPageMarkdownFromHtml = (html, pageUrl, options = {}) => { + const { root, context } = parseHtml(html); + return extractPageMarkdownFromRoot(root, pageUrl, context, options); +}; + +const toMarkdownPathname = (pathname) => { + let normalized = pathname || "/"; + if (normalized.endsWith("/")) { + normalized = normalized.slice(0, -1); + } + if (!normalized) { + normalized = "/index"; + } + if (normalized.endsWith(".md")) { + return normalized; + } + + const lastSegment = normalized.slice(normalized.lastIndexOf("/") + 1); + if (lastSegment.includes(".")) { + return normalized.replace(/\.[^/.]+$/, ".md"); + } + return `${normalized}.md`; +}; + +const getMarkdownRouteUrl = (pageUrl) => { + try { + const url = new URL(pageUrl); + url.hash = ""; + url.search = ""; + url.pathname = toMarkdownPathname(url.pathname); + return url.toString(); + } catch (error) { + const [withoutHash] = String(pageUrl).split("#"); + const [withoutSearch] = withoutHash.split("?"); + return toMarkdownPathname(withoutSearch); + } +}; + +module.exports = { + SELECTORS_TO_REMOVE, + cleanText, + convertToMarkdown, + extractPageMarkdownFromDocument, + extractPageMarkdownFromHtml, + getMarkdownRouteUrl, + toMarkdownPathname, +}; diff --git a/plugins/copy-page-button/index.js b/plugins/copy-page-button/index.js new file mode 100644 index 00000000..b2922794 --- /dev/null +++ b/plugins/copy-page-button/index.js @@ -0,0 +1,18 @@ +const path = require("path"); +const basePlugin = require("docusaurus-plugin-copy-page-button"); + +/** + * Local wrapper around docusaurus-plugin-copy-page-button. + * Uses a vendored client bundle that opens serve-markdown .md URLs + * instead of in-browser blob URLs. + */ +module.exports = function copyPageButtonPlugin(context, options) { + const plugin = basePlugin(context, options); + + return { + ...plugin, + getClientModules() { + return [path.resolve(__dirname, "client.js")]; + }, + }; +}; diff --git a/plugins/copy-page-button/styles.module.css b/plugins/copy-page-button/styles.module.css new file mode 100644 index 00000000..76673b8d --- /dev/null +++ b/plugins/copy-page-button/styles.module.css @@ -0,0 +1,274 @@ +/* Flow at the top of the TOC column — content-width pill, right-aligned. */ +#copy-page-button-container:not([data-fallback]) { + position: static; + display: flex; + justify-content: flex-end; + width: 100%; + z-index: 1; + pointer-events: auto; + min-height: 0; + padding-bottom: 0; + margin-bottom: 1rem; + box-sizing: border-box; +} + +#copy-page-button-container > * { + pointer-events: auto; +} + +.copyPageContainer { + position: relative; + display: inline-block; + width: fit-content; + max-width: 100%; +} + +.copyPageButton { + display: inline-flex; + align-items: center; + justify-content: flex-start; + gap: 8px; + width: fit-content; + padding: 8px 12px; + margin-bottom: 0; + background: var(--ifm-background-color); + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: 6px; + color: var(--ifm-font-color-base); + cursor: pointer; + font-family: var(--ifm-font-family-base, inherit); + font-size: 13px; + font-weight: 500; + line-height: 1.25; + transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease; + white-space: nowrap; +} + +.copyPageButton:hover { + background: var(--ifm-color-emphasis-100); + border-color: var(--ifm-color-emphasis-400); +} + +.copyPageButton:focus { + background: var(--ifm-color-emphasis-100); + border-color: var(--ifm-color-primary); + outline: none; +} + +.copyPageButtonCopied { + border-color: var(--ifm-color-primary); + color: var(--ifm-color-primary); +} + +.buttonIcons { + position: relative; + flex-shrink: 0; + width: 1rem; + height: 1rem; +} + +.buttonIcon, +.buttonSuccessIcon { + position: absolute; + top: 0; + left: 0; + width: 1rem; + height: 1rem; + fill: currentColor; + transition: all var(--ifm-transition-fast) ease; +} + +.buttonSuccessIcon { + top: 50%; + left: 50%; + transform: translate(-50%, -50%) scale(0.33); + opacity: 0; +} + +.copyPageButtonCopied .buttonIcon { + transform: scale(0.33); + opacity: 0; +} + +.copyPageButtonCopied .buttonSuccessIcon { + transform: translate(-50%, -50%) scale(1); + opacity: 1; + transition-delay: 0.075s; +} + +.copyPageButton svg { + flex-shrink: 0; +} + +.chevron { + transition: transform 0.2s ease; +} + +.chevron.open { + transform: rotate(180deg); +} + +.copyPageDropdown { + min-width: 300px; + background: var(--ifm-dropdown-background-color, #1c1e21); + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + overflow: hidden; +} + +.dropdownItem { + display: flex; + align-items: center; + gap: 12px; + width: 100%; + padding: 12px 16px; + background: transparent; + border: none; + color: var(--ifm-font-color-base); + cursor: pointer; + font-family: var(--ifm-font-family-base, inherit); + text-align: left; + transition: background-color 0.2s ease; + border-bottom: 1px solid var(--ifm-color-emphasis-200); +} + +.dropdownItem:last-child { + border-bottom: none; +} + +.dropdownItem:hover { + background: var(--ifm-color-emphasis-100); +} + +.dropdownItem svg { + flex-shrink: 0; + opacity: 0.7; +} + +.itemTitle { + font-size: 14px; + font-weight: 500; + margin-bottom: 2px; + color: var(--ifm-font-color-base); +} + +.itemDescription { + font-size: 13px; + color: var(--ifm-color-emphasis-700); + line-height: 1.3; +} + +[data-theme="dark"] .copyPageButton { + background: var(--ifm-background-surface-color, var(--ifm-background-color)); + border-color: var(--ifm-color-emphasis-300); +} + +[data-theme="dark"] .copyPageButton:hover, +[data-theme="dark"] .copyPageButton:focus { + background: var(--ifm-color-emphasis-200); + border-color: var(--ifm-color-emphasis-400); +} + +[data-theme="dark"] .copyPageDropdown { + background: var(--ifm-dropdown-background-color); + border-color: var(--ifm-color-emphasis-300); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); +} + +[data-theme="dark"] .dropdownItem:hover { + background: var(--ifm-color-emphasis-200); +} + +[data-theme="light"] .copyPageButton { + background: #ffffff; + border-color: #d0d7de; + color: #24292f; +} + +[data-theme="light"] .copyPageButton:hover, +[data-theme="light"] .copyPageButton:focus { + background: #f6f8fa; + border-color: #8c959f; +} + +[data-theme="light"] .copyPageDropdown { + background: #ffffff; + border-color: #d0d7de; + box-shadow: 0 8px 24px rgba(140, 149, 159, 0.2); +} + +[data-theme="light"] .dropdownItem { + color: #24292f; + border-color: #d0d7de; +} + +[data-theme="light"] .dropdownItem:hover { + background: #f6f8fa; +} + +[data-theme="light"] .itemDescription { + color: #656d76; +} + +/* Hide the "Copy page" text on smaller screens (below 768px) */ +@media (max-width: 767px) { + .copyPageText { + display: none; + } +} + +/* Force breadcrumbs to wrap earlier to avoid overlap with copy button */ +:global(.theme-doc-breadcrumbs) { + /* Reserve space for copy button on smaller screens */ + @media (max-width: 996px) { + max-width: calc(100% - 120px); /* Reserve 120px for copy button */ + flex-wrap: wrap; + } + + @media (max-width: 768px) { + max-width: calc(100% - 80px); /* Reserve 80px for icon-only button */ + } + + @media (max-width: 480px) { + max-width: calc(100% - 60px); /* Reserve 60px for very small screens */ + } +} + +/* Ensure breadcrumb items can wrap */ +:global(.theme-doc-breadcrumbs .breadcrumbs__item) { + flex-shrink: 1; + min-width: 0; +} + +/* Make breadcrumb links wrap text if needed */ +:global(.theme-doc-breadcrumbs .breadcrumbs__link) { + white-space: normal; + word-break: break-word; + hyphens: auto; +} + +:global(#copy-page-button-container:not([data-fallback])) { + @media (max-width: 996px) { + position: absolute; + right: 16px; + top: 75px; + z-index: 50; + width: auto; + margin-bottom: 0; + } +} + +@media (max-width: 996px) { + .copyPageContainer { + width: auto; + } + + .copyPageButton { + width: auto; + } + + .copyPageDropdown { + max-width: 300px; + } +} diff --git a/yarn.lock b/yarn.lock index fba0aeda..47ee7d16 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3596,6 +3596,11 @@ resolved "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== +"@yarnpkg/lockfile@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" + integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== + abbrev@^3.0.0, abbrev@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-3.0.1.tgz#8ac8b3b5024d31464fe2a5feeea9f4536bf44025" @@ -4314,6 +4319,14 @@ call-bound@^1.0.2: call-bind "^1.0.8" get-intrinsic "^1.2.5" +call-bound@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" + call-me-maybe@^1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz" @@ -4487,7 +4500,7 @@ chrome-trace-event@^1.0.2: resolved "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz" integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== -ci-info@^3.2.0: +ci-info@^3.2.0, ci-info@^3.7.0: version "3.9.0" resolved "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz" integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== @@ -5383,6 +5396,11 @@ dns-packet@^5.2.2: dependencies: "@leichtgewicht/ip-codec" "^2.0.1" +docusaurus-plugin-copy-page-button@^0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/docusaurus-plugin-copy-page-button/-/docusaurus-plugin-copy-page-button-0.5.2.tgz#135d6f1b4e5bd6a93e3252f92e5367c24c3565e4" + integrity sha512-k1zdpHWPSktyxTnKfi5kN4ObDDYXQ7HEJBBL5euSTuQfE+SKryvGeDyt8uDhS0FFNcWjTbYPU+AxNWSgqe2Zfg== + docusaurus-plugin-hotjar@^0.0.2: version "0.0.2" resolved "https://registry.npmjs.org/docusaurus-plugin-hotjar/-/docusaurus-plugin-hotjar-0.0.2.tgz" @@ -6199,6 +6217,13 @@ find-up@^6.3.0: locate-path "^7.1.0" path-exists "^5.0.0" +find-yarn-workspace-root@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz#f47fb8d239c900eb78179aa81b66673eac88f7bd" + integrity sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ== + dependencies: + micromatch "^4.0.2" + flat@^5.0.2: version "5.0.2" resolved "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz" @@ -6289,7 +6314,7 @@ fresh@0.5.2: resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== -fs-extra@^10.1.0: +fs-extra@^10.0.0, fs-extra@^10.1.0: version "10.1.0" resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz" integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== @@ -6382,7 +6407,7 @@ get-intrinsic@^1.2.4, get-intrinsic@^1.2.5: hasown "^2.0.2" math-intrinsics "^1.0.0" -get-intrinsic@^1.2.6: +get-intrinsic@^1.2.6, get-intrinsic@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== @@ -6568,7 +6593,7 @@ graceful-fs@4.2.10: resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: +graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -7493,7 +7518,7 @@ is-unicode-supported@^2.0.0: resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz" integrity sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ== -is-wsl@^2.2.0: +is-wsl@^2.1.1, is-wsl@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz" integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== @@ -7517,6 +7542,11 @@ isarray@0.0.1: resolved "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + isarray@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" @@ -7701,6 +7731,17 @@ json-schema-traverse@^1.0.0: resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz" integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== +json-stable-stringify@^1.0.2: + version "1.3.0" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz#8903cfac42ea1a0f97f35d63a4ce0518f0cc6a70" + integrity sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.4" + isarray "^2.0.5" + jsonify "^0.0.1" + object-keys "^1.1.1" + json-stringify-nice@^1.1.4: version "1.1.4" resolved "https://registry.npmjs.org/json-stringify-nice/-/json-stringify-nice-1.1.4.tgz" @@ -7720,6 +7761,11 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" +jsonify@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.1.tgz#2aa3111dae3d34a0f151c63f3a45d995d9420978" + integrity sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg== + jsonparse@^1.3.1: version "1.3.1" resolved "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz" @@ -7747,6 +7793,13 @@ kind-of@^6.0.0, kind-of@^6.0.2: resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== +klaw-sync@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/klaw-sync/-/klaw-sync-6.0.0.tgz#1fd2cfd56ebb6250181114f0a581167099c2b28c" + integrity sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ== + dependencies: + graceful-fs "^4.1.11" + kleur@^3.0.3: version "3.0.3" resolved "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz" @@ -9259,7 +9312,7 @@ minimatch@^9.0.0, minimatch@^9.0.4, minimatch@^9.0.5: dependencies: brace-expansion "^2.0.1" -minimist@^1.2.0: +minimist@^1.2.0, minimist@^1.2.6: version "1.2.8" resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== @@ -9954,6 +10007,14 @@ open@^10.1.0: is-inside-container "^1.0.0" is-wsl "^3.1.0" +open@^7.4.2: + version "7.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321" + integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q== + dependencies: + is-docker "^2.0.0" + is-wsl "^2.1.1" + open@^8.0.9, open@^8.4.0: version "8.4.2" resolved "https://registry.npmjs.org/open/-/open-8.4.2.tgz" @@ -10265,6 +10326,26 @@ pascal-case@^3.1.2: no-case "^3.0.4" tslib "^2.0.3" +patch-package@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-8.0.1.tgz#79d02f953f711e06d1f8949c8a13e5d3d7ba1a60" + integrity sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw== + dependencies: + "@yarnpkg/lockfile" "^1.1.0" + chalk "^4.1.2" + ci-info "^3.7.0" + cross-spawn "^7.0.3" + find-yarn-workspace-root "^2.0.0" + fs-extra "^10.0.0" + json-stable-stringify "^1.0.2" + klaw-sync "^6.0.0" + minimist "^1.2.6" + open "^7.4.2" + semver "^7.5.3" + slash "^2.0.0" + tmp "^0.2.4" + yaml "^2.2.2" + path-browserify@1.0.1, path-browserify@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz" @@ -12408,6 +12489,11 @@ skin-tone@^2.0.0: dependencies: unicode-emoji-modifier-base "^1.0.0" +slash@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" + integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== + slash@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" @@ -12617,16 +12703,7 @@ stream-http@^3.2.0: readable-stream "^3.6.0" xtend "^4.0.2" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -12684,14 +12761,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -12996,6 +13066,11 @@ tmp@^0.2.3: resolved "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz" integrity sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w== +tmp@^0.2.4: + version "0.2.5" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.5.tgz#b06bcd23f0f3c8357b426891726d16015abfd8f8" + integrity sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow== + to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" @@ -13850,7 +13925,7 @@ wordwrap@^1.0.0: resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -13868,15 +13943,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz" @@ -13983,6 +14049,11 @@ yaml@1.10.2, yaml@^1.10.0, yaml@^1.7.2: resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== +yaml@^2.2.2: + version "2.9.0" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.9.0.tgz#78274afd93598a1dfdd6130df6a566defcbf9aa4" + integrity sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA== + yaml@^2.3.4: version "2.6.1" resolved "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz" From 2f4d1d7524756845d51b0ee97e156062261789de Mon Sep 17 00:00:00 2001 From: Shaheen Date: Mon, 25 May 2026 18:46:52 +0530 Subject: [PATCH 2/4] docs(sidebar): place Build with AI after Deployment in v2.0 nav Reorder Rollups v2.0 sidebar so Deployment precedes Build with AI. Co-authored-by: Cursor --- .../version-2.0-sidebars.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/cartesi-rollups_versioned_sidebars/version-2.0-sidebars.json b/cartesi-rollups_versioned_sidebars/version-2.0-sidebars.json index 0e5bf915..ab060dd9 100644 --- a/cartesi-rollups_versioned_sidebars/version-2.0-sidebars.json +++ b/cartesi-rollups_versioned_sidebars/version-2.0-sidebars.json @@ -162,23 +162,23 @@ }, { "type": "category", - "label": "Build with AI", + "label": "Deployment", "collapsed": true, "items": [ - "build-with-ai/overview", - "build-with-ai/mcp-server", - "build-with-ai/skills", - "build-with-ai/prompting" + "deployment/introduction", + "deployment/snapshot", + "deployment/self-hosted" ] }, { "type": "category", - "label": "Deployment", + "label": "Build with AI", "collapsed": true, "items": [ - "deployment/introduction", - "deployment/snapshot", - "deployment/self-hosted" + "build-with-ai/overview", + "build-with-ai/mcp-server", + "build-with-ai/skills", + "build-with-ai/prompting" ] }, { From cbf584ee148fb1a51d9b21248709e5ea8b7020fb Mon Sep 17 00:00:00 2001 From: Shaheen Date: Mon, 25 May 2026 20:14:46 +0530 Subject: [PATCH 3/4] docs(prompting): add spec-driven development prompt pattern Add a multi-step voting-app template: SPEC.md with user stories, PLAN.md and tests, backend implementation, then local verification commands. Co-authored-by: Cursor --- .../version-2.0/build-with-ai/prompting.md | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/cartesi-rollups_versioned_docs/version-2.0/build-with-ai/prompting.md b/cartesi-rollups_versioned_docs/version-2.0/build-with-ai/prompting.md index e4f37993..6818bda2 100644 --- a/cartesi-rollups_versioned_docs/version-2.0/build-with-ai/prompting.md +++ b/cartesi-rollups_versioned_docs/version-2.0/build-with-ai/prompting.md @@ -83,6 +83,64 @@ What to do instead: ## Prompt patterns +### Spec-driven development + +Use this pattern when you want requirements locked before code: explicit user stories, tests that trace to those stories, a Cartesi Rollups v2 backend implementation, then a verification step where you run the tests locally. This avoids agents inventing APIs, skipping validation, or getting lost during vibe coding. + +```text +Drive this work spec-first for a Cartesi Rollups v2 app called "voting-app". + +Use cartesi-scaffold, cartesi-backend-core, cartesi-backend-js-ts, and +cartesi-local-dev. Scope: Cartesi machine backend only (advance/inspect, +validation, state). No frontend unless I ask later. + +Step 1 — Spec and user stories: +- Read my requirements below and produce SPEC.md with: + - User stories (required), each with ID US-001, US-002, ... + Format: "As a , I want , so that ." + Add acceptance criteria (Given / When / Then) per story. + - Map each story to advance actions, inspect routes, or reject paths. + - Actors, inputs, and state transitions. + - Payload schemas for every advance input and inspect route. + - Invariants (one vote per address per proposal, no votes after deadline). + - Error cases and how each is rejected. +- Stop after SPEC.md and wait for my approval. + +Step 2 — Plan and test scripts: +- After I approve SPEC.md, generate PLAN.md (folder layout, modules, data flow). +- Add a failing test suite that covers every user story in SPEC.md: + - Name tests after story IDs (e.g. US-003_reject_vote_after_deadline). + - Cover happy path, validation failures, and inspect responses per story. + - Use the project's test runner (e.g. npm test / pytest) for handler and + validation logic; document any Cartesi CLI smoke checks separately. +- Do not implement production handlers yet. + +Step 3 — Cartesi backend implementation: +- Only after I approve PLAN.md, implement the Rollups v2 backend to make tests + pass: advance_state, inspect handlers, validation/, serialization as needed. +- Deliver backend code plus the test files from Step 2 (updated to pass). +- Keep changes scoped to SPEC.md and user story IDs. Flag gaps; do not guess. +- Do not run tests or cartesi commands in this step. + +Step 4 — Verify (I run locally): +- Print exact commands for me to run, in order: + 1) Install deps (if needed) + 2) Run the full test suite + 3) cartesi build and cartesi run (if applicable) + 4) Sample cartesi send / inspect commands to manually confirm one story +- Summarize expected outcomes per user story ID after each command group. + +Constraints: +- Cartesi Rollups v2 only. Pin alpha packages explicitly. +- Every test and code change must trace to a user story ID in SPEC.md. +- Print CLI commands for me to run; do not execute them. + +Requirements: +- Proposals with a title, options, and a deadline. +- advance_state actions: create_proposal, cast_vote, close_proposal. +- inspect routes: /proposals, /proposals/:id, /results/:id. +``` + ### Scaffold a new app ```text From 74d6d5eab385adf9ae46aef8db6c87138cb84587 Mon Sep 17 00:00:00 2001 From: Shaheen Date: Tue, 26 May 2026 21:07:58 +0530 Subject: [PATCH 4/4] docs(overview): add Codex, Claude Desktop, and VS Code static docs tabs Extend llms-full.txt setup guide with per-client instructions for Codex (AGENTS.md), Claude Desktop (Project knowledge), and VS Code Copilot. Co-authored-by: Cursor --- .../version-2.0/build-with-ai/overview.md | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/cartesi-rollups_versioned_docs/version-2.0/build-with-ai/overview.md b/cartesi-rollups_versioned_docs/version-2.0/build-with-ai/overview.md index 237ecad6..947d31a9 100644 --- a/cartesi-rollups_versioned_docs/version-2.0/build-with-ai/overview.md +++ b/cartesi-rollups_versioned_docs/version-2.0/build-with-ai/overview.md @@ -48,6 +48,9 @@ Download or reference: [https://docs.cartesi.io/llms-full.txt](https://docs.cart @@ -74,6 +77,80 @@ For ongoing work, add the Cartesi MCP server to your project's `.mcp.json` (see + + +[Codex CLI](https://github.com/openai/codex) loads an `AGENTS.md` file from your project root into context for every session. + +1. Download the static documentation file to your project: + + ```shell + curl -fSL https://docs.cartesi.io/llms-full.txt -o docs/cartesi-llms-full.txt + ``` + +2. Add a pointer in `AGENTS.md` at the repo root so Codex grounds answers in Cartesi docs: + + ```md + # Cartesi context + + This project targets **Cartesi Rollups v2**. When answering questions or + generating code: + + - Read `docs/cartesi-llms-full.txt` for the full Cartesi documentation corpus. + - Default to `/cartesi-rollups/2.0/` routes; do not surface v1.x APIs unless asked. + - Prefer fetching individual pages from `https://docs.cartesi.io/.md` when + you need fresh, focused context. + ``` + +3. In a session, reference the file directly with `@docs/cartesi-llms-full.txt` when you want the assistant to ground a specific answer in docs. + +For live tooling, also connect the [MCP server](./mcp-server.mdx). + + + + + +[Claude Desktop](https://claude.ai/download) supports **Projects** with persistent knowledge files. + +1. Download the static documentation file: [https://docs.cartesi.io/llms-full.txt](https://docs.cartesi.io/llms-full.txt) +2. In Claude Desktop, create a new **Project** (for example, "Cartesi Rollups v2"). +3. Open **Project knowledge** and upload `llms-full.txt` (rename to `cartesi-llms-full.txt` if you keep multiple sources). +4. Add a short **Project instructions** entry such as: + + > Default to Cartesi Rollups v2.0. Use the attached `cartesi-llms-full.txt` as the source of truth. Do not surface v1.x APIs unless explicitly asked. + +5. Start a new chat inside the project; Claude will ground answers in the uploaded docs. + +For live tools (CLI commands, skills, repo search), connect the [MCP server](./mcp-server.mdx) in Claude Desktop's MCP config. + + + + + +[GitHub Copilot in VS Code](https://code.visualstudio.com/docs/copilot/overview) reads custom instructions from `.github/copilot-instructions.md` and lets you attach files to chat with `#file:`. + +1. Download the static documentation file into your repo: + + ```shell + curl -fSL https://docs.cartesi.io/llms-full.txt -o docs/cartesi-llms-full.txt + ``` + +2. Create `.github/copilot-instructions.md` (or extend the existing one) with a Cartesi grounding block: + + ```md + This repository builds on **Cartesi Rollups v2**. + + - Use `docs/cartesi-llms-full.txt` as the source of truth for Cartesi APIs, + CLI commands, and deployment. + - Default to `/cartesi-rollups/2.0/` routes; ignore v1.x guidance unless asked. + - When generating CLI steps, prepare commands for the user to run locally. + ``` + +3. In Copilot Chat, attach the file on demand with `#file:docs/cartesi-llms-full.txt` when you want the assistant to cite specific Cartesi docs. + +For live, version-aware tooling, also connect the [MCP server](./mcp-server.mdx) via VS Code's MCP support. + + + Alternatively, you can use the [Cartesi MCP server](./mcp-server.mdx) to get the latest documentation and skills.