Catalogue the whole repository view, not only its parts

efaecf176332 · AtlantisPleb · · parent a69decf10a4d

Catalogue the whole repository view, not only its parts

The library had the pieces of a repository home and no page showing them
together: a file table, an About rail, a diff, and a breadcrumb, each
demonstrated alone. A reader could not tell from the catalogue what a
repository page is supposed to look like, and a surface that needed one had to
invent the arrangement.

`repo_view/1` is that arrangement and nothing else. Identity, sections, main
column, rail -- the three parts arrive as slots, so a surface that wants a
commit list where the tree usually goes passes one instead of a flag being
added here. The `.repo-view` grid it uses was already written and unused.

`repo_tabs/1` is the missing piece. Code, Issues, Pull requests, Projects,
Wiki, Insights, Settings are links that change the URL, so the selected one
carries `aria-current="page"` and none of them claims a tab role: a tab role
promises panels that swap in place, and a reader who accepts the promise and
reaches for the arrow keys gets nothing. Counts live in their own element, so
"Issues" stays findable by that word alone.

Two existing components grew what the composition needs. `file_table/1` takes
a commit count beside the latest commit, and renders a message and an age per
row when an entry carries them -- when no entry does, those two columns are
not emitted at all, so the cheap tree keeps the markup it already had. Its
docstring promised this and called the slot `meta`; the entry map is a better
place for it, because the caller filling it is walking history per path
anyway. `repo_about/1` takes contributors as faces with a total, since six
faces alone claims six contributors.

The demo is this repository: its own root, each row's message from
`git log -1 -- <path>`, the real sizes, the real contributors, the real
commit count. A composed demo is only worth reading if it carries real
proportions -- invented ones make every column look comfortable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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/openagents.css
  • modified lib/openagents_web/component_catalog.ex
  • modified lib/openagents_web/components/ui.ex
  • modified lib/openagents_web/live/components_live.ex
  • modified test/openagents_web/live/components_live_test.exs

Diff

5 files changed, +608 -6

assets/css/openagents.css modified +187 -1

@@ -4935,6 +4935,102 @@

4935 4935
    }
4936 4936
  }
4937 4937
4938
  /* A grid child will not shrink below its content unless told to, and a long
4939
     unbroken commit message inside the tree is exactly that content. */
4940
  .repo-view__main,
4941
  .repo-view__rail {
4942
    min-width: 0;
4943
  }
4944
4945
  /* ── Repository page ────────────────────────────────────────────────────── */
4946
4947
  .repo-page {
4948
    display: flex;
4949
    flex-direction: column;
4950
    gap: 16px;
4951
  }
4952
4953
  .repo-page__identity {
4954
    display: flex;
4955
    flex-wrap: wrap;
4956
    align-items: center;
4957
    gap: 12px;
4958
  }
4959
4960
  /* The trail is the page title here, so it is set at title size rather than
4961
     at the small size a trail carries when it sits above one. */
4962
  .repo-page__trail .breadcrumb__link,
4963
  .repo-page__trail .breadcrumb__current,
4964
  .repo-page__trail .breadcrumb__separator {
4965
    font-size: 1.25rem;
4966
  }
4967
4968
  .repo-page__trail .breadcrumb__current {
4969
    color: var(--text-primary);
4970
    font-weight: 600;
4971
  }
4972
4973
  /* ── Section bar ────────────────────────────────────────────────────────── */
4974
4975
  /* Links that change the URL, so the selected one is marked by aria-current
4976
     and the underline follows that attribute rather than a second class. */
4977
  .repo-tabs {
4978
    display: flex;
4979
    gap: 4px;
4980
    overflow-x: auto;
4981
    border-block-end: 1px solid var(--line);
4982
  }
4983
4984
  .repo-tabs__tab {
4985
    display: inline-flex;
4986
    align-items: center;
4987
    gap: 8px;
4988
    padding: 8px 12px;
4989
    border-block-end: 2px solid transparent;
4990
    color: var(--text-muted);
4991
    font-size: 0.875rem;
4992
    text-decoration: none;
4993
    white-space: nowrap;
4994
    transition:
4995
      color var(--motion-fast) var(--ease),
4996
      border-color var(--motion-fast) var(--ease);
4997
  }
