Copy docs pages as Markdown, drop the components copy button, match the rails

ba828e1990b6 · AtlantisPleb · · parent 779ce2e4de13

Copy docs pages as Markdown, drop the components copy button, match the rails

The docs copy button put the page's URL on the clipboard. A reader copying
from a docs page is nearly always taking it somewhere that renders Markdown --
an issue, a model prompt, a note -- and a link makes them fetch and strip the
page themselves. It now copies the Markdown source, which `render/1` carries
alongside the HTML and the table of contents so all three come from one read.

Component pages lose their copy button. What it copied was a single function
name already printed on the page.

The two sidebars carried a comment asserting they were identical while they
had drifted: the docs rail had 12px of block padding and 8px between its parts
that the application rail did not, and 2px between rows against the other's
1px. They are now one selector, so they cannot drift again, and both navs
state the same rhythm.

The component library is advertised outside production only. It documents the
parts a page is built from rather than anything a visitor came for. The route
still resolves everywhere -- a surface that is reachable but unadvertised is
honest, while one that disappears between environments is a difference that
only shows up in production.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149rBWy7br1Z7bbz9NrQhEr
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified assets/css/app.css
  • modified lib/openagents/runtime_config.ex
  • modified lib/openagents_web/components/layouts.ex
  • modified lib/openagents_web/components/layouts/components.html.heex
  • modified lib/openagents_web/components/layouts/docs.html.heex
  • modified lib/openagents_web/docs_catalog.ex
  • modified test/openagents/runtime_config_test.exs

Diff

7 files changed, +70 -29

assets/css/app.css modified +11 -14

@@ -222,15 +222,19 @@

222 222
/* This file is for your main application CSS */
223 223
224 224
/* ── Sidebar (OpenAgents layout with OpenAgents color tokens) ───────────── */
225
.sidebar {
225
/* One rule rather than two kept identical by hand. The previous pair carried a
226
   comment asserting they matched while they had already drifted on gap and
227
   padding, so the docs rail sat 12px lower with 8px more between its parts
228
   than the application rail. A shared selector cannot drift. */
229
.sidebar,
230
.docs-sidebar {
226 231
  display: flex;
227 232
  min-height: 0;
233
  flex: none;
228 234
  flex-direction: column;
229 235
  width: 280px;
230 236
  height: 100%;
231 237
  overflow: hidden;
232
  /* Identical to .docs-sidebar. Two sidebars in one application that differ by
233
     a surface rung and a border colour read as two applications. */
234 238
  border-right: 1px solid var(--line);
235 239
  background: var(--ink-void);
236 240
}

@@ -412,16 +416,6 @@

412 416
  overflow: hidden;
413 417
}
414 418
415
.docs-sidebar {
416
  display: flex;
417
  width: 280px;
418
  flex: none;
419
  flex-direction: column;
420
  gap: 8px;
421
  border-right: 1px solid var(--line);
422
  background: var(--ink-void);
423
  padding: 12px 0;
424
}
425 419
426 420
.docs-sidebar__header {
427 421
  display: flex;

@@ -434,7 +428,10 @@

434 428
  display: flex;
435 429
  flex: 1;
436 430
  flex-direction: column;
437
  gap: 2px;
431
  /* The same rhythm as .sidebar-nav. These two rails differ in exactly one
432
     way -- this one scrolls -- and nothing else about them should. */
433
  gap: 1px;
434
  padding-block: 2px;
438 435
  overflow-y: auto;
439 436
  overscroll-behavior: none;
440 437
}
lib/openagents/runtime_config.ex modified +17

@@ -93,6 +93,23 @@ defmodule OpenAgents.RuntimeConfig do

93 93
  def feature_enabled?(%__MODULE__{features: features}, feature),
94 94
    do: Map.fetch!(features, feature)
95 95
96
  @doc """
97
  Whether surfaces built for people working on the application are advertised.
98
99
  The component library documents the parts a page is assembled from rather
100
  than anything a visitor came for, so production does not link to it. This
101
  hides the link, not the route: `/components` still resolves, because a
102
  surface that is reachable but unadvertised is honest, while a route that
103
  disappears between environments is a difference that only shows up in
104
  production.
105
  """
106
  @spec internal_surfaces_visible?() :: boolean()
107
  def internal_surfaces_visible?, do: internal_surfaces_visible?(current!())
108
109
  @spec internal_surfaces_visible?(t()) :: boolean()
110
  def internal_surfaces_visible?(%__MODULE__{environment: environment}),
111
    do: environment != :production
112
96 113
  @spec fetch_secret(:openai_api_key) :: {:ok, String.t()} | {:error, :not_configured}
97 114
  def fetch_secret(:openai_api_key) do
98 115
    case Application.fetch_env(:openagents, :openai_api_key) do
