From 9cd3f87dd63bff27acbe3966e90f3b689e898b93 Mon Sep 17 00:00:00 2001 From: t Date: Sat, 11 Jul 2026 14:06:25 -0600 Subject: Import Pantograph website project Add the standalone Pantograph documentation site, including its assets, shared styling, and bundled design-system artifacts. --- pantograph/assets/logo-mark-mono.svg | 10 + pantograph/assets/logo-mark.svg | 15 + pantograph/cli.html | 32 + pantograph/configuration.html | 55 + .../_adherence.oxlintrc.json | 425 +++++ .../_ds_bundle.js | 1878 ++++++++++++++++++++ .../_ds_manifest.json | 1 + .../readme.md | 144 ++ .../styles.css | 13 + .../tokens/base.css | 113 ++ .../tokens/colors.css | 87 + .../tokens/fonts.css | 19 + .../tokens/spacing.css | 72 + .../tokens/typography.css | 66 + pantograph/index.html | 37 + pantograph/keybindings.html | 31 + pantograph/lua.html | 74 + pantograph/shared/components.css | 160 ++ pantograph/shared/docs.css | 202 +++ pantograph/shared/docs.js | 85 + 20 files changed, 3519 insertions(+) create mode 100644 pantograph/assets/logo-mark-mono.svg create mode 100644 pantograph/assets/logo-mark.svg create mode 100644 pantograph/cli.html create mode 100644 pantograph/configuration.html create mode 100644 pantograph/ds/pantograph-design-system-56a1c74e-41c1-486b-ba3c-4eabdfacef31/_adherence.oxlintrc.json create mode 100644 pantograph/ds/pantograph-design-system-56a1c74e-41c1-486b-ba3c-4eabdfacef31/_ds_bundle.js create mode 100644 pantograph/ds/pantograph-design-system-56a1c74e-41c1-486b-ba3c-4eabdfacef31/_ds_manifest.json create mode 100644 pantograph/ds/pantograph-design-system-56a1c74e-41c1-486b-ba3c-4eabdfacef31/readme.md create mode 100644 pantograph/ds/pantograph-design-system-56a1c74e-41c1-486b-ba3c-4eabdfacef31/styles.css create mode 100644 pantograph/ds/pantograph-design-system-56a1c74e-41c1-486b-ba3c-4eabdfacef31/tokens/base.css create mode 100644 pantograph/ds/pantograph-design-system-56a1c74e-41c1-486b-ba3c-4eabdfacef31/tokens/colors.css create mode 100644 pantograph/ds/pantograph-design-system-56a1c74e-41c1-486b-ba3c-4eabdfacef31/tokens/fonts.css create mode 100644 pantograph/ds/pantograph-design-system-56a1c74e-41c1-486b-ba3c-4eabdfacef31/tokens/spacing.css create mode 100644 pantograph/ds/pantograph-design-system-56a1c74e-41c1-486b-ba3c-4eabdfacef31/tokens/typography.css create mode 100644 pantograph/index.html create mode 100644 pantograph/keybindings.html create mode 100644 pantograph/lua.html create mode 100644 pantograph/shared/components.css create mode 100644 pantograph/shared/docs.css create mode 100644 pantograph/shared/docs.js (limited to 'pantograph') diff --git a/pantograph/assets/logo-mark-mono.svg b/pantograph/assets/logo-mark-mono.svg new file mode 100644 index 0000000..ed9b65d --- /dev/null +++ b/pantograph/assets/logo-mark-mono.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/pantograph/assets/logo-mark.svg b/pantograph/assets/logo-mark.svg new file mode 100644 index 0000000..f577b74 --- /dev/null +++ b/pantograph/assets/logo-mark.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pantograph/cli.html b/pantograph/cli.html new file mode 100644 index 0000000..88bd226 --- /dev/null +++ b/pantograph/cli.html @@ -0,0 +1,32 @@ + + + + + +panto — Command line + + + + + +
+
+
+ +panto/docs +CLI +
+ + +
+
+ +
+

Reference

Command line

panto dispatches on its first argument. Some names are true subcommands (sessions, models, auth, bootstrap, update, lua, help); the flag forms start the agent itself, either interactively or in one-shot print mode.

CommandDoes
pantoStart a new interactive conversation in the current directory.
panto --resume [<id>]Resume the most recent session, or a specific session by id prefix.
panto -cAlias for bare --resume.
panto -p [prompt]Run one non-interactive turn, print assistant text, exit 0/1.
panto sessionsList saved sessions for this directory.
panto models syncFetch models.dev and rebuild the base models.toml.
panto auth …Show auth status, log in, or log out.
panto bootstrap [--force]Prepare the bundled Lua / luarocks runtime and exit.
panto updateInstall or refresh every rock listed in extensions.rocks.
panto lua [args…]Run the embedded Lua interpreter.
panto --versionPrint the panto version.

Starting a session

pantoStart a fresh interactive conversation.
panto --resume [<id>]Resume the latest or a specific saved session.
panto -p, --print [<prompt>]Run one turn without the TUI.

Interactive mode requires a tty on stdin and stdout. Print mode does not: the prompt can come from the flag argument or from piped stdin, and the command prints only the assistant text before exiting.

-c, --continue
Alias for bare --resume.
-m, --model <provider:alias>
Override the model for this run. A bare alias also works when it is unique across providers.
--effort <level>
Override the reasoning level for this run. This beats both models.toml knobs and [defaults] reasoning.
--no-extensions
Skip the Lua runtime entirely. The core still starts, but because the shipped tools are extensions the agent runs with no tools.

Unknown arguments are errors now

Agent-mode flag parsing is strict. An unrecognized argument prints an error and exits instead of being ignored.

panto sessions

panto sessionsList sessions for the current directory.

Sessions are stored per working directory. The listing is newest-modified first and prints the shortest unambiguous id prefix, the modified timestamp, message count, model, and last user message:

panto · sessions
$ panto sessions
ID MODIFIED MSGS MODEL LAST MESSAGE
0197c2a4 2026-07-01 14:32 18 sonnet add json mode to stats
0197bfe1 2026-06-30 09:11 6 gpt-5 clean up the parser errors
~/.local/share/panto/sessions/--Users-travis-Code-ledger--

If there are no sessions yet, panto prints no sessions for <cwd> and then the directory it would use.

panto models

panto models syncFetch models.dev and rebuild the base models.toml.

The sync command looks at the providers that resolved from your layered config, maps each one to a models.dev catalog entry, optionally intersects that with the provider's own live /models listing, and writes the result to the base-layer models.toml at $XDG_DATA_HOME/panto/models.toml (or ~/.local/share/panto/models.toml).

