datasette/docs/generate-datasette-modal-example.sh
Claude 967c1298ea
Add script to regenerate the datasette-modal docs screenshot
docs/generate-datasette-modal-example.sh builds a temporary demo
database and plugins directory containing the createModal() example
from the Modal dialogs documentation, starts a Datasette server, takes
the screenshot with shot-scraper, quantizes it to an 8-bit palette PNG
and stops the server again. Output is byte-identical to the committed
docs/datasette-modal-example.png.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TShiUYVMmmF4zyJR6GMw34
2026-07-02 16:19:37 +00:00

147 lines
4.3 KiB
Bash
Executable file

#!/bin/bash
# Recreate docs/datasette-modal-example.png
#
# Takes a screenshot of the <datasette-modal> example plugin from the
# "Modal dialogs" section of docs/javascript_plugins.rst, running against
# a temporary Datasette instance that this script starts and stops.
#
# Requirements:
# - datasette importable by $PYTHON (e.g. "pip install -e ." in this repo)
# - uv, for "uvx shot-scraper" and the Pillow-based PNG quantization step
# (shot-scraper needs a Playwright browser: "uvx shot-scraper install")
#
# Environment variable overrides:
# PYTHON Python command (default: python3)
# SHOT_SCRAPER shot-scraper command (default: uvx shot-scraper)
# PORT port for the temporary server (default: 8574)
set -euo pipefail
read -r -a PYTHON_CMD <<< "${PYTHON:-python3}"
read -r -a SHOT_SCRAPER_CMD <<< "${SHOT_SCRAPER:-uvx shot-scraper}"
PORT="${PORT:-8574}"
docs_dir="$(cd "$(dirname "$0")" && pwd)"
output="$docs_dir/datasette-modal-example.png"
tmp_dir=$(mktemp -d)
server_pid=""
cleanup() {
if [ -n "$server_pid" ]; then
kill "$server_pid" 2>/dev/null || true
fi
rm -rf "$tmp_dir"
}
trap cleanup EXIT
# A small demo database for the page shown behind the dialog
"${PYTHON_CMD[@]}" - "$tmp_dir/demo.db" <<'EOF'
import sqlite3
import sys
conn = sqlite3.connect(sys.argv[1])
conn.executescript(
"""
create table plugins (id integer primary key, name text, description text);
insert into plugins (name, description) values
('datasette-cluster-map', 'Renders a map of geographic data'),
('datasette-vega', 'Visualize data with Vega charts'),
('datasette-graphql', 'GraphQL endpoint for Datasette');
"""
)
conn.commit()
EOF
# The example plugin code from the "Modal dialogs" section of
# docs/javascript_plugins.rst, injected via extra_body_script()
mkdir "$tmp_dir/plugins"
cat > "$tmp_dir/plugins/modal_example.py" <<'EOF'
from datasette import hookimpl
@hookimpl
def extra_body_script():
return {
"script": """
document.addEventListener("datasette_init", function (event) {
const manager = event.detail;
const modal = manager.createModal({
id: "my-plugin-dialog",
className: "my-plugin-dialog",
title: "My plugin",
content: `
<p style="padding: 16px 24px">Hello from a plugin!</p>
<div class="modal-footer">
<span class="footer-info"></span>
<button type="button" class="btn btn-ghost my-plugin-cancel">Cancel</button>
<button type="button" class="btn btn-primary my-plugin-save">Save</button>
</div>
`,
});
if (!modal) {
return; // Browser does not support <dialog>
}
modal.dialog
.querySelector(".my-plugin-cancel")
.addEventListener("click", () => modal.requestClose("cancel"));
// Open it later, for example from a button click:
// modal.showModal({trigger: button});
});
"""
}
EOF
"${PYTHON_CMD[@]}" -m datasette "$tmp_dir/demo.db" \
--plugins-dir "$tmp_dir/plugins" --port "$PORT" &
server_pid=$!
# Wait for the server to start responding
for _ in $(seq 1 50); do
if curl -s -o /dev/null "http://127.0.0.1:$PORT/"; then
break
fi
sleep 0.2
done
# Open the modal with animations disabled, blur the auto-focused Cancel
# button so no focus ring appears, then take a retina viewport shot
"${SHOT_SCRAPER_CMD[@]}" shot "http://127.0.0.1:$PORT/demo/plugins" \
--javascript '
new Promise((resolve) => {
const style = document.createElement("style");
style.textContent =
"dialog, dialog::backdrop { animation: none !important; }";
document.head.appendChild(style);
const modal = document
.getElementById("my-plugin-dialog")
.closest("datasette-modal");
modal.showModal();
if (document.activeElement) {
document.activeElement.blur();
}
setTimeout(resolve, 500);
})' \
--width 760 --height 460 --retina \
--output "$tmp_dir/shot.png" --silent
kill "$server_pid" 2>/dev/null || true
wait "$server_pid" 2>/dev/null || true
server_pid=""
# Quantize to an 8-bit palette PNG to roughly halve the file size
cat > "$tmp_dir/quantize.py" <<'EOF'
import sys
from PIL import Image
img = Image.open(sys.argv[1]).convert("RGB")
img.quantize(
colors=256,
method=Image.Quantize.MEDIANCUT,
dither=Image.Dither.FLOYDSTEINBERG,
).save(sys.argv[2], optimize=True)
EOF
uv run --no-project --with pillow python \
"$tmp_dir/quantize.py" "$tmp_dir/shot.png" "$output"
echo "Wrote $output"