Add a /components catalog and a planned-component inventory.

1f32e14d527d · AtlantisPleb · · parent e0e61fb1718b

Add a /components catalog and a planned-component inventory.

Render every reusable CoreComponents and Layouts control on one
LiveView. Document the GitHub-shaped atoms, molecules, and organisms
still to build, and leave the branch unmerged.

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

  • added docs/component-library.md
  • modified docs/issues-projects-ui-roadmap.md
  • modified lib/openagents_web/components/core_components.ex
  • modified lib/openagents_web/components/layouts.ex
  • added lib/openagents_web/live/components_live.ex
  • modified lib/openagents_web/router.ex
  • added test/openagents_web/live/components_live_test.exs

Diff

7 files changed, +418 -2

docs/component-library.md added +142

@@ -0,0 +1,142 @@

1
# Component library
2
3
Date: 2026-08-19
4
5
This is the master list of reusable HEEx components for the Agent Forge UI.
6
7
- **Shipped** components appear on `/components`.
8
- **Planned** components come from the issues and projects UI roadmap
9
  (`docs/issues-projects-ui-roadmap.md`) and the GitHub-shaped harvest
10
  work in `sarah` (`docs/audits/2026-08-19-github-forge-atomic-components.md`,
11
  `docs/audits/2026-08-19-github-clone-harvest-candidates.md`).