panto · models sync
$ panto models sync
synced 42 model(s) across 3 provider(s) to ~/.local/share/panto/models.toml

Your user and project models.toml layers still sit on top and win.

panto auth

With no action, panto auth defaults to status.

panto auth status
Show every configured auth session. API-key sessions report resolved or unresolved (key/env missing); OAuth sessions report whether they are logged in and when the current access token expires.
panto auth login <name>
Run the OAuth device flow for the named session and persist the resulting token set.
panto auth logout <name>
Delete the stored token set for the named session.

panto bootstrap

panto bootstrap [--force]Run the runtime bootstrap pipeline, then exit.

Bootstrap stages the base agent tree, creates the generated base config as needed, and prepares the versioned luarocks tree under the data home. The normal agent startup path runs the same bootstrap automatically.

panto updateInstall or refresh the rocks listed in extensions.rocks.

panto update is the explicit “go hit luarocks again” command. Startup only installs missing rocks; changing a version pin or forcing a re-resolve is what update is for.

panto lua

panto lua [args…]Run the embedded Lua interpreter with panto's runtime wired in.

This is the same embedded Lua build panto uses internally, with package paths and luarocks configured the same way a session sees them. It is handy both for experiments and for driving luarocks manually:

shellbash
panto lua my-script.lua
+panto lua -e 'arg[0]="luarocks"; require("luarocks.cmd").run_command("install","lpeg")'

Slash commands

Lines beginning with / are handled locally inside the TUI and never sent to the model.

Built in
/help, /quit, /model, /reasoning, /new, /resume, /status, and /compact.
Extension-defined
Lua extensions can register more slash commands with panto.ext.register_command.

Environment

VariableEffect
ANTHROPIC_API_KEYConsumed by the default Anthropic provider.
OPENAI_API_KEYConsumed by the default OpenAI provider.
PANTO_DEBUGWhen non-zero, route std.log output to <data home>/debug/<session>.log instead of the terminal.
PANTO_SESSION_DIROverride the base sessions directory. panto still appends one encoded subdirectory per working directory.
XDG_CONFIG_HOMELocates the user config and user extensions directories.
XDG_DATA_HOMELocates the data home: base config, base models, sessions, auth cache, agent tree, and rocks.
+ +
+
+ + + diff --git a/pantograph/configuration.html b/pantograph/configuration.html new file mode 100644 index 0000000..27f631a --- /dev/null +++ b/pantograph/configuration.html @@ -0,0 +1,55 @@ + + + + + +panto — Configuration + + + + + +
+
+
+ +panto/docs +CLI +
+ + +
+
+ +
+

Reference

Configuration

TOML

panto reads layered config.toml files for providers, auth, defaults, TUI settings, and extension policy. Model aliases and pricing live beside them in a separate layered models.toml.

Files & layers

Four config.toml layers are merged, lowest precedence first. Missing files are skipped.

LayerPathRole
base$XDG_DATA_HOME/panto/config.toml · else ~/.local/share/panto/config.tomlAuto-generated defaults.
user$XDG_CONFIG_HOME/panto/config.toml · else ~/.config/panto/config.tomlMachine-wide personal settings.
project./.panto/config.tomlChecked-in project defaults.
local./.panto/config.local.tomlPersonal per-project overrides.

Merge rule of thumb: tables merge recursively; scalars and arrays from a higher layer replace lower-layer values. One important exception is [extensions]: its allow/deny rules and source lists accumulate across layers so policy and extra extension sources can be refined instead of erased.

config.tomltoml
[defaults]
+model = "anthropic:sonnet"
+reasoning = "high"
+
+[tui]
+editor = "code -w"
+tools_collapsed = false
+
+[providers.anthropic]
+style    = "anthropic_messages"
+base_url = "https://api.anthropic.com"
+auth     = "anthropic_api"
+
+[auth.anthropic_api]
+key = "${env:ANTHROPIC_API_KEY}"

[providers.<name>]

A provider describes transport only: API shape, base URL, auth session, and a few request-level knobs. The provider name is the left half of a provider:alias model reference.

stylestringrequired
One of anthropic_messages, openai_chat, or openai_responses.
dialectstring
For openai_responses, the only current dialect is "codex".
base_urlstringrequired
API base URL.
authstringrequired
Names the [auth.<name>] session used for this provider.
prompt_cachebool
Anthropic only. Controls the advancing cache_control breakpoint.
extra_headerstable
Static request headers merged onto every request.
model_catalog_namestring
Optional models.dev provider key for panto models sync.
config.tomltoml
[providers.copilot]
+style    = "openai_chat"
+base_url = "https://api.individual.githubcopilot.com"
+auth     = "github_copilot"
+
+[providers.copilot.extra_headers]
+Copilot-Integration-Id = "vscode-chat"
+X-Initiator            = "user"

[auth.<name>]

Auth sessions are named, reusable credential sources. If type is omitted, panto infers it from the fields present: key implies api_key; client_id implies oauth_device.

Substitution

Every auth value supports ${...} substitution. ${env:VAR} reads the environment; ${name} reads another key in the same auth block. Unresolved substitutions become the empty string.

API-key sessions

keystringrequired
Usually "${env:VAR}". Resolved eagerly at load time.

OAuth device sessions

client_id / device_code_url / token_urlstringrequired
Core device-flow endpoints.
dialecttoken | codex
codex additionally requires device_poll_url.
scope / verification_url / redirect_uristring
Optional flow details.
exchange_…string
Optional secondary token exchange after device login.

[defaults]

modelstring
Default model as "<provider>:<alias>".
reasoningstring
Session-default reasoning label — the same names the Ctrl+R picker shows.

Model selection falls through in this order: --model, then [defaults] model, then the single-provider/single-alias convenience case. Reasoning falls through similarly: --effort beats a per-alias setting in models.toml, which beats [defaults] reasoning.

[tui]

editorstring
Overrides $VISUAL / $EDITOR for Ctrl+G.
tools_collapsedbool
Initial state of the global tool-output collapse toggle.

Extensions policy & sources

[extensions] governs every Lua-authored capability, including the shipped standard tools. The names you match are extension entry names such as std.read, std.write, std.edit, and std.shell.

allow / denystring[]
Glob rules over entry names. The default is allow; later, more specific rules win, and deny wins an exact tie.
pathsstring[]
Extra directories to scan for extension sources.
rocksstring[]
Installed rock specs to load as extension sources. panto update installs or refreshes them.
config.tomltoml
[extensions]
+deny  = ["std.shell"]
+paths = ["./dev/panto-ext"]
+rocks = ["panto-agent 1.4.2-1"]