lib/openagents_web/components/layouts.ex modified +8 -2

@@ -162,14 +162,20 @@ defmodule OpenAgentsWeb.Layouts do

162 162
  @doc """
163 163
  The secondary links at the foot of a sidebar.
164 164
165
  Every sidebar carries the same two, so a reader who finds the component
165
  Every sidebar carries the same set, so a reader who finds the component
166 166
  library from the application can get back to the docs from either, and does
167 167
  not have to remember which shell they are in.
168
169
  The component library is advertised outside production only. It documents
170
  the parts a page is built from rather than anything a visitor came for.
168 171
  """
169 172
  def sidebar_footer(assigns) do
173
    assigns =
174
      assign(assigns, :components_link?, OpenAgents.RuntimeConfig.internal_surfaces_visible?())
175
170 176
    ~H"""
171 177
    <footer class="sidebar-footer">
172
      <.link navigate={~p"/components"} class="sidebar-footer__link">
178
      <.link :if={@components_link?} navigate={~p"/components"} class="sidebar-footer__link">
173 179
        <UI.icon name="widget" /> Components
174 180
      </.link>
175 181
      <.link navigate={~p"/docs"} class="sidebar-footer__link">
lib/openagents_web/components/layouts/components.html.heex modified -7

@@ -46,13 +46,6 @@

46 46
47 47
      <div class="docs-header__controls">
48 48
        <Layouts.theme_toggle />
49
        <UI.copy_button
50
          :if={assigns[:item]}
51
          id="copy-component-source"
52
          text={@item.source}
53
          label="Copy source"
54
          copied_label="Copied"
55
        />
56 49
      </div>
57 50
    </header>
58 51
lib/openagents_web/components/layouts/docs.html.heex modified +7 -3

@@ -32,11 +32,15 @@

32 32
33 33
      <div class="docs-header__controls">
34 34
        <Layouts.theme_toggle />
35
        <%!-- The Markdown source, not the URL. A reader copying from a docs
36
        page is nearly always taking it somewhere that renders Markdown -- an
37
        issue, a model prompt, a note -- and a link makes them fetch and strip
38
        the page themselves. --%>
35 39
        <UI.copy_button
36 40
          :if={assigns[:page]}
37
          id="copy-docs-link"
38
          text={"#{OpenAgentsWeb.Endpoint.url()}/docs/#{@page.item.slug}"}
39
          label="Copy link"
41
          id="copy-docs-page"
42
          text={@page.markdown}
43
          label="Copy page"
40 44
          copied_label="Copied"
41 45
        />
42 46
      </div>
lib/openagents_web/docs_catalog.ex modified +4 -3

@@ -118,8 +118,9 @@ defmodule OpenAgentsWeb.DocsCatalog do

118 118
  @doc """
119 119
  Read and render one page.
120 120
121
  Returns the rendered HTML and the headings found in it, so a page and its
122
  table of contents come from one parse rather than two that can disagree.
121
  Returns the rendered HTML, the headings found in it, and the Markdown source
122
  it came from, so a page, its table of contents, and the text the copy button
123
  hands over all come from one read rather than several that can disagree.
123 124
  """
124 125
  def render(slug) do
125 126
    with %{} = item <- fetch(slug),

@@ -128,7 +129,7 @@ defmodule OpenAgentsWeb.DocsCatalog do

128 129
      toc = headings(markdown)
129 130
      html = markdown |> OpenAgents.Markdown.to_html() |> anchor_headings(toc)
130 131
131
      {:ok, %{item: item, html: html, toc: toc}}
132
      {:ok, %{item: item, html: html, toc: toc, markdown: markdown}}
132 133
    else
133 134
      _ -> :error
134 135
    end
test/openagents/runtime_config_test.exs modified +23

@@ -327,4 +327,27 @@ defmodule OpenAgents.RuntimeConfigTest do

327 327
  defp put_endpoint(settings, key, value) do
328 328
    Map.update!(settings, OpenAgentsWeb.Endpoint, &Keyword.put(&1, key, value))
329 329
  end
330
331
  describe "internal_surfaces_visible?/1" do
332
    test "production does not advertise the component library" do
333
      refute RuntimeConfig.internal_surfaces_visible?(%RuntimeConfig{
334
               environment: :production,
335
               staging_gate: 0,
336
               features: %{},
337
               groups: %{}
338
             })
339
    end
340
341
    test "every other environment does" do
342
      for environment <- [:development, :test, :staging] do
343
        assert RuntimeConfig.internal_surfaces_visible?(%RuntimeConfig{
344
                 environment: environment,
345
                 staging_gate: 0,
346
                 features: %{},
347
                 groups: %{}
348
               }),
349
               "expected #{environment} to advertise the component library"
350
      end
351
    end
352
  end
330 353
end

This page updates live while a promote is in flight · changelog