12
13
Build new forge-only molecules and organisms in a dedicated module such as
14
`OpenAgentsWeb.Code` or `OpenAgentsWeb.Issues`. Keep generic atoms in
15
`OpenAgentsWeb.CoreComponents`. Add every new component to `/components`
16
when it ships.
17
18
Clone root: `~/work/projects/repos/githubclones/`.
19
20
## What exists today
21
22
These are the reusable function components on `origin/main`. The catalog
23
at `/components` renders each one.
24
25
| Layer | Component | Module | Notes |
26
| --- | --- | --- | --- |
27
| Atom | `button/1` | `CoreComponents` | Soft default and `variant="primary"`; supports `navigate` |
28
| Atom | `input/1` | `CoreComponents` | Text, checkbox, select, textarea, hidden |
29
| Atom | `icon/1` | `CoreComponents` | Heroicons via `hero-*` class names |
30
| Atom | `flash/1` | `CoreComponents` | Info and error toasts |
31
| Molecule | `header/1` | `CoreComponents` | Title, subtitle, actions |
32
| Molecule | `list/1` | `CoreComponents` | Titled description rows |
33
| Organism | `table/1` | `CoreComponents` | Zebra table; supports LiveView streams |
34
| Organism | `flash_group/1` | `Layouts` | Wraps page flashes; do not call outside layouts |
35
| Organism | `app/1` | `Layouts` | Page chrome |
36
| Atom | `theme_toggle/1` | `Layouts` | System, light, dark |
37
38
Phoenix also provides `<.form>`, `<.link>`, and `<.inputs_for>`. Use those
39
instead of hand-rolled forms.
40
41
## Planned atoms
42
43
Generic or tiny forge-only spans. Reuse `button`, `input`, and `icon`
44
where they already fit.
45
46
| Build | Role | Harvest |
47
| --- | --- | --- |
48
| `badge/1` | Visibility, issue state, label color | DaisyUI `badge`; Primer `primer-view_components/app/components/primer/beta/label.rb` |
49
| `avatar/1` | Owner and author faces | DaisyUI `avatar`; `gh-next/src/components/avatar.tsx` |
50
| `sha/1` | Short commit id that links to `/commit/:sha` | `git.limo/apps/gitgud_web/lib/gitgud_web/live/commit_diff_live.html.heex` |
51
| `ref_name/1` | Branch or tag pill | `primer-css/src/branch-name/branch-name.scss` |
52
| `counter/1` | Count beside a nav word | `primer-view_components/app/components/primer/beta/counter.rb` |
53
| `relative_time/1` | "3 hours ago" with a `datetime` | `primer-view_components/app/components/primer/beta/relative_time.rb` |
54
| `clipboard_copy/1` | Copy a SHA or clone URL | `primer-view_components/app/components/primer/beta/clipboard_copy.rb` |
55
| `truncate/1` | Single-line commit or issue title | `primer-view_components/app/components/primer/beta/truncate.rb` |
56
| `empty/1` | Empty issues, compare, or repo | DaisyUI empty pattern; `CoreComponents` has no empty yet |
57
| `kbd/1` | Keyboard hints | DaisyUI `kbd` |
58
59
## Planned molecules
60
61
| Build | Role | Harvest |
62
| --- | --- | --- |
63
| `owner_lockup/1` | Avatar + `owner/repo` links | `gh-next/src/app/(app)/[user]/[repository]/page.tsx`; `gitea/templates/repo/header.tmpl` |
64
| `path_breadcrumb/1` | Path prefixes that `patch` | `git.limo/apps/gitgud_web/lib/gitgud_web/live/tree_browser_live.html.heex`; `primer-view_components/app/components/primer/beta/breadcrumbs.html.erb` |
65
| `clone_field/1` | Readonly clone URL + copy | `git.limo/.../tree_browser_live.html.heex` (`#clone-repo`); `gitea/templates/repo/clone_panel.tmpl` |
66
| `branch_picker/1` | Current ref + menu of refs | `git.limo/.../branch_select_live.ex`; `gitea/templates/repo/branch_dropdown.tmpl` |
67
| `file_row/1` | Name, last commit, age (commit may be nil) | `gitea/templates/repo/view_list.tmpl`; `git.limo/.../tree_browser_live.html.heex` |
68
| `commit_row/1` | SHA, subject, author, time | `gitea/templates/repo/commits_table.tmpl` |
69
| `issue_row/1` | State, title, labels, author, comments | `gh-next/src/components/issues/issue-row.tsx`; `gitea/templates/repo/issue/list.tmpl` |
70
| `ref_row/1` | Branch or tag + SHA | `gitea/templates/repo/branch/list.tmpl` |
71
| `underline_nav/1` | Repo tabs with an active underline | `primer-view_components/app/components/primer/alpha/underline_nav.html.erb`; `gitea/templates/repo/navbar.tmpl` |
72
| `label_badge/1` | Colored issue label | `gh-next/src/components/label-badge.tsx`; `gitea/templates/repo/issue/labels/label_list.tmpl` |
73
| `assignee_stack/1` | Avatar group with overflow | `gh-next/src/components/issues/issue-row-avatar-stack.tsx` |
74
| `compare_ends/1` | Base picker + `...` + head picker | `gitea/templates/repo/diff/compare.tmpl` |
75
| `diff_stat/1` | File count and `+n` / `−n` | `gitea/templates/repo/diff/stats.tmpl` |
76
| `comment_form/1` | Markdown body + submit | `git.limo/.../comment_form_live.html.heex`; `gitea/templates/repo/issue/comment_tab.tmpl` |
77
78
## Planned organisms
79
80
| Build | Role | Harvest |
81
| --- | --- | --- |
82
| `repo_header/1` | Owner lockup, visibility, clone, actions | `gitea/templates/repo/header.tmpl`; `gh-next/.../[repository]/page.tsx` |
83
| `repo_subnav/1` | Code, Issues, Pull requests, Projects, Settings | `gitea/templates/repo/navbar.tmpl`; `gitea/templates/repo/issue/navbar.tmpl` |
84
| `issue_list/1` | Open/closed tabs, filters, streamed rows | `gh-next/src/components/issues/issue-list.tsx`; `gitea/templates/repo/issue/list.tmpl` |
85
| `issue_detail/1` | Title, state, body, sidebar metadata | `gh-next/src/app/(app)/[user]/[repository]/issues/[number]/page.tsx`; `gitea/templates/repo/issue/view.tmpl` |
86
| `comment_thread/1` | Chronological comments | `gitea/templates/repo/issue/view_content.tmpl`; `git.limo/.../issue_live.html.heex` |
87
| `issue_form/1` | New and edit issue | `gh-next/src/components/issues/new-issue-form.tsx`; `gitea/templates/repo/issue/new_form.tmpl` |
88
| `label_manager/1` | Create, edit, delete labels | `gitea/templates/repo/issue/labels/label_list.tmpl` |
89
| `milestone_list/1` | Progress cards | `gitea/templates/repo/issue/milestones.tmpl` |
90
| `project_board/1` | Columns of items (no drag-and-drop at first) | GitHub Projects V2 REST in `docs/github-api-issues-projects-assessment.md` |
91
| `file_table/1` | Directory listing | `gitea/templates/repo/view_list.tmpl`; `git.limo/.../tree_browser_live.ex` |
92
| `blob_panel/1` | File view + actions | `gitea/templates/repo/view_file.tmpl`; `git.limo/.../blob_viewer_live.ex` |
93
| `readme_panel/1` | Rendered README | `git.limo/.../tree_browser_live.html.heex` README card |
94
| `commit_list/1` | Bounded log | `gitea/templates/repo/commits.tmpl` |
95
| `diff_viewer/1` | Unified hunks | `gitea/templates/repo/diff/box.tmpl`; `section_unified.tmpl` |
96
| `compare_panel/1` | Compare ends + diff | `gitea/templates/repo/diff/compare.tmpl` |
97
98
## Planned pages
99
100
Mount these on GitHub-shaped paths. See
101
`docs/issues-projects-ui-roadmap.md` and the sarah harvest audit.
102
103
| Page | Path | First organisms |
104
| --- | --- | --- |
105
| Owner | `/:owner` | Repo cards |
106
| Repo home | `/:owner/:repo` | `repo_header`, `file_table`, `readme_panel` |
107
| Issues | `/:owner/:repo/issues` | `repo_header`, `issue_list` |
108
| Issue | `/:owner/:repo/issues/:number` | `issue_detail`, `comment_thread` |
109
| New issue | `/:owner/:repo/issues/new` | `issue_form` |
110
| Labels | `/:owner/:repo/labels` | `label_manager` |
111
| Milestones | `/:owner/:repo/milestones` | `milestone_list` |
112
| Projects | `/:owner/:repo/projects` | `project_board` |
113
| Tree | `/:owner/:repo/tree/:ref/*path` | `file_table` |
114
| Blob | `/:owner/:repo/blob/:ref/*path` | `blob_panel` |
115
| Commit | `/:owner/:repo/commit/:sha` | `diff_viewer` |
116
| Compare | `/:owner/:repo/compare/:base...:head` | `compare_panel` |
117
| Catalog | `/components` | This page |
118
119
## Build order
120
121
1. Atoms that issues need first: `badge`, `avatar`, `relative_time`,
122
   `empty`, `label_badge`.