4998
4999
  .repo-tabs__tab .icon {
5000
    width: 16px;
5001
    height: 16px;
5002
    flex: none;
5003
    color: var(--icon-tertiary);
5004
    font-size: 16px;
5005
  }
5006
5007
  @media (hover: hover) {
5008
    .repo-tabs__tab:hover {
5009
      border-color: var(--line-strong);
5010
      color: var(--text-primary);
5011
    }
5012
  }
5013
5014
  .repo-tabs__tab[aria-current="page"] {
5015
    border-color: var(--accent-bright);
5016
    color: var(--text-primary);
5017
    font-weight: 600;
5018
  }
5019
5020
  .repo-tabs__tab[aria-current="page"] .icon {
5021
    color: var(--icon-primary);
5022
  }
5023
5024
  .repo-tabs__count {
5025
    border-radius: 999px;
5026
    background: var(--wash-selected);
5027
    padding: 1px 7px;
5028
    color: var(--text-muted);
5029
    font-size: 0.75rem;
5030
    font-variant-numeric: tabular-nums;
5031
    font-weight: 500;
5032
  }
5033
4938 5034
  /* ── File table ─────────────────────────────────────────────────────────── */
4939 5035
4940 5036
  .file-table {

@@ -4995,6 +5091,30 @@

4995 5091
    font-size: 0.8125rem;
4996 5092
  }
4997 5093
5094
  /* How many commits are behind the one named to its left. It belongs at the
5095
     far edge because it is the only thing in this row that is not about that
5096
     single commit. */
5097
  .file-table__commits {
5098
    display: inline-flex;
5099
    align-items: center;
5100
    gap: 6px;
5101
    margin-inline-start: auto;
5102
    color: var(--text-muted);
5103
  }
5104
5105
  .file-table__commits .icon {
5106
    width: 16px;
5107
    height: 16px;
5108
    flex: none;
5109
    color: var(--icon-tertiary);
5110
    font-size: 16px;
5111
  }
5112
5113
  .file-table__commits strong {
5114
    color: var(--text-primary);
5115
    font-variant-numeric: tabular-nums;
5116
  }
5117
4998 5118
  .file-table__list {
4999 5119
    width: 100%;
5000 5120
    border-collapse: collapse;

@@ -5039,6 +5159,20 @@

5039 5159
    }
5040 5160
  }
5041 5161
5162
  /* The one column that gives up its width. `width: 100%` claims the slack a
5163
     table would otherwise hand to the name, and `max-width: 0` lets the cell
5164
     shrink back under it so a long subject truncates instead of widening the
5165
     table past its container. */
5166
  .file-row__message {
5167
    width: 100%;
5168
    max-width: 0;
5169
    overflow: hidden;
5170
    color: var(--text-muted);
5171
    font-size: 0.8125rem;
5172
    text-overflow: ellipsis;
5173
    white-space: nowrap;
5174
  }
5175
5042 5176
  .file-row__size {
5043 5177
    width: 1%;
5044 5178
    color: var(--text-dim);

@@ -5048,6 +5182,14 @@

5048 5182
    white-space: nowrap;
5049 5183
  }
5050 5184
5185
  .file-row__age {
5186
    width: 1%;
5187
    color: var(--text-dim);
5188
    font-size: 0.75rem;
5189
    text-align: end;
5190
    white-space: nowrap;
5191
  }
5192
5051 5193
  .file-table__empty {
5052 5194
    padding: 16px 12px;
5053 5195
    color: var(--text-dim);

@@ -5118,13 +5260,57 @@

5118 5260
    font-size: 16px;
5119 5261
  }
