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 00000000..9de7576f Binary files /dev/null and b/docs/images/modal-classes.webp differ diff --git a/docs/images/modal-example.webp b/docs/images/modal-example.webp new file mode 100644 index 00000000..6ba95686 Binary files /dev/null and b/docs/images/modal-example.webp differ 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"]