123
2. Molecules: `owner_lockup`, `issue_row`, `underline_nav`,
124
   `comment_form`.
125
3. Organisms: `repo_header`, `repo_subnav`, `issue_list`,
126
   `issue_detail`, `comment_thread`.
127
4. Pages on `/:owner/:repo/issues` as in the UI roadmap.
128
5. Code surfaces (`file_table`, `blob_panel`, `diff_viewer`) after the
129
   tracker dogfoods.
130
6. `/api/v3` stays the machine API from
131
   `docs/github-api-issues-projects-assessment.md`. It is not a
132
   component.
133
134
Do not add Octicons, Primer CSS as a runtime, or a second button
135
system. DaisyUI plus `CoreComponents` is the kit.
136
137
## See also
138
139
- `/components` — live catalog of shipped components
140
- `docs/issues-projects-ui-roadmap.md` — page-level UI plan
141
- `docs/issues-projects-work-plan.md` — API epics
142
- `docs/github-api-issues-projects-assessment.md` — GitHub REST subset
docs/issues-projects-ui-roadmap.md modified +2

@@ -6,6 +6,8 @@ Source: `docs/issues-projects-work-plan.md`, `docs/github-api-issues-projects-as

6 6
7 7
This roadmap outlines a simple browser UI for the existing OpenAgents issues, labels, assignees, milestones, comments, and Projects V2 API. The implementation uses Phoenix 1.8 LiveView, `OpenAgentsWeb.CoreComponents`, and DaisyUI component classes. The goal is a GitHub-shaped surface that is usable, not a pixel-perfect clone.
8 8
9
The reusable-component inventory, live catalog, and the list of components still to build are in `docs/component-library.md`. Shipped components render at `/components`.
10
9 11
## Scope and assumptions
10 12
11 13
- The JSON API is in place and follows the paths in `docs/github-api-issues-projects-assessment.md`.
lib/openagents_web/components/core_components.ex modified +1 -1

@@ -95,7 +95,7 @@ defmodule OpenAgentsWeb.CoreComponents do

95 95
      <.button phx-click="go" variant="primary">Send!</.button>
96 96
      <.button navigate={~p"/"}>Home</.button>
97 97
  """
98
  attr :rest, :global, include: ~w(href navigate patch method download name value disabled)
98
  attr :rest, :global, include: ~w(href navigate patch method download name value disabled type)
99 99
  attr :class, :any
100 100
  attr :variant, :string, values: ~w(primary)
101 101
  slot :inner_block, required: true
lib/openagents_web/components/layouts.ex modified +8 -1

@@ -31,6 +31,10 @@ defmodule OpenAgentsWeb.Layouts do

31 31
    default: nil,
32 32
    doc: "the current [scope](https://phoenix.hexdocs.pm/scopes.html)"
33 33
34
  attr :wide, :boolean,
35
    default: false,
36
    doc: "use a wider content column for catalog and list surfaces"
37
34 38
  slot :inner_block, required: true
35 39
36 40
  def app(assigns) do

@@ -44,6 +48,9 @@ defmodule OpenAgentsWeb.Layouts do

44 48
      </div>
45 49
      <div class="flex-none">
46 50
        <ul class="flex flex-column px-1 space-x-4 items-center">
51
          <li>
52
            <.link navigate={~p"/components"} class="btn btn-ghost">Components</.link>
53
          </li>
47 54
          <li>
48 55
            <a href="https://phoenixframework.org/" class="btn btn-ghost">Website</a>
49 56
          </li>

@@ -63,7 +70,7 @@ defmodule OpenAgentsWeb.Layouts do

63 70
    </header>
64 71
65 72
    <main class="px-4 py-20 sm:px-6 lg:px-8">
66
      <div class="mx-auto max-w-2xl space-y-4">
73
      <div class={["mx-auto space-y-4", @wide && "max-w-6xl", !@wide && "max-w-2xl"]}>
67 74
        {render_slot(@inner_block)}
68 75
      </div>
69 76
    </main>
lib/openagents_web/live/components_live.ex added +236

@@ -0,0 +1,236 @@

1
defmodule OpenAgentsWeb.ComponentsLive do
2
  @moduledoc """
3
  Public catalog of reusable HEEx components that ship in this codebase.
4
5
  Renders every function component in `OpenAgentsWeb.CoreComponents` and
6
  `OpenAgentsWeb.Layouts` (except `Layouts.app/1` and `flash_group/1`, which
7
  wrap this page). Planned forge and issues components live in
8
  `docs/component-library.md`.
9
  """
10
11
  use OpenAgentsWeb, :live_view
12
13
  @sample_rows [
14
    %{id: 1, owner: "OpenAgentsInc", repo: "openagents.com", state: "open"},
15
    %{id: 2, owner: "OpenAgentsInc", repo: "sarah", state: "open"},
16
    %{id: 3, owner: "OpenAgentsInc", repo: "arcade", state: "closed"}
17
  ]
18
19
  @icons ~w(
20
    hero-information-circle hero-exclamation-circle hero-x-mark
21
    hero-arrow-path hero-sun-micro hero-moon-micro hero-computer-desktop-micro
22
  )
23
24
  @impl true
25
  def mount(_params, _session, socket) do
26
    form =
27
      to_form(
28
        %{
29
          "title" => "Ship the component catalog",
30
          "body" => "Show every reusable control on one page.",
31
          "state" => "open",
32
          "public" => "true"
33
        },
34
        as: :demo
35
      )
36
37
    {:ok,
38
     socket
39
     |> assign(:page_title, "Components")
40
     |> assign(:form, form)
41
     |> assign(:rows, @sample_rows)
42
     |> assign(:icons, @icons)}
43
  end
44
45
  @impl true
46
  def handle_event("validate", %{"demo" => params}, socket) do
47
    {:noreply, assign(socket, :form, to_form(params, as: :demo))}
48
  end
49
50
  def handle_event("save", _params, socket) do
51
    {:noreply, put_flash(socket, :info, "Demo form submitted. Nothing was saved.")}
52
  end
53
54
  def handle_event("flash-info", _params, socket) do
55
    {:noreply, put_flash(socket, :info, "This is the info flash from CoreComponents.")}
56
  end
57
58
  def handle_event("flash-error", _params, socket) do
59
    {:noreply, put_flash(socket, :error, "This is the error flash from CoreComponents.")}
60
  end
61
62
  @impl true
63
  def render(assigns) do
64
    ~H"""
65
    <Layouts.app flash={@flash} wide>
66
      <div id="components-gallery" class="space-y-12">
67
        <header class="space-y-3">
68
          <.header>
69
            Component library
70
            <:subtitle>
71
              Live examples of every reusable function component in this
72
              repository. Planned GitHub-shaped components are listed in
73
              docs/component-library.md.
74
            </:subtitle>
75
          </.header>
76
          <p class="text-pretty text-base text-base-content/70 max-w-[68ch]">
77
            These controls come from <code>OpenAgentsWeb.CoreComponents</code>
78
            and <code>OpenAgentsWeb.Layouts</code>. DaisyUI classes style them.
79
            New forge and issues components should land in a dedicated module
80
            and appear on this page when they ship.
81
          </p>
82
        </header>
83
84
        <.catalog_section
85
          id="section-button"
86
          title="Button"
87
          source="OpenAgentsWeb.CoreComponents.button/1"
88
        >
89
          <div class="flex flex-wrap items-center gap-3">
90
            <.button id="demo-button-default">Default</.button>
91
            <.button id="demo-button-primary" variant="primary">Primary</.button>
92
            <.button id="demo-button-navigate" navigate={~p"/"}>Navigate home</.button>
93
            <.button id="demo-button-disabled" disabled>Disabled</.button>
94
          </div>
95
        </.catalog_section>
96
97
        <.catalog_section
98
          id="section-input"
99
          title="Input"
100
          source="OpenAgentsWeb.CoreComponents.input/1"
101
        >
102
          <.form
103
            for={@form}
104
            id="component-form"
105
            phx-change="validate"
106
            phx-submit="save"
107
            class="grid gap-4 sm:grid-cols-2"
108
          >
109
            <div>
110
              <.input field={@form[:title]} label="Title" />
111
            </div>
112
            <div>
113
              <.input
114
                field={@form[:state]}
115
                type="select"
116
                label="State"
117
                options={[{"Open", "open"}, {"Closed", "closed"}]}
118
              />
119
            </div>
120
            <div class="sm:col-span-2">
121
              <.input field={@form[:body]} type="textarea" label="Body" />
122
            </div>
123
            <div>
124
              <.input field={@form[:public]} type="checkbox" label="Public repository" />
125
            </div>
126
            <div class="flex items-end">
127
              <.button type="submit" variant="primary">Save demo</.button>
128
            </div>
129
          </.form>
130
        </.catalog_section>
131
132
        <.catalog_section
133
          id="section-header"
134
          title="Header"
135
          source="OpenAgentsWeb.CoreComponents.header/1"
136
        >
137
          <.header>
138
            Repository issues
139
            <:subtitle>Open and closed issues for this repository.</:subtitle>
140
            <:actions>
141
              <.button variant="primary">New issue</.button>
142
            </:actions>
143
          </.header>
144
        </.catalog_section>
145
146
        <.catalog_section
147
          id="section-table"
148
          title="Table"
149
          source="OpenAgentsWeb.CoreComponents.table/1"
150
        >
151
          <.table id="demo-table" rows={@rows}>
152
            <:col :let={row} label="Owner">{row.owner}</:col>
153
            <:col :let={row} label="Repository">{row.repo}</:col>
154
            <:col :let={row} label="State">{row.state}</:col>
155
            <:action :let={row}>
156
              <.link navigate={~p"/"} class="link link-hover">View {row.repo}</.link>
157
            </:action>
158
          </.table>
159
        </.catalog_section>
160
161
        <.catalog_section
162
          id="section-list"
163
          title="List"
164
          source="OpenAgentsWeb.CoreComponents.list/1"
165
        >
166
          <.list>
167
            <:item title="Flash">Toast alerts for info and error.</:item>
168
            <:item title="Button">Primary and soft variants, plus navigation.</:item>
169
            <:item title="Input">Text, select, textarea, and checkbox.</:item>
170
          </.list>
171
        </.catalog_section>
172
173
        <.catalog_section
174
          id="section-icon"
175
          title="Icon"
176
          source="OpenAgentsWeb.CoreComponents.icon/1"
177
        >
178
          <ul id="demo-icons" role="list" class="flex flex-wrap gap-4">
179
            <li :for={name <- @icons} class="flex flex-col items-center gap-2 w-28">
180
              <.icon name={name} class="size-6" />
181
              <p class="text-center text-sm text-base-content/70">{name}</p>
182
            </li>
183
          </ul>
184
        </.catalog_section>
185
186
        <.catalog_section
187
          id="section-flash"
188
          title="Flash"
189
          source="OpenAgentsWeb.CoreComponents.flash/1"
190
        >
191
          <p class="text-pretty text-base text-base-content/70 max-w-[68ch]">
192
            Flash renders through <code>Layouts.flash_group/1</code> at the
193
            corner of the page. Trigger a sample message:
194
          </p>
195
          <div class="flex flex-wrap gap-3">
196
            <.button id="demo-flash-info" phx-click="flash-info">Show info flash</.button>
197
            <.button id="demo-flash-error" phx-click="flash-error">Show error flash</.button>
198
          </div>
199
        </.catalog_section>
200
201
        <.catalog_section
202
          id="section-theme-toggle"
203
          title="Theme toggle"
204
          source="OpenAgentsWeb.Layouts.theme_toggle/1"
205
        >
206
          <p class="text-pretty text-base text-base-content/70 max-w-[68ch]">
207
            System, light, and dark. The same control is in the site header.
208
          </p>
209
          <div id="demo-theme-toggle">
210
            <Layouts.theme_toggle />
211
          </div>
212
        </.catalog_section>
213
      </div>
214
    </Layouts.app>
215
    """
216
  end
217
218
  attr :id, :string, required: true
219
  attr :title, :string, required: true
220
  attr :source, :string, required: true
221
  slot :inner_block, required: true
222
223
  defp catalog_section(assigns) do
224
    ~H"""
225
    <section id={@id} class="space-y-4">
226
      <div class="space-y-1">
227
        <h2 class="text-xl font-semibold tracking-tight text-balance">{@title}</h2>
228
        <p class="text-sm text-base-content/70"><code>{@source}</code></p>
229
      </div>
230
      <div class="rounded-box border border-base-300 bg-base-100 p-6">
231
        {render_slot(@inner_block)}
232
      </div>
233
    </section>
234
    """
235
  end
236
end
lib/openagents_web/router.ex modified +1

@@ -18,6 +18,7 @@ defmodule OpenAgentsWeb.Router do

18 18
    pipe_through :browser
19 19
20 20
    get "/", PageController, :home
21
    live "/components", ComponentsLive, :index
21 22
  end
22 23
23 24
  scope "/api/v3", OpenAgentsWeb do
test/openagents_web/live/components_live_test.exs added +28

@@ -0,0 +1,28 @@

1
defmodule OpenAgentsWeb.ComponentsLiveTest do
2
  use OpenAgentsWeb.ConnCase, async: true
3
4
  import Phoenix.LiveViewTest
5
6
  test "renders every reusable component on /components", %{conn: conn} do
7
    {:ok, view, html} = live(conn, ~p"/components")
8
9
    assert html =~ "Component library"
10
    assert has_element?(view, "#components-gallery")
11
    assert has_element?(view, "#demo-button-primary", "Primary")
12
    assert has_element?(view, "#component-form")
13
    assert has_element?(view, "#demo-table")
14
    assert has_element?(view, "#demo-icons")
15
    assert has_element?(view, "#demo-theme-toggle")
16
    assert has_element?(view, "#section-flash")
17
  end
18
19
  test "info flash action shows the flash", %{conn: conn} do
20
    {:ok, view, _html} = live(conn, ~p"/components")
21
22
    view
23
    |> element("#demo-flash-info")
24
    |> render_click()
25
26
    assert render(view) =~ "This is the info flash from CoreComponents."
27
  end
28
end

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