5120 5262
5121
  .repo-about__languages h3 {
5263
  .repo-about__languages h3,
5264
  .repo-about__contributors h3 {
5122 5265
    padding-block-end: 8px;
5123 5266
    color: var(--text-primary);
5124 5267
    font-size: 0.875rem;
5125 5268
    font-weight: 600;
5126 5269
  }
5127 5270
5271
  .repo-about__count {
5272
    color: var(--text-dim);
5273
    font-variant-numeric: tabular-nums;
5274
    font-weight: 400;
5275
  }
5276
5277
  /* A wrapped cluster rather than a column: the group reads as one fact about
5278
     the repository, not as a list to be gone through name by name. Each face
5279
     still carries its own accessible name. */
5280
  .contributor-cluster {
5281
    display: flex;
5282
    flex-wrap: wrap;
5283
    align-items: center;
5284
    gap: 4px;
5285
    margin: 0;
5286
    padding: 0;
5287
    list-style: none;
5288
  }
5289
5290
  .contributor-cluster > li {
5291
    display: inline-flex;
5292
  }
5293
5294
  .contributor-cluster__count {
5295
    color: var(--text-muted);
5296
    font-size: 0.75rem;
5297
    font-variant-numeric: tabular-nums;
5298
  }
5299
5300
  /* A hairline in the page's own fill, so adjacent faces of similar tone still
5301
     read as separate people. */
5302
  .contributor-cluster .avatar {
5303
    border: 1px solid var(--ink-void);
5304
  }
5305
5306
  /* Wide enough to type a path into and no wider: the ref bar is a row of small
5307
     controls, and a search field that fills it reads as the subject of the bar
5308
     rather than one control in it. */
5309
  .file-table__search {
5310
    width: 100%;
5311
    max-width: 220px;
5312
  }
5313
5128 5314
  /* One bar rather than a stack: the proportions are the point, and they only
5129 5315
     read as proportions when the segments share a length. */
5130 5316
  .language-bar {
lib/openagents_web/component_catalog.ex modified +14

@@ -184,6 +184,20 @@ defmodule OpenAgentsWeb.ComponentCatalog do

184 184
          source: "OpenAgentsWeb.UI.repo_about/1",
185 185
          summary: "The rail beside a repository: description, licence, languages."
186 186
        },
187
        %{
188
          slug: "openagents-repo-tabs",
189
          title: "Repo tabs",
190
          icon: "category",
191
          source: "OpenAgentsWeb.UI.repo_tabs/1",
192
          summary: "A repository's sections as links, the current one marked by aria-current."
193
        },
194
        %{
195
          slug: "openagents-repo-view",
196
          title: "Repository view",
197
          icon: "folders",
198
          source: "OpenAgentsWeb.UI.repo_view/1",
199
          summary: "The whole repository home: identity, sections, tree, and rail in one frame."
200
        },
187 201
        %{
188 202
          slug: "openagents-status-indicator",
189 203
          title: "Status indicator",
lib/openagents_web/components/ui.ex modified +174 -5

@@ -818,6 +818,51 @@ defmodule OpenAgentsWeb.UI do

818 818
    """
819 819
  end
820 820
821
  @doc """
822
  The section bar under a repository's name: Code, Issues, Pull requests, and
823
  whatever else that repository publishes.
824
825
  Page navigation rather than a tab widget. Every entry changes the URL, so the
826
  selected one carries `aria-current="page"` and none of them carries a tab
827
  role -- a tab role promises panels that swap in place, and a reader who takes
828
  that promise and reaches for the arrow keys gets nothing.
829
830
  A count lives in its own element rather than inside the label, so "Issues"
831
  stays findable by that word alone and the number can be toned down, or
832
  dropped at a narrow width, without rewriting the string.
833
  """
834
  attr :label, :string, default: "Repository sections"
835
  attr :class, :any, default: nil
836
  attr :rest, :global
837
838
  slot :tab, required: true do
839
    attr :icon, :string
840
    attr :count, :integer
841
    attr :current, :boolean
842
    attr :navigate, :string
843
    attr :patch, :string
844
    attr :href, :string
845
  end
846
847
  def repo_tabs(assigns) do
848
    ~H"""
849
    <nav class={["repo-tabs", @class]} aria-label={@label} {@rest}>
850
      <.link
851
        :for={tab <- @tab}
852
        navigate={tab[:navigate]}
853
        patch={tab[:patch]}
854
        href={tab[:href]}
855
        aria-current={tab[:current] && "page"}
856
        class="repo-tabs__tab"
857
      >
858
        <.icon :if={tab[:icon]} name={tab.icon} />
859
        <span class="repo-tabs__label">{render_slot(tab)}</span>
860
        <span :if={tab[:count]} class="repo-tabs__count">{tab.count}</span>
861
      </.link>
862
    </nav>
863
    """
864
  end
865
821 866
  @doc """
822 867
  A repository's file table: the ref bar, the latest commit, and the entries.
823 868

@@ -835,22 +880,34 @@ defmodule OpenAgentsWeb.UI do

835 880
  looks like a row is the reason GitHub's own version of this reads oddly on
836 881
  first sight.
837 882
838
  Per-row commit messages are deliberately absent. GitHub fills them by
839
  walking history once per path, which is one process per file; when we have
840
  that cheaply the slot is `meta` and it costs no markup change.
883
  Per-row commit messages are optional, because GitHub fills them by walking
884
  history once per path, which is one process per file. An entry that carries
885
  `message` and `updated` gets them; when no entry does, those two columns are
886
  not rendered at all rather than emitted empty, so the cheap tree keeps the
887
  markup it already had.
841 888
  """
842 889
  attr :owner, :string, required: true
843 890
  attr :repo, :string, required: true
844 891
  attr :ref, :string, required: true
845 892
  attr :path, :string, default: ""
846
  attr :entries, :list, required: true, doc: "`[%{name, kind, size}]` from `Browse.tree/3`"
893
894
  attr :entries, :list,
895
    required: true,
896
    doc:
897
      "`[%{name, kind, size}]` from `Browse.tree/3`, each optionally carrying " <>
898
        "`message` and `updated` from that path's last commit"
899
847 900
  attr :branches, :integer, default: nil
848 901
  attr :tags, :integer, default: nil
902
  attr :commits, :integer, default: nil, doc: "commits on this ref, shown beside the latest one"
849 903
  attr :class, :any, default: nil
850 904
  slot :commit, doc: "the latest commit, shown above the table"
851 905
  slot :actions, doc: "controls at the trailing edge of the ref bar"
852 906
853 907
  def file_table(assigns) do
908
    assigns =
909
      assign(assigns, :history?, Enum.any?(assigns.entries, &(&1[:message] || &1[:updated])))
910
854 911
    ~H"""
855 912
    <div class={["file-table", @class]}>
856 913
      <div class="file-table__bar">

@@ -866,7 +923,13 @@ defmodule OpenAgentsWeb.UI do

866 923
        <span :if={@actions != []} class="file-table__actions">{render_slot(@actions)}</span>
867 924
      </div>
868 925
869
      <div :if={@commit != []} class="file-table__commit">{render_slot(@commit)}</div>
926
      <div :if={@commit != [] or @commits} class="file-table__commit">
927
        {render_slot(@commit)}
928
        <span :if={@commits} class="file-table__commits">
929
          <.icon name="history" />
930
          <strong>{@commits}</strong> {plural(@commits, "Commit", "Commits")}
931
        </span>
932
      </div>
870 933
871 934
      <table class="file-table__list">
872 935
        <caption class="visually-hidden">

@@ -880,7 +943,11 @@ defmodule OpenAgentsWeb.UI do

880 943
                {entry.name}
881 944
              </.link>
882 945
            </td>
946
            <td :if={@history?} class="file-row__message" title={entry[:message]}>
947
              {entry[:message]}
948
            </td>
883 949
            <td class="file-row__size">{size_label(entry)}</td>
950
            <td :if={@history?} class="file-row__age">{entry[:updated]}</td>
884 951
          </tr>
885 952
        </tbody>
886 953
      </table>

@@ -926,6 +993,11 @@ defmodule OpenAgentsWeb.UI do

926 993
  """
927 994
  attr :description, :string, default: nil
928 995
  attr :license, :string, default: nil
996
997
  attr :contributors, :integer,
998
    default: nil,
999
    doc: "the total, when more people committed than there are faces to show"
1000
929 1001
  attr :class, :any, default: nil
930 1002
931 1003
  slot :link, doc: "one related destination" do

@@ -938,11 +1010,23 @@ defmodule OpenAgentsWeb.UI do

938 1010
    attr :icon, :string
939 1011
  end
940 1012
1013
  slot :contributor, doc: "one contributor, drawn as a face" do
1014
    attr :name, :string, required: true
1015
    attr :src, :string
1016
  end
1017
941 1018
  slot :language, doc: "one language" do
942 1019
    attr :percent, :float, required: true
943 1020
  end
944 1021
945 1022
  def repo_about(assigns) do
1023
    assigns =
1024
      assign(
1025
        assigns,
1026
        :overflow,
1027
        max((assigns.contributors || 0) - length(assigns.contributor), 0)
1028
      )
1029
946 1030
    ~H"""
947 1031
    <aside class={["repo-about", @class]} aria-label="About this repository">
948 1032
      <h2 class="repo-about__title">About</h2>

@@ -965,6 +1049,25 @@ defmodule OpenAgentsWeb.UI do

965 1049
        </li>
966 1050
      </ul>
967 1051
1052
      <div :if={@contributor != []} class="repo-about__contributors">
1053
        <h3>
1054
          Contributors <span class="repo-about__count">{@contributors || length(@contributor)}</span>
1055
        </h3>
1056
        <%!-- The count is the point. Six faces alone says the repository has
1057
        six contributors, which is usually wrong. --%>
1058
        <ul class="contributor-cluster">
1059
          <li :for={person <- @contributor}>
1060
            <.avatar
1061
              src={person[:src]}
1062
              fallback={String.first(person.name)}
1063
              size={:sm}
1064
              label={person.name}
1065
            />
1066
          </li>
1067
          <li :if={@overflow > 0} class="contributor-cluster__count">+{@overflow}</li>
1068
        </ul>
1069
      </div>
1070
968 1071
      <div :if={@language != []} class="repo-about__languages">
969 1072
        <h3>Languages</h3>
970 1073
        <%!-- One bar, not a stack of bars: the proportions are the point, and

@@ -988,6 +1091,72 @@ defmodule OpenAgentsWeb.UI do

988 1091
    """
989 1092
  end
990 1093
1094
  @doc """
1095
  A repository's home page, assembled.
1096
1097
  The pieces already exist on their own -- `breadcrumb/1` for the owner trail,
1098
  `repo_tabs/1` for the sections, `file_table/1` for the tree, `repo_about/1`
1099
  for the rail. This holds them in one frame so that a surface showing a
1100
  repository does not reassemble that frame by hand and drift from the next
1101
  surface that shows one.
1102
1103
  The rail is a second grid column above 1024px and falls below the tree under
1104
  it. Provenance -- what this is, how it is licensed, who wrote it -- is what a
1105
  reader wants beside the file list on a desktop and after it on a phone, and
1106
  the source order is already the phone order.
1107
1108
  Composition is by slot rather than by attribute, so this owns the frame and
1109
  nothing else: a caller that needs a tree with no rail, or a commit list where
1110
  the tree usually goes, passes that instead without a flag being added here.
1111
  """
1112
  attr :owner, :string, required: true
1113
  attr :repo, :string, required: true
1114
1115
  attr :owner_path, :string,
1116
    default: nil,
1117
    doc: "where the owner's name leads; `/OWNER` by default"
1118
1119
  attr :visibility, :atom, values: [:public, :private], default: :public
1120
  attr :class, :any, default: nil
1121
  attr :rest, :global
1122
1123
  slot :tabs, doc: "the section bar, normally one `repo_tabs/1`"
1124
  slot :inner_block, required: true, doc: "the main column, normally one `file_table/1`"
1125
  slot :about, doc: "the trailing rail, normally one `repo_about/1`"
1126
1127
  def repo_view(assigns) do
1128
    assigns = assign(assigns, :owner_path, assigns.owner_path || "/#{assigns.owner}")
1129
1130
    ~H"""
1131
    <div class={["repo-page", @class]} {@rest}>
1132
      <header class="repo-page__identity">
1133
        <%!-- Decorative: the owner's name is the next thing in the trail, and an
1134
        initial announced ahead of it reads as a stray letter. --%>
1135
        <.avatar
1136
          fallback={String.upcase(String.first(@owner))}
1137
          tone={:accent}
1138
          aria-hidden="true"
1139
        />
1140
        <.breadcrumb class="repo-page__trail" label={"#{@owner} / #{@repo}"}>
1141
          <:item navigate={@owner_path}>{@owner}</:item>
1142
          <:item>{@repo}</:item>
1143
        </.breadcrumb>
1144
        <.badge variant={:dim}>{visibility_label(@visibility)}</.badge>
1145
      </header>
1146
1147
      {render_slot(@tabs)}
1148
1149
      <div class="repo-view">
1150
        <div class="repo-view__main">{render_slot(@inner_block)}</div>
1151
        <div :if={@about != []} class="repo-view__rail">{render_slot(@about)}</div>
1152
      </div>
1153
    </div>
1154
    """
1155
  end
1156
1157
  defp visibility_label(:private), do: "Private"
1158
  defp visibility_label(_visibility), do: "Public"
1159
991 1160
  @doc """
992 1161
  One file's diff: a header, its hunks, and every line numbered on both sides.
993 1162
lib/openagents_web/live/components_live.ex modified +214

@@ -104,6 +104,95 @@ defmodule OpenAgentsWeb.ComponentsLive do

104 104
    %{name: "Lena Fischer"}
105 105
  ]
106 106
107
  # This repository's own root, in the order `Browse.tree/3` returns it:
108
  # directories above files, each group by name. The message and age on a row
109
  # are what `git log -1 -- <path>` says about that path, and the sizes are the
110
  # real ones. The composed repository demo is only worth reading if the
111
  # composition is carrying real proportions -- invented data makes every
112
  # column look comfortable.
113
  @demo_repo_entries [
114
    %{
115
      name: "assets",
116
      kind: "tree",
117
      size: nil,
118
      message: "Delete the CONNECTED bar from the conversation",
119
      updated: "3 hours ago"
120
    },
121
    %{
122
      name: "config",
123
      kind: "tree",
124
      size: nil,
125
      message: "Connect staging fleet through Cloud SQL Auth Proxy",
126
      updated: "1 hour ago"
127
    },
128
    %{
129
      name: "docs",
130
      kind: "tree",
131
      size: nil,
132
      message: "Specify repository creation and the OpenAgents CLI",
133
      updated: "18 minutes ago"
134
    },
135
    %{
136
      name: "infra",
137
      kind: "tree",
138
      size: nil,
139
      message: "Connect staging fleet through Cloud SQL Auth Proxy",
140
      updated: "1 hour ago"
141
    },
142
    %{
143
      name: "lib",
144
      kind: "tree",
145
      size: nil,
146
      message: "Group the agent's surfaces under her name",
147
      updated: "15 minutes ago"
148
    },
149
    %{
150
      name: "priv",
151
      kind: "tree",
152
      size: nil,
153
      message: "Restore the power mark from v4 and v5 as the favicon",
154
      updated: "3 hours ago"
155
    },
156
    %{
157
      name: "test",
158
      kind: "tree",
159
      size: nil,
160
      message: "Give the modal scrim a token, and drop the guard's exception",
161
      updated: "5 hours ago"
162
    },
163
    %{
164
      name: ".formatter.exs",
165
      kind: "blob",
166
      size: 225,
167
      message: "Initialize Phoenix 1.8 application with latest dependencies",
168
      updated: "yesterday"
169
    },
170
    %{
171
      name: "AGENTS.md",
172
      kind: "blob",
173
      size: 28_064,
174
      message: "Authorize the theme bootstrap with a CSP nonce",
175
      updated: "9 hours ago"
176
    },
177
    %{
178
      name: "mix.exs",
179
      kind: "blob",
180
      size: 4_252,
181
      message: "Publish immutable staging candidate artifacts",
182
      updated: "9 hours ago"
183
    },
184
    %{
185
      name: "README.md",
186
      kind: "blob",
187
      size: 5_527,
188
      message: "Build isolated staging infrastructure",
189
      updated: "10 hours ago"
190
    }
191
  ]
192
193
  # Everyone `git shortlog -sn` names on this repository, most commits first.
194
  @demo_repo_contributors [%{name: "AtlantisPleb"}, %{name: "Christopher David"}]
195
107 196
  # The catalog demonstrates the preferred vendored Apps SDK tier. Heroicons
108 197
  # remains an exceptional fallback with an empty product-use inventory.
109 198
  @openagents_icons ~w(sparkle compass folder document user bell play star)

@@ -1127,6 +1216,131 @@ defmodule OpenAgentsWeb.ComponentsLive do

1127 1216
    """
1128 1217
  end
1129 1218
1219
  defp component_demo(%{item: %{slug: "openagents-repo-tabs"}} = assigns) do
1220
    ~H"""
1221
    <div class="space-y-3">
1222
      <p class="text-sm text-base-content/60">
1223
        Seven links, not seven tabs. Each one changes the URL, so the selected entry
1224
        is marked with <code>aria-current="page"</code>
1225
        and the underline follows that attribute. Giving these a tab role would promise
1226
        panels that swap in place, and a reader who accepted the promise and reached for
1227
        the arrow keys would get nothing.
1228
      </p>
1229
      <UI.repo_tabs>
1230
        <:tab icon="code" href="#code" current>Code</:tab>
1231
        <:tab icon="empty-circle" href="#issues" count={12}>Issues</:tab>
1232
        <:tab icon="pull-request-open" href="#pulls" count={3}>Pull requests</:tab>
1233
        <:tab icon="cube" href="#projects">Projects</:tab>
1234
        <:tab icon="book-open" href="#wiki">Wiki</:tab>
1235
        <:tab icon="chart" href="#insights">Insights</:tab>
1236
        <:tab icon="settings-cog" href="#settings">Settings</:tab>
1237
      </UI.repo_tabs>
1238
      <p class="text-sm text-base-content/60">
1239
        The count sits in its own element rather than inside the word, so "Issues" stays
1240
        findable by that word alone, the number keeps its own weight, and a narrow screen
1241
        can drop it without anyone rewriting the label. The destinations here are page
1242
        anchors, because the catalog has nowhere real to send you.
1243
      </p>
1244
    </div>
1245
    """
1246
  end
1247
1248
  defp component_demo(%{item: %{slug: "openagents-repo-view"}} = assigns) do
1249
    assigns =
1250
      assigns
1251
      |> assign(:entries, @demo_repo_entries)
1252
      |> assign(:contributors, @demo_repo_contributors)
1253
1254
    ~H"""
1255
    <div class="space-y-3">
1256
      <p class="text-sm text-base-content/60">
1257
        Everything above, assembled: the owner trail from <code>breadcrumb/1</code>, the sections from <code>repo_tabs/1</code>, the tree from <code>file_table/1</code>, and the rail from <code>repo_about/1</code>. The frame
1258
        itself is the only thing this component owns, which is why the three pieces arrive
1259
        as slots — a surface that wants a commit list where the tree usually goes passes
1260
        one, and no flag is added here.
1261
      </p>
1262
      <div class="-mx-6">
1263
        <UI.repo_view
1264
          owner="OpenAgentsInc"
1265
          repo="openagents.com"
1266
          owner_path={~p"/components/openagents-repo-view"}
1267
        >
1268
          <:tabs>
1269
            <UI.repo_tabs>
1270
              <:tab icon="code" href="#code" current>Code</:tab>
1271
              <:tab icon="empty-circle" href="#issues" count={12}>Issues</:tab>
1272
              <:tab icon="pull-request-open" href="#pulls" count={3}>Pull requests</:tab>
1273
              <:tab icon="cube" href="#projects">Projects</:tab>
1274
              <:tab icon="book-open" href="#wiki">Wiki</:tab>
1275
              <:tab icon="chart" href="#insights">Insights</:tab>
1276
              <:tab icon="settings-cog" href="#settings">Settings</:tab>
1277
            </UI.repo_tabs>
1278
          </:tabs>
1279
1280
          <UI.file_table
1281
            owner="OpenAgentsInc"
1282
            repo="openagents.com"
1283
            ref="main"
1284
            entries={@entries}
1285
            branches={2}
1286
            tags={0}
1287
            commits={234}
1288
          >
1289
            <:actions>
1290
              <UI.input
1291
                id="demo-go-to-file"
1292
                type="text"
1293
                name="go-to-file"
1294
                placeholder="Go to file"
1295
                aria-label="Go to file"
1296
                class="input file-table__search"
1297
              />
1298
              <UI.button size={:sm}>Add file</UI.button>
1299
              <UI.button variant={:primary} size={:sm}>Code</UI.button>
1300
            </:actions>
1301
            <:commit>
1302
              <UI.avatar size={:sm} fallback="A" label="AtlantisPleb" />
1303
              <strong>AtlantisPleb</strong>
1304
              <span>Group the agent's surfaces under her name</span>
1305
              <code>86416fc</code>
1306
              <span>15 minutes ago</span>
1307
            </:commit>
1308
          </UI.file_table>
1309
1310
          <:about>
1311
            <UI.repo_about description="The Agent Forge" license="AGPL-3.0" contributors={2}>
1312
              <:link icon="link" href="https://openagents.com">openagents.com</:link>
1313
              <:link icon="book" navigate="/docs">Readme</:link>
1314
              <:link icon="text" navigate="/changelog">Activity</:link>
1315
              <:stat icon="star">2 stars</:stat>
1316
              <:stat icon="eye">0 watching</:stat>
1317
              <:stat icon="branch">0 forks</:stat>
1318
              <:contributor :for={person <- @contributors} name={person.name} />
1319
              <:language percent={91.8}>Elixir</:language>
1320
              <:language percent={3.1}>CSS</:language>
1321
              <:language percent={3.1}>Shell</:language>
1322
              <:language percent={1.0}>JavaScript</:language>
1323
              <:language percent={1.0}>Other</:language>
1324
            </UI.repo_about>
1325
          </:about>
1326
        </UI.repo_view>
1327
      </div>
1328
      <p class="text-sm text-base-content/60">
1329
        The tree, the commit subjects, the branch, the contributors, and the commit count
1330
        are this repository's own — each row's message is what <code>git log -1 -- &lt;path&gt;</code>
1331
        says about that path. A composed demo is only worth reading if the composition is
1332
        carrying real proportions; invented ones make every column look comfortable.
1333
      </p>
1334
      <p class="text-sm text-base-content/60">
1335
        The rail becomes a second column above 1024px and falls below the tree under it.
1336
        Source order is already the narrow order, so nothing reorders: what a repository
1337
        is, how it is licensed, and who wrote it are what a reader wants beside the file
1338
        list on a desktop and after it on a phone.
1339
      </p>
1340
    </div>
1341
    """
1342
  end
1343
1130 1344
  defp component_demo(%{item: %{slug: "openagents-diff-file"}} = assigns) do
1131 1345
    assigns =
1132 1346
      assign(
test/openagents_web/live/components_live_test.exs modified +19

@@ -73,6 +73,25 @@ defmodule OpenAgentsWeb.ComponentsLiveTest do

73 73
    assert has_element?(view, "#component-form")
74 74
  end
75 75
76
  test "the repository view page composes trail, sections, tree, and rail", %{conn: conn} do
77
    {:ok, view, _html} = live(conn, ~p"/components/openagents-repo-view")
78
79
    assert has_element?(view, ".repo-page .breadcrumb__current", "openagents.com")
80
    assert has_element?(view, ~s{.repo-tabs__tab[aria-current="page"]}, "Code")
81
    assert has_element?(view, ".repo-view__main .file-table .file-row__message")
82
    assert has_element?(view, ".repo-view__rail .repo-about__contributors")
83
  end
84
85
  test "repository sections are links, not a tab widget", %{conn: conn} do
86
    {:ok, view, _html} = live(conn, ~p"/components/openagents-repo-tabs")
87
88
    # A tab role promises panels that swap in place. These change the URL, so
89
    # the current one is marked by aria-current and nothing claims to be a tab.
90
    assert has_element?(view, "nav.repo-tabs[aria-label]")
91
    refute has_element?(view, ~s{.repo-tabs [role="tab"]})
92
    assert has_element?(view, ".repo-tabs__tab .repo-tabs__count", "12")
93
  end
94
76 95
  test "the catalog exposes no nonfunctional theme control", %{conn: conn} do
77 96
    {:ok, view, _html} = live(conn, ~p"/components")
78 97

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