[compaction]

keep_verbatimint
Token budget for the recent tail kept verbatim during compaction.
modelstring
Optional provider:alias override for the compaction model.

models.toml

models.toml is layered just like config.toml: base at $XDG_DATA_HOME/panto/models.toml, then user, project, and ./.panto/models.local.toml. Each entry is keyed [<provider>.<alias>]. If an alias is missing entirely, panto simply uses the alias itself as the wire model name.

Common keys

modelstring
Wire model id sent to the provider API. Defaults to the alias.
context_window / max_tokensint
Context metadata for the UI and a per-request output cap.
input / output / cache_read / cache_writefloat
Pricing in USD per million tokens.

OpenAI-style knobs

reasoningstring
Applies to openai_chat and openai_responses: default, off, minimal, low, medium, or high.

Anthropic-style knobs

thinkingstring
disabled, enabled, or adaptive.
effortstring
low, medium, high, xhigh, or max.
thinking_budget_tokens / thinking_interleaved / api_versionint | bool | string
Manual-thinking budget, interleaved-thinking beta header, and Anthropic API version.

System prompt

Prompt files are layered beside the extension tree: base under $XDG_DATA_HOME/panto/agent/, then user, then project. The conventional files are SYSTEM.md, APPEND_SYSTEM.md, and COMPACTION.md.

