From cec5e6b2ef5d33eda31f9be092c4ec174ff0c5e9 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Thu, 24 Sep 2026 15:06:57 -0700 Subject: [PATCH] Screenshots for reusable modal dialogs docs, plus just shots (#2958) docs/shots.yml defines documentation screenshots for shot-scraper "Just shots" runs them. --- Justfile | 7 ++++- docs/contributing.rst | 13 ++++++++ docs/images/modal-classes.webp | Bin 0 -> 16490 bytes docs/images/modal-example.webp | Bin 0 -> 12154 bytes docs/javascript_plugins.rst | 54 ++++++++++----------------------- docs/shots.yml | 38 +++++++++++++++++++++++ docs/shots/modal-classes.js | 41 +++++++++++++++++++++++++ docs/shots/modal-example.js | 38 +++++++++++++++++++++++ pyproject.toml | 3 ++ 9 files changed, 155 insertions(+), 39 deletions(-) create mode 100644 docs/images/modal-classes.webp create mode 100644 docs/images/modal-example.webp create mode 100644 docs/shots.yml create mode 100644 docs/shots/modal-classes.js create mode 100644 docs/shots/modal-example.js diff --git a/Justfile b/Justfile index 6ffff870..d1b69378 100644 --- a/Justfile +++ b/Justfile @@ -49,13 +49,18 @@ export DATASETTE_SECRET := "not_a_secret" uv run cog -r README.md docs/*.rst # Serve live docs on localhost:8000 -@docs: cog blacken-docs +@docs: shots cog blacken-docs uv run make -C docs livehtml # Build docs as static HTML @docs-build: cog blacken-docs rm -rf docs/_build && cd docs && uv run make html +# Take any missing documentation screenshots defined in docs/shots.yml +@shots: + uv run --group shots shot-scraper install + cd docs && uv run --group shots shot-scraper multi shots.yml --no-clobber --reduced-motion --retina + # Apply Black @black: uv run black datasette tests diff --git a/docs/contributing.rst b/docs/contributing.rst index 692f94c8..2de1fce0 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -312,6 +312,19 @@ To update these pages, run the following command:: uv run cog -r docs/*.rst +.. _contributing_documentation_screenshots: + +Documentation screenshots +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Screenshots in the documentation are defined in ``docs/shots.yml`` and taken using `shot-scraper `__. That file starts a Datasette server that loads JavaScript from ``docs/shots/``, then saves each screenshot as a WebP image in ``docs/images/``. + +To take any screenshots that do not exist yet, run:: + + just shots + +``just docs`` runs this too. Existing images are skipped. To replace a screenshot, delete its image file and run ``just shots`` again. + .. _contributing_template_contexts: Documented template contexts diff --git a/docs/images/modal-classes.webp b/docs/images/modal-classes.webp new file mode 100644 index 0000000000000000000000000000000000000000..9de7576f76f93b4896a8a6c87be6d77242c73b13 GIT binary patch literal 16490 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�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 s_}}Ou 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 literal 0 HcmV?d00001 diff --git a/docs/javascript_plugins.rst b/docs/javascript_plugins.rst index 4dd3f809..159f952d 100644 --- a/docs/javascript_plugins.rst +++ b/docs/javascript_plugins.rst @@ -488,46 +488,16 @@ Creating a dialog This example uses the :ref:`datasette_init event ` to add a button that opens a dialog: -.. code-block:: javascript +.. literalinclude:: shots/modal-example.js + :language: javascript - document.addEventListener("datasette_init", () => { - const openButton = document.createElement("button"); - openButton.type = "button"; - openButton.textContent = "Open example dialog"; - // Indicate that this button opens a dialog: - openButton.setAttribute("aria-haspopup", "dialog"); - // Identify which dialog it controls: - openButton.setAttribute("aria-controls", "my-plugin-dialog"); +Clicking that button opens this dialog: - const modal = DatasetteModal.create(); - const dialog = modal.dialog; - dialog.id = "my-plugin-dialog"; - // Tell screenreaders the dialog is labelled by #my-plugin-dialog-title - dialog.setAttribute("aria-labelledby", "my-plugin-dialog-title"); - dialog.innerHTML = ` - - - `; +.. only:: not latex - const closeButton = dialog.querySelector("button"); - closeButton.addEventListener("click", () => { - modal.requestClose("cancel"); - }); - openButton.addEventListener("click", () => { - modal.show({ returnFocusTo: openButton, initialFocus: closeButton }); - }); - - document.body.append(modal); - document.querySelector("section.content").append(openButton); - }); + .. image:: images/modal-example.webp + :width: 584px + :alt: A dialog titled Example dialog, with the text "This dialog uses Datasette's shared styles and keyboard behavior." and a Close button in the footer, shown in front of a dimmed Datasette page Opening and closing ~~~~~~~~~~~~~~~~~~~ @@ -584,7 +554,15 @@ If an operation fails, set ``modal.busy = false`` so the user can retry or close Shared CSS classes ~~~~~~~~~~~~~~~~~~ -The classes in the example above provide built-in styling. The following classes can be used by your modal: +The classes in the example above provide built-in styling. This dialog uses every class listed below, including a ``modal-meta`` count in the header and ``footer-info`` text next to ``modal-btn-ghost`` and ``modal-btn-primary`` buttons in the footer: + +.. only:: not latex + + .. image:: images/modal-classes.webp + :width: 584px + :alt: A dialog titled Export rows with a "3 selected" badge in its header, a list of three plant names in the body, and a footer containing the text "CSV, UTF-8", a Cancel button and a blue Export button + +The following classes can be used by your modal: ``datasette-modal`` Added automatically to the native ```` when the wrapper is connected to the page. Provides the dialog's sizing, background, rounded corners, shadow, backdrop and animations. diff --git a/docs/shots.yml b/docs/shots.yml new file mode 100644 index 00000000..0a5f9fa6 --- /dev/null +++ b/docs/shots.yml @@ -0,0 +1,38 @@ +# Screenshots used by the documentation, taken using shot-scraper: +# https://shot-scraper.datasette.io/en/stable/multi.html +# +# Run "just shots" from the repository root to create any that are +# missing. Existing images are skipped, so delete an image to recreate it. +# +# Paths are relative to this docs/ directory. + +# Serves the JavaScript in docs/shots/ and loads it on every page. +# List form means the datasette process is stopped directly when done. +- server: + - datasette + - --memory + - --port + - 8755 + - --static + - shots:shots + - -s + - extra_js_urls + - '["/shots/modal-example.js", "/shots/modal-classes.js"]' + +# javascript_plugins.rst - Reusable modal dialogs +- output: images/modal-example.webp + url: http://localhost:8755/ + javascript: | + document.querySelector('[aria-controls="my-plugin-dialog"]').click(); + selector: "#my-plugin-dialog" + padding: 32 + quality: 70 + +- output: images/modal-classes.webp + url: http://localhost:8755/ + javascript: | + document.querySelector('[aria-controls="export-dialog"]').click(); + document.activeElement.blur(); + selector: "#export-dialog" + padding: 32 + quality: 70 diff --git a/docs/shots/modal-classes.js b/docs/shots/modal-classes.js new file mode 100644 index 00000000..57372613 --- /dev/null +++ b/docs/shots/modal-classes.js @@ -0,0 +1,41 @@ +// Demonstrates every shared modal CSS class, for images/modal-classes.webp +document.addEventListener("datasette_init", () => { + const openButton = document.createElement("button"); + openButton.type = "button"; + openButton.textContent = "Open export dialog"; + openButton.setAttribute("aria-haspopup", "dialog"); + openButton.setAttribute("aria-controls", "export-dialog"); + + const modal = DatasetteModal.create(); + const dialog = modal.dialog; + dialog.id = "export-dialog"; + dialog.setAttribute("aria-labelledby", "export-dialog-title"); + dialog.innerHTML = ` + + + `; + + const [cancelButton, exportButton] = dialog.querySelectorAll(".modal-footer button"); + cancelButton.addEventListener("click", () => modal.requestClose("cancel")); + exportButton.addEventListener("click", () => modal.close()); + openButton.addEventListener("click", () => { + modal.show({ returnFocusTo: openButton, initialFocus: exportButton }); + }); + + document.body.append(modal); + document.querySelector("section.content").append(openButton); +}); diff --git a/docs/shots/modal-example.js b/docs/shots/modal-example.js new file mode 100644 index 00000000..fd8210ab --- /dev/null +++ b/docs/shots/modal-example.js @@ -0,0 +1,38 @@ +document.addEventListener("datasette_init", () => { + const openButton = document.createElement("button"); + openButton.type = "button"; + openButton.textContent = "Open example dialog"; + // Indicate that this button opens a dialog: + openButton.setAttribute("aria-haspopup", "dialog"); + // Identify which dialog it controls: + openButton.setAttribute("aria-controls", "my-plugin-dialog"); + + const modal = DatasetteModal.create(); + const dialog = modal.dialog; + dialog.id = "my-plugin-dialog"; + // Tell screenreaders the dialog is labelled by #my-plugin-dialog-title + dialog.setAttribute("aria-labelledby", "my-plugin-dialog-title"); + dialog.innerHTML = ` + + + `; + + const closeButton = dialog.querySelector("button"); + closeButton.addEventListener("click", () => { + modal.requestClose("cancel"); + }); + openButton.addEventListener("click", () => { + modal.show({ returnFocusTo: openButton, initialFocus: closeButton }); + }); + + document.body.append(modal); + document.querySelector("section.content").append(openButton); +}); diff --git a/pyproject.toml b/pyproject.toml index edabae2d..cd8b5513 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,6 +87,9 @@ dev = [ playwright = [ "pytest-playwright>=0.8.0", ] +shots = [ + "shot-scraper>=1.12", +] [project.optional-dependencies] rich = ["rich"]