+ +
+
+ + + diff --git a/pantograph/ds/pantograph-design-system-56a1c74e-41c1-486b-ba3c-4eabdfacef31/_adherence.oxlintrc.json b/pantograph/ds/pantograph-design-system-56a1c74e-41c1-486b-ba3c-4eabdfacef31/_adherence.oxlintrc.json new file mode 100644 index 0000000..d745f07 --- /dev/null +++ b/pantograph/ds/pantograph-design-system-56a1c74e-41c1-486b-ba3c-4eabdfacef31/_adherence.oxlintrc.json @@ -0,0 +1,425 @@ +{ + "plugins": [ + "react", + "import" + ], + "rules": { + "react/forbid-elements": [ + "warn", + { + "forbid": [] + } + ], + "no-restricted-imports": [ + "warn", + { + "patterns": [ + { + "group": [ + "components/code/**", + "components/core/**", + "components/forms/**", + "components/navigation/**", + "ui_kits/docs/**", + "ui_kits/home/**" + ], + "message": "Import design-system components from 'index.js', not component internals." + } + ] + } + ], + "no-restricted-syntax": [ + "warn", + { + "selector": "Literal[value=/#[0-9a-fA-F]{3,8}\\b/]", + "message": "Raw hex color — use a design-system color token via var()." + }, + { + "selector": "Literal[value=/\\b\\d+px\\b/]", + "message": "Raw px value — use a design-system spacing token via var()." + }, + { + "selector": "Literal[value=/font-family\\s*:\\s*(?!['\\\"]?(?:IBM Plex Mono|Saira Condensed))/i]", + "message": "Font not provided by the design system. Available: IBM Plex Mono, Saira Condensed." + }, + { + "selector": "JSXOpeningElement[name.name='Badge'] > JSXAttribute > JSXIdentifier[name!=/^(?:variant|dot|children|key|ref|className|style|children)$/]", + "message": " doesn't accept that prop. Declared props: variant, dot, children." + }, + { + "selector": "JSXOpeningElement[name.name='Badge'] > JSXAttribute[name.name='variant'] > Literal[value!=/^(?:neutral|danger|success|ink|outline)$/]", + "message": " variant must be one of 'neutral' | 'danger' | 'success' | 'ink' | 'outline'." + }, + { + "selector": "JSXOpeningElement[name.name='Button'] > JSXAttribute > JSXIdentifier[name!=/^(?:variant|size|block|icon|iconRight|as|children|key|ref|className|style|children)$/]", + "message": " +panto/docs +CLI + + + + +
+ +
+

Getting started

panto

terminal agentziglua

panto is a terminal coding agent with a small Zig core and a Lua extension surface. Run it interactively in a project directory, or use -p/--print for one-shot non-interactive turns. These pages cover the current CLI, configuration layers, key bindings, and extension API.

Overview

The default entry point is just panto: it opens the TUI in the current directory, selects a model from your layered config, and lets the agent work through tools. The shipped file tools are themselves Lua extensions, so the same loader that powers your own add-ons also powers std.read, std.write, std.edit, and std.shell.

The first argument also dispatches subcommands. panto sessions, panto auth, panto models sync, panto bootstrap, panto update, and panto lua all short-circuit before the chat loop; panto -p runs a single non-interactive turn and exits.

Install

panto builds from source with Zig. Clone the repository and build the binary:

shellbash
git clone https://code.tjp.lol/pantograph.git
+cd pantograph
+zig build            # native binary at zig-out/bin/panto

The first real run bootstraps panto's bundled Lua runtime automatically, so most users can go straight to panto. The explicit bootstrap command is still useful for CI, fresh-machine setup, or forcing the rocks tree to exist before you start a session:

shellbash
panto bootstrap
+# or: panto bootstrap --force

Where things live

The data home is $XDG_DATA_HOME/panto (or ~/.local/share/panto when XDG_DATA_HOME is unset). User config lives under $XDG_CONFIG_HOME/panto (or ~/.config/panto). Project-local overrides live in ./.panto/.

First run

Start an interactive session with no arguments:

shellbash
cd ~/code/ledger
+panto

Useful first-day flags:

panto --resume
Reattach the most recent conversation in this directory. --resume <id> resumes a specific session by id prefix; -c / --continue is an alias for bare --resume.
panto -p "prompt"
Run one turn non-interactively, print only the assistant text, and exit. The prompt may also come from piped stdin.
panto -m openai:gpt --effort high
Override the model and reasoning level for just this run.
panto --no-extensions
Skip the Lua runtime entirely. This disables shipped tools too, but it is a useful escape hatch when an extension breaks startup.

The TUI requires an interactive terminal. If you only want a single answer in a script or CI job, use -p instead of trying to pipe the full-screen UI.

Authenticate

Providers draw credentials from named auth sessions in config.toml. API-key sessions usually resolve from the environment:

shellbash
export ANTHROPIC_API_KEY=sk-ant-…
+panto auth status

OAuth device sessions are logged in explicitly once, then cached on disk:

shellbash
panto auth login github_copilot
+panto auth status

If a provider seems missing

An api_key auth session that resolves to the empty string leaves its provider unavailable, so exporting the missing environment variable is often enough to make it appear. OAuth providers stay configured and resolve at use time.
+ +
+ + + + diff --git a/pantograph/keybindings.html b/pantograph/keybindings.html new file mode 100644 index 0000000..3d82675 --- /dev/null +++ b/pantograph/keybindings.html @@ -0,0 +1,31 @@ + + + + + +panto — Key bindings + + + + + +
+
+
+ +panto/docs +CLI +
+ + +
+
+ +
+

Reference

Key bindings

TUI input

The session UI reads raw keystrokes and negotiates richer terminal keyboard protocols at startup. These are the bindings the current TUI recognizes.

Editing & cursor

The input box is multi-line and accepts full UTF-8 text. Pasted text arrives as one literal block via bracketed paste.

KeyAction
any printable keyInsert text at the cursor.
/ Move one character.
Alt+B / Alt+F
Ctrl+ / Ctrl+
Move by word. Terminals vary in which encoding they send; panto accepts the common ones.
Ctrl+A / Ctrl+EJump to start / end of the current line.
Backspace / DeleteDelete backward / forward.
Ctrl+U / Ctrl+WDelete to line start / delete previous word.
pasteInsert the pasted bytes literally; pasted newlines do not auto-submit.

Submit & newline

KeyAction
EnterSubmit the current input.
Shift+EnterInsert a newline when the terminal can report it distinctly.

Whether Shift+Enter works depends on keyboard-protocol support. panto negotiates Kitty first, then falls back to xterm modifyOtherKeys.

Selectors & control

KeyAction
Ctrl+TOpen the model selector.
Ctrl+ROpen the reasoning selector.
Ctrl+OCollapse / expand all tool output.
Ctrl+GEdit the current draft in $EDITOR (or [tui] editor).
EscInterrupt the running turn, close an open selector, or clear the input when idle.
Ctrl+C / Ctrl+DExit cleanly.

Selectors are filterable: type to narrow the list, move with / or Ctrl+N/Ctrl+P, and accept with Enter (or Tab for completion-style pickers).

Selectors are session-local

Choosing a model or reasoning level changes the live session only. Nothing is written back to config.toml or models.toml.

Terminal protocol

panto enables bracketed paste and probes for keyboard protocol support at startup.

Shift + Enter

With the legacy terminal protocol, Enter and Shift+Enter are the same byte. panto pushes the Kitty keyboard protocol and queries the terminal; if that is unavailable it falls back to xterm modifyOtherKeys.

TerminalShift + Enter
Kitty, Ghostty, footSupported via Kitty keyboard protocol.
xterm, tmuxSupported via modifyOtherKeys fallback.
Terminal.app and similarIndistinguishable from Enter, so it submits.

Bracketed paste

Bracketed paste means multi-line pastes are inserted literally instead of being reinterpreted as a sequence of keypresses.

+ +
+
+ + + diff --git a/pantograph/lua.html b/pantograph/lua.html new file mode 100644 index 0000000..ace492f --- /dev/null +++ b/pantograph/lua.html @@ -0,0 +1,74 @@ + + + + + +panto — Lua extensions + + + + + +
+
+
+ +panto/docs +CLI +
+ + +
+
+ +
+

Reference

Lua extensions

require("panto")

panto's standard tools are Lua extensions, and your own extensions use the same loader. A source file returns one entry, one tool declaration, or a list of them; activation then registers tools, slash commands, and UI-event handlers into the live session.

Overview

Extension source files are evaluated first and activated later. A file must return one of these shapes:

{ name, activate }
A named entry whose activate() function performs registration when the entry survives policy and shadowing.
{ name, description, schema, handler }
Sugar form for a single tool entry. Activation registers it as a tool automatically.
{ ... , ... }
A list of entries or tool declarations returned from one file.

Inside activate(), use local panto = require("panto") and the panto.ext table:

register_tool
Register a model-facing tool.
register_command
Register a slash command.
on / emit
Subscribe to transcript/UI lifecycle events or fire custom ones.

Loading extensions

panto scans both extensions/ and tools/ directories at each layer:

LayerLocation
base$XDG_DATA_HOME/panto/agent/{extensions,tools}
user$XDG_CONFIG_HOME/panto/{extensions,tools} · else ~/.config/panto/{extensions,tools}
project./.panto/{extensions,tools}

Additional sources can come from [extensions] paths and [extensions] rocks. Cross-layer precedence is project > user > base. Within one layer, canonical directories beat extra paths, which beat rocks. Identity is the returned entry's name, not the filename.

Eval is side-effect free

Every candidate file is evaluated before policy and shadowing are resolved, including entries that will later be denied. Put side effects in activate(), not at top level.

register_tool

The simplest way to define a tool is to return the sugar form directly:

.panto/tools/echo.lualua
return {
+  name = "echo",
+  description = "Echo back the given message.",
+  schema = {
+    type = "object",
+    properties = {
+      message = { type = "string" },
+    },
+    required = { "message" },
+  },
+  handler = function(input)
+    return "echo: " .. input.message
+  end,
+}

Use register_tool inside activate() when you want one entry to register multiple tools or commands together.

register_command

Slash commands are typically registered from an explicit entry:

.panto/extensions/greet.lualua
return {
+  name = "greet",
+  activate = function()
+    local panto = require("panto")
+    panto.ext.register_command {
+      name = "greet",
+      description = "Print a greeting.",
+      handler = function(args)
+        local who = args ~= "" and args or "world"
+        io.write("\n[greet] hello, " .. who .. "!\n")
+      end,
+    }
+  end,
+}

Events: on & emit

panto.ext.on(name, handler) subscribes to transcript/UI lifecycle events. The handler receives an event userdata with snake-case methods get_component() and set_component(), plus payload fields such as tool_name, delta, text, id, input, output, cwd, and model.

Event familyNames
tooltool, tool_details, tool_delta, tool_call_complete, tool_result, tool_collapse
thinkingthinking, thinking_delta, thinking_complete
assistant textassistant_text, assistant_text_delta, assistant_text_complete
single-shotuser_message, session_start, compaction
customwhatever you emit via panto.ext.emit

A few payload fields are writable during dispatch: user_message.text, tool_call_complete.input (assign a Lua table; panto serializes it to JSON), and tool_result.output.

.panto/extensions/skill-badge.lualua
return {
+  name = "skill-badge",
+  activate = function()
+    local panto = require("panto")
+    panto.ext.on("tool_details", function(e)
+      if e.tool_name ~= "skill" then return end
+      local inner = e:get_component()
+      e:set_component({
+        render = function(self, width)
+          local lines = inner:render(width)
+          lines[#lines + 1] = "▸ running skill"
+          return lines
+        end,
+      })
+    end)
+  end,
+}

Lua components

A Lua component is a table passed to set_component(). The required method is render(self, width), which returns an array of strings. handleInput(self, data) is optional. Differential repaint is cache-derived automatically; you do not need to implement firstLineChanged yourself.

Runtime & luarocks

panto embeds a pinned Lua and luarocks runtime and ships luv as its core async dependency. Extra rocks may be loaded as extension sources via [extensions] rocks and installed with panto update, or added manually with panto lua.

shellbash
panto update
+panto lua -e 'arg[0]="luarocks"; require("luarocks.cmd").run_command("install","lpeg")'
+ +
+
+ + + diff --git a/pantograph/shared/components.css b/pantograph/shared/components.css new file mode 100644 index 0000000..2b50d99 --- /dev/null +++ b/pantograph/shared/components.css @@ -0,0 +1,160 @@ +/* ============================================================ + panto docs — static component styles. + Extracted from the Pantograph design system's React components + (Badge, Tag, Terminal, CodeBlock, Callout) so the docs render + identically as plain HTML, with no JavaScript and no React. + These mirror the styles those components inject at runtime. + ============================================================ */ + +/* ---- Badge ----------------------------------------------------------- */ +.pg-badge { + display: inline-flex; align-items: center; gap: 5px; + font-family: var(--font-display); font-weight: var(--weight-semibold); + font-size: var(--text-2xs); letter-spacing: var(--tracking-label); + text-transform: uppercase; line-height: 1; white-space: nowrap; + padding: 4px 7px; border-radius: var(--radius-xs); + border: var(--border-thin) solid transparent; +} +.pg-badge__dot { width: 6px; height: 6px; border-radius: var(--radius-pill); background: currentColor; } +.pg-badge--neutral { color: var(--ink-2); background: var(--paper-2); border-color: var(--paper-3); } +.pg-badge--danger { color: var(--red-deep); background: var(--red-wash); border-color: var(--red-soft); } +.pg-badge--success { color: var(--green-deep); background: var(--green-wash); border-color: var(--green-soft); } +.pg-badge--ink { color: var(--paper-0); background: var(--ink-1); border-color: var(--ink-1); } +.pg-badge--outline { color: var(--ink-2); background: transparent; border-color: var(--border-default); } + +/* ---- Tag ------------------------------------------------------------- */ +.pg-tag { + display: inline-flex; align-items: center; gap: 6px; + font-family: var(--font-mono); font-weight: var(--weight-medium); + font-size: var(--text-xs); line-height: 1; white-space: nowrap; + color: var(--ink-1); background: var(--paper-0); + padding: 5px 9px; border-radius: var(--radius-pill); + border: var(--border-thin) solid var(--border-default); +} +.pg-tag__dot { width: 7px; height: 7px; border-radius: var(--radius-pill); flex: none; background: var(--ink-3); } + +/* ---- Callout (admonition) -------------------------------------------- */ +.pg-callout { + display: flex; gap: 12px; + padding: 13px 15px; + background: var(--paper-0); + border: var(--border-thin) solid var(--border-hairline); + border-left: var(--border-thick) solid var(--ink-3); + border-radius: var(--radius-sm); +} +.pg-callout__icon { flex: none; width: 18px; height: 18px; margin-top: 1px; color: var(--ink-3); } +.pg-callout__icon svg { width: 100%; height: 100%; } +.pg-callout__body { font: var(--weight-regular) var(--text-sm)/1.55 var(--font-sans); color: var(--ink-1); } +.pg-callout__title { + font: var(--weight-semibold) var(--text-2xs)/1 var(--font-display); + letter-spacing: var(--tracking-label); text-transform: uppercase; + margin: 0 0 5px; color: var(--ink-2); +} +.pg-callout__body p { margin: 0 0 var(--space-2); } +.pg-callout__body :last-child { margin-bottom: 0; } +.pg-callout--note { border-left-color: var(--ink-3); } +.pg-callout--tip { border-left-color: var(--green); background: var(--green-wash); } +.pg-callout--tip .pg-callout__icon, .pg-callout--tip .pg-callout__title { color: var(--green-deep); } +.pg-callout--warn { border-left-color: var(--brass); background: #F0E7D0; } +.pg-callout--warn .pg-callout__icon, .pg-callout--warn .pg-callout__title { color: #7A5E20; } +.pg-callout--danger { border-left-color: var(--red); background: var(--red-wash); } +.pg-callout--danger .pg-callout__icon, .pg-callout--danger .pg-callout__title { color: var(--red-deep); } + +/* ---- CodeBlock ------------------------------------------------------- */ +.pg-code { + background: var(--surface-code); + border: var(--border-thin) solid var(--border-default); + border-radius: var(--radius-md); + overflow: hidden; + font-family: var(--font-mono); +} +.pg-code__bar { + display: flex; align-items: center; gap: 10px; + padding: 8px 12px; background: var(--paper-2); + border-bottom: var(--border-thin) solid var(--border-hairline); +} +.pg-code__name { font: var(--weight-medium) var(--text-xs)/1 var(--font-mono); color: var(--ink-2); } +.pg-code__lang { + font: var(--weight-semibold) var(--text-2xs)/1 var(--font-display); + letter-spacing: var(--tracking-label); text-transform: uppercase; color: var(--ink-3); + margin-left: auto; padding-right: 4px; +} +.pg-code__copy { + display: inline-flex; align-items: center; gap: 5px; + font: var(--weight-medium) var(--text-2xs)/1 var(--font-display); + letter-spacing: 0.1em; text-transform: uppercase; + color: var(--ink-3); background: none; border: 0; cursor: pointer; + padding: 4px 6px; border-radius: var(--radius-xs); + transition: color var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out); +} +.pg-code__copy:hover { color: var(--ink-0); background: var(--paper-1); } +.pg-code__copy--done { color: var(--green-deep); } +.pg-code__copy svg { width: 13px; height: 13px; } +.pg-code__copy .pg-code__check { display: none; } +.pg-code__copy--done .pg-code__check { display: inline; } +.pg-code__copy--done .pg-code__copyglyph { display: none; } +.pg-code__scroll { overflow-x: auto; padding: 14px 16px; } +.pg-code pre { font-size: var(--text-sm); line-height: var(--leading-mono); color: var(--ink-1); margin: 0; } +.pg-code code { font-family: inherit; background: none; border: 0; padding: 0; color: inherit; } +/* token colors for hand-marked spans */ +.pg-code .tok-kw { color: var(--green-deep); } +.pg-code .tok-str { color: var(--red-deep); } +.pg-code .tok-com { color: var(--ink-4); font-style: italic; } +.pg-code .tok-fn { color: var(--ink-0); font-weight: 500; } +.pg-code .tok-num { color: var(--brass); } + +/* ---- Terminal -------------------------------------------------------- */ +.pg-term { + background: var(--ink-1); + border: var(--border-thin) solid var(--ink-0); + border-radius: var(--radius-md); + overflow: hidden; + box-shadow: var(--shadow-md); + font-family: var(--font-mono); +} +.pg-term__bar { + display: flex; align-items: center; gap: 8px; + padding: 9px 12px; background: #232B3B; + border-bottom: var(--border-thin) solid #2E3749; +} +.pg-term__dot { width: 9px; height: 9px; border-radius: var(--radius-pill); } +.pg-term__title { + margin-left: 6px; + font: var(--weight-semibold) var(--text-2xs)/1 var(--font-display); + letter-spacing: var(--tracking-label); text-transform: uppercase; color: var(--paper-4); +} +.pg-term__body { padding: 14px 16px; overflow-x: auto; font-size: var(--text-sm); line-height: 1.65; } +.pg-term__line { white-space: pre; color: var(--paper-2); } +.pg-term__line .pg-term__prompt { color: var(--green-soft); user-select: none; } +.pg-term__line .pg-term__panto { color: var(--red-soft); user-select: none; } +.pg-term__line--cmd { color: var(--paper-0); } +.pg-term__line--out { color: var(--paper-4); } +.pg-term__line--ok { color: var(--green-soft); } +.pg-term__line--err { color: var(--red-soft); } +.pg-term__line--com { color: #6E7689; font-style: italic; } +.pg-term__cursor { + display: inline-block; width: 8px; height: 1.05em; vertical-align: text-bottom; + background: var(--red); margin-left: 2px; + animation: pg-term-blink 1.1s steps(1) infinite; +} +@keyframes pg-term-blink { 50% { opacity: 0; } } +@media (prefers-reduced-motion: reduce) { .pg-term__cursor { animation: none; } } +/* panto TUI mock: welcome banner, user block, tool boxes, footer, input frame */ +.pg-term__line--accent { color: var(--red-soft); } +.pg-term__line--dim { color: #6E7689; } +.pg-term__user { + white-space: pre-wrap; color: var(--paper-0); + background: rgba(178,58,46,0.16); padding: 5px 10px; margin: 3px 0; +} +.pg-term__tool { margin: 3px 0; } +.pg-term__tool-head { white-space: pre; color: var(--paper-0); padding-left: 1ch; } +.pg-term__tool-body { + white-space: pre; color: var(--paper-4); + padding: 3px 0 0 calc(1ch + 8px); margin-left: 1ch; + border-left: 2px solid #2E3749; +} +.pg-term__add { color: var(--green-soft); } +.pg-term__del { color: var(--red-soft); } +.pg-term .pg-term__rule { border: 0; border-top: 1px solid #2E3749; margin: 7px 0; } +.pg-term code { background: none; border: 0; padding: 0; color: var(--green-soft); font: inherit; } +.pg-term__user code { color: inherit; } diff --git a/pantograph/shared/docs.css b/pantograph/shared/docs.css new file mode 100644 index 0000000..bfa1e44 --- /dev/null +++ b/pantograph/shared/docs.css @@ -0,0 +1,202 @@ +/* ============================================================ + panto docs — shared documentation chrome. + Built on the Pantograph design system tokens. Class prefix: pd- + ============================================================ */ + +body { margin: 0; background: var(--surface-page); } +html { scroll-behavior: smooth; } +@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } } + +.pd-app { min-height: 100vh; } + +/* ---- Header ---------------------------------------------------------- */ +.pd-header { + position: sticky; top: 0; z-index: 30; + display: flex; align-items: center; gap: var(--space-5); + height: 60px; padding: 0 var(--space-6); + background: color-mix(in srgb, var(--paper-1) 88%, transparent); + backdrop-filter: blur(8px); + border-bottom: var(--border-thin) solid var(--border-hairline); +} +.pd-header__left { display: flex; align-items: center; gap: var(--space-3); } +.pd-menu { display: none; background: none; border: 0; cursor: pointer; color: var(--ink-2); font-size: 20px; padding: 4px; } +.pd-brand { display: flex; align-items: center; gap: 9px; text-decoration: none; } +.pd-brand:hover { text-decoration: none; } +.pd-brand__word { font: 700 20px var(--font-display); letter-spacing: -0.01em; color: var(--ink-0); } +.pd-brand__div { color: var(--paper-4); font: 400 18px var(--font-display); } +.pd-brand__sub { font: 600 18px var(--font-display); color: var(--ink-3); } +.pd-nav { display: flex; gap: 2px; margin-left: var(--space-4); } +.pd-nav__link { + font: 500 var(--text-sm)/1 var(--font-sans); color: var(--ink-3); + padding: 8px 11px; border-radius: var(--radius-xs); text-decoration: none; + transition: color var(--dur-fast), background var(--dur-fast); +} +.pd-nav__link:hover { color: var(--ink-1); background: var(--paper-2); text-decoration: none; } +.pd-nav__link--active { color: var(--ink-0); font-weight: 600; } +.pd-header__right { display: flex; align-items: center; gap: var(--space-3); margin-left: auto; } +.pd-ghlink { display: inline-flex; align-items: center; gap: 7px; font: 500 var(--text-sm) var(--font-mono); color: var(--ink-2); text-decoration: none; } +.pd-ghlink:hover { color: var(--ink-0); text-decoration: none; } +.pd-ghlink svg { font-size: 16px; } + +/* ---- Shell grid ------------------------------------------------------ */ +.pd-shell { + display: grid; + grid-template-columns: var(--sidebar-width) minmax(0, 1fr) 220px; + max-width: 1340px; margin: 0 auto; + align-items: start; +} +.pd-sidebar { + position: sticky; top: 60px; align-self: start; + height: calc(100vh - 60px); overflow: hidden; + border-right: var(--border-thin) solid var(--border-hairline); +} +.pd-sidebar__scroll { height: 100%; overflow-y: auto; padding: var(--space-5) var(--space-4) var(--space-8); } +.pd-navsec { margin-bottom: var(--space-5); } +.pd-navsec__title { + display: flex; align-items: center; gap: 7px; + font: 600 var(--text-2xs) var(--font-display); letter-spacing: var(--tracking-label); + text-transform: uppercase; color: var(--ink-4); margin: 0 0 var(--space-2); padding-left: 10px; +} +.pd-navsec__title svg { font-size: 13px; } +.pd-navsec--current .pd-navsec__title { color: var(--red-deep); } +.pd-navsec__list { list-style: none; margin: 0; padding: 0; } +.pd-navsec__link { + display: block; font: 400 var(--text-sm)/1.2 var(--font-sans); color: var(--ink-2); + text-decoration: none; padding: 7px 10px; border-radius: var(--radius-xs); + border-left: 2px solid transparent; transition: color var(--dur-fast), background var(--dur-fast), border-color var(--dur-fast); +} +.pd-navsec__link:hover { color: var(--ink-0); background: var(--paper-2); text-decoration: none; } +.pd-navsec__link--active { + color: var(--red-deep); font-weight: 600; background: var(--paper-0); + border-left-color: var(--red); +} +.pd-navsec__link code { background: none; border: 0; padding: 0; color: inherit; font-size: 0.92em; } + +/* ---- Main ------------------------------------------------------------ */ +.pd-main { padding: var(--space-6) var(--space-7) var(--space-9); min-width: 0; } +.pd-doc { max-width: 800px; } +.pd-eyebrow { display: block; margin-bottom: var(--space-2); } +.pd-doc__h1 { font: 700 var(--text-3xl) var(--font-display); color: var(--ink-0); letter-spacing: -0.01em; margin: 0 0 var(--space-4); } +.pd-doc__meta { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); margin-bottom: var(--space-5); } +.pd-lead { font: 400 var(--text-md)/1.6 var(--font-serif); color: var(--ink-1); margin: 0 0 var(--space-6); max-width: 66ch; } + +.pd-h2 { + font: 600 var(--text-xl) var(--font-sans); color: var(--ink-0); + margin: var(--space-8) 0 var(--space-4); padding-bottom: var(--space-2); + border-bottom: var(--border-thin) solid var(--border-hairline); + scroll-margin-top: 76px; +} +.pd-h3 { font: 600 var(--text-lg) var(--font-sans); color: var(--ink-0); margin: var(--space-6) 0 var(--space-3); scroll-margin-top: 76px; } +.pd-body { font: 400 var(--text-base)/1.65 var(--font-sans); color: var(--ink-1); max-width: 66ch; } +.pd-body + .pd-body { margin-top: var(--space-4); } +.pd-doc a { text-decoration-thickness: 1px; } +.pd-doc > .pg-callout, .pd-doc .pd-callout { margin: var(--space-5) 0; } +.pd-doc .pg-code, .pd-doc .pg-term { margin: var(--space-4) 0; max-width: 800px; } +.pd-section { margin-bottom: var(--space-2); } + +/* ---- Command signature header --------------------------------------- */ +.pd-cmd { + display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-3); + margin: var(--space-6) 0 var(--space-2); scroll-margin-top: 76px; +} +.pd-cmd__sig { + font: 600 var(--text-md) var(--font-mono); color: var(--ink-0); + background: var(--surface-code); border: var(--border-thin) solid var(--border-default); + border-radius: var(--radius-sm); padding: 5px 10px; +} +.pd-cmd__sig .opt { color: var(--ink-3); } +.pd-cmd__note { font: 400 var(--text-sm) var(--font-sans); color: var(--ink-3); } + +/* ---- Option / parameter list ---------------------------------------- */ +.pd-opts { display: flex; flex-direction: column; gap: 0; margin: var(--space-4) 0; border-top: var(--border-thin) solid var(--border-hairline); } +.pd-opt { + display: grid; grid-template-columns: minmax(160px, 240px) 1fr; gap: var(--space-4); + padding: var(--space-4) var(--space-2); border-bottom: var(--border-thin) solid var(--border-hairline); +} +.pd-opt__key { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; } +.pd-opt__name { font: 500 var(--text-sm) var(--font-mono); color: var(--ink-0); } +.pd-opt__type { font: 400 var(--text-xs) var(--font-mono); color: var(--green-deep); } +.pd-opt__desc { font: 400 var(--text-sm)/1.6 var(--font-sans); color: var(--ink-1); } +.pd-opt__desc code { font-size: 0.9em; } +.pd-opt__desc p { margin: 0 0 var(--space-2); } +.pd-opt__desc p:last-child { margin: 0; } +@media (max-width: 640px) { .pd-opt { grid-template-columns: 1fr; gap: var(--space-2); } } + +/* ---- Table ----------------------------------------------------------- */ +.pd-table-wrap { border: var(--border-thin) solid var(--border-hairline); border-radius: var(--radius-md); overflow: hidden; margin: var(--space-4) 0; } +.pd-table { width: 100%; border-collapse: collapse; font: 400 var(--text-sm) var(--font-sans); } +.pd-table th { + text-align: left; font: 600 var(--text-2xs) var(--font-display); letter-spacing: var(--tracking-label); + text-transform: uppercase; color: var(--ink-3); background: var(--paper-2); + padding: 10px 14px; border-bottom: var(--border-thin) solid var(--border-hairline); +} +.pd-table td { padding: 11px 14px; border-bottom: var(--border-thin) solid var(--paper-2); color: var(--ink-1); vertical-align: top; } +.pd-table tr:last-child td { border-bottom: 0; } +.pd-table tr:nth-child(even) td { background: color-mix(in srgb, var(--paper-2) 40%, transparent); } +.pd-table td code { font-size: 0.9em; } +.pd-table .mono { font-family: var(--font-mono); } +.pd-table .t-type { font: 400 var(--text-xs) var(--font-mono); color: var(--green-deep); } +.pd-table .t-def { font: 400 var(--text-xs) var(--font-mono); color: var(--ink-3); } + +/* ---- Keycaps --------------------------------------------------------- */ +.pd-kbd { + display: inline-flex; align-items: center; gap: 4px; + font: 600 var(--text-xs) var(--font-mono); color: var(--ink-1); + background: var(--paper-0); border: var(--border-thin) solid var(--paper-4); + border-bottom-width: 2px; border-radius: var(--radius-sm); + padding: 3px 7px; white-space: nowrap; +} +.pd-keytable .pd-kbd { font-size: var(--text-sm); } + +/* ---- Inline tick rule ------------------------------------------------ */ +.pd-rule { margin: var(--space-7) 0; } + +/* ---- TOC ------------------------------------------------------------- */ +.pd-toc { position: sticky; top: 60px; padding: var(--space-6) var(--space-4); align-self: start; max-height: calc(100vh - 60px); overflow-y: auto; } +.pd-toc__title { font: 600 var(--text-2xs) var(--font-display); letter-spacing: var(--tracking-label); text-transform: uppercase; color: var(--ink-4); margin: 0 0 var(--space-3); } +.pd-toc__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; } +.pd-toc__link { display: block; font: 400 var(--text-sm)/1.3 var(--font-sans); color: var(--ink-3); text-decoration: none; padding: 5px 10px; border-left: 2px solid var(--paper-3); transition: color var(--dur-fast), border-color var(--dur-fast); } +.pd-toc__link:hover { color: var(--ink-0); border-left-color: var(--ink-3); text-decoration: none; } +.pd-toc__link--active { color: var(--red-deep); border-left-color: var(--red); font-weight: 600; } + +/* ---- Prev / next footer --------------------------------------------- */ +.pd-prevnext { + display: flex; gap: var(--space-4); margin-top: var(--space-8); + padding-top: var(--space-5); border-top: var(--border-thin) solid var(--border-hairline); +} +.pd-pn { + flex: 1; display: flex; flex-direction: column; gap: 4px; text-decoration: none; + padding: var(--space-4); border: var(--border-thin) solid var(--border-hairline); + border-radius: var(--radius-md); background: var(--surface-card); box-shadow: var(--shadow-xs); + transition: box-shadow var(--dur-fast), border-color var(--dur-fast); +} +.pd-pn:hover { box-shadow: var(--shadow-sm); border-color: var(--border-default); text-decoration: none; } +.pd-pn--next { text-align: right; } +.pd-pn__dir { font: 600 var(--text-2xs) var(--font-display); letter-spacing: var(--tracking-label); text-transform: uppercase; color: var(--ink-4); } +.pd-pn__title { font: 600 var(--text-base) var(--font-sans); color: var(--ink-0); } + +/* ---- Footnote / source line ----------------------------------------- */ +.pd-srcnote { font: 400 var(--text-xs)/1.6 var(--font-sans); color: var(--ink-3); margin-top: var(--space-6); } +.pd-srcnote code { font-size: 0.92em; } + +.pd-scrim { display: none; position: fixed; inset: 60px 0 0; z-index: 35; background: rgba(22,27,39,0.28); } + +/* ---- Responsive ------------------------------------------------------ */ +@media (max-width: 1120px) { + .pd-shell { grid-template-columns: var(--sidebar-width) minmax(0, 1fr); } + .pd-toc { display: none; } +} +@media (max-width: 860px) { + .pd-shell { grid-template-columns: minmax(0, 1fr); } + .pd-nav { display: none; } + .pd-menu { display: inline-flex; } + .pd-sidebar { + position: fixed; top: 60px; left: 0; z-index: 40; width: var(--sidebar-width); + background: var(--paper-1); transform: translateX(-105%); + transition: transform var(--dur-base) var(--ease-slide); + } + .pd-sidebar--open { transform: translateX(0); box-shadow: var(--shadow-lg); } + .pd-sidebar--open ~ .pd-scrim { display: block; } + .pd-main { padding: var(--space-5) var(--space-5) var(--space-8); } + .pd-prevnext { flex-direction: column; } +} diff --git a/pantograph/shared/docs.js b/pantograph/shared/docs.js new file mode 100644 index 0000000..7e07310 --- /dev/null +++ b/pantograph/shared/docs.js @@ -0,0 +1,85 @@ +/* panto docs — progressive enhancement only. + The site is fully functional with this file absent or JS disabled: + every nav item is a real link, all content is in the HTML. This adds + the mobile sidebar toggle, code copy buttons, and TOC scroll-spy. */ +(function () { + 'use strict'; + + /* ---- Mobile sidebar toggle ---------------------------------------- */ + var menuBtn = document.querySelector('[data-menu-toggle]'); + var sidebar = document.querySelector('.pd-sidebar'); + var scrim = document.querySelector('.pd-scrim'); + function closeMenu() { + if (sidebar) sidebar.classList.remove('pd-sidebar--open'); + } + if (menuBtn && sidebar) { + menuBtn.addEventListener('click', function () { + sidebar.classList.toggle('pd-sidebar--open'); + }); + } + if (scrim) scrim.addEventListener('click', closeMenu); + if (sidebar) { + sidebar.addEventListener('click', function (e) { + if (e.target.closest('a')) closeMenu(); + }); + } + + /* ---- Code copy buttons -------------------------------------------- */ + document.querySelectorAll('.pg-code__copy').forEach(function (btn) { + btn.addEventListener('click', function () { + var block = btn.closest('.pg-code'); + var pre = block && block.querySelector('pre'); + if (!pre) return; + var text = pre.innerText; + var done = function () { + btn.classList.add('pg-code__copy--done'); + var label = btn.querySelector('.pg-code__label'); + if (label) label.textContent = 'Copied'; + setTimeout(function () { + btn.classList.remove('pg-code__copy--done'); + if (label) label.textContent = 'Copy'; + }, 1400); + }; + if (navigator.clipboard) navigator.clipboard.writeText(text).then(done, done); + else done(); + }); + }); + + /* ---- Scroll-spy: highlight the active section in TOC + sidebar ---- */ + var tocLinks = Array.prototype.slice.call(document.querySelectorAll('.pd-toc__link')); + var sideLinks = Array.prototype.slice.call( + document.querySelectorAll('.pd-navsec--current .pd-navsec__link') + ); + if (!tocLinks.length || !('IntersectionObserver' in window)) return; + + var ids = tocLinks.map(function (a) { + var href = a.getAttribute('href') || ''; + return href.charAt(0) === '#' ? href.slice(1) : href.split('#')[1]; + }).filter(Boolean); + + function setActive(id) { + tocLinks.forEach(function (a) { + var h = a.getAttribute('href') || ''; + a.classList.toggle('pd-toc__link--active', h === '#' + id); + }); + sideLinks.forEach(function (a) { + var h = a.getAttribute('href') || ''; + a.classList.toggle('pd-navsec__link--active', h.split('#')[1] === id); + }); + } + + var visible = new Set(); + var obs = new IntersectionObserver(function (entries) { + entries.forEach(function (e) { + if (e.isIntersecting) visible.add(e.target.id); + else visible.delete(e.target.id); + }); + var first = ids.find(function (id) { return visible.has(id); }); + if (first) setActive(first); + }, { rootMargin: '-72px 0px -68% 0px', threshold: 0 }); + + ids.forEach(function (id) { + var el = document.getElementById(id); + if (el) obs.observe(el); + }); +})(); -- cgit v1.3