Render issue state with GitHub's octicons and state colors

8514ad950d67 · AtlantisPleb · · parent 45f6fff3222c

Render issue state with GitHub's octicons and state colors

Issue rows, headers, and close events drew Linear-heritage shapes: an open
issue showed the empty-circle glyph in muted ink and a completed close a
green check. GitHub-compatible state is domain vocabulary, so issues now
read like GitHub everywhere the shared presentation components draw them:

- An open issue takes `octicon-issue-opened`, tinted with the success token.
- A completed close takes `octicon-issue-closed`, tinted with a new `--done`
  token (GitHub's purple, light and dark values both defined).
- Not-planned keeps the cancelled glyph; triage categories are untouched.

The glyphs come from Primer Octicons (MIT), vendored verbatim into
`priv/octicons` at a pinned commit and embedded by OpenAgentsWeb.Icons under
`octicon-*` names. docs/ICONS.md records why neither governed tier could
carry them and governs the new source accordingly.

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 assets/css/openagents.css
  • modified docs/ICONS.md
  • modified lib/openagents_web/components/circle.ex
  • modified lib/openagents_web/components/issue_presentation.ex
  • modified lib/openagents_web/icons.ex
  • modified lib/openagents_web/live/issue_show_live.ex
  • added priv/octicons/README.md
  • added priv/octicons/issue-closed.svg
  • added priv/octicons/issue-opened.svg
  • modified test/openagents_web/components/circle_test.exs
  • modified test/openagents_web/icons_test.exs

Diff

12 files changed, +91 -15

assets/css/app.css modified +2

@@ -106,6 +106,7 @@

106 106
  --accent-bright: #828fff;
107 107
  --info: #4ea7fc;
108 108
  --success: #27a644;
109
  --done: #ab7df8; /* GitHub's closed-as-completed purple */
109 110
  --warning: #d4b144;
110 111
  --danger: #eb5757;
111 112

@@ -176,6 +177,7 @@

176 177
     hover/visited state rather than the resting one. */
177 178
  --accent: #5e6ad2;
178 179
  --accent-bright: #4a55b8;
180
  --done: #8250df; /* GitHub's light-theme closed-as-completed purple */
179 181
180 182
  --text-body: #3c4149;
181 183
  --text-muted: #62666d;
assets/css/openagents.css modified +6 -1

@@ -5679,9 +5679,14 @@

5679 5679
    --issue-tint: var(--info);
5680 5680
  }
5681 5681
5682
  .issue-status[data-category="open"],
5683
  .issue-group[data-category="open"] {
5684
    --issue-tint: var(--success);
5685
  }
5686
5682 5687
  .issue-status[data-category="completed"],
5683 5688
  .issue-group[data-category="completed"] {
5684
    --issue-tint: var(--success);
5689
    --issue-tint: var(--done);
5685 5690
  }
5686 5691
5687 5692
  .issue-status {
docs/ICONS.md modified +18

@@ -49,6 +49,24 @@ identify the service reached by an action. Render it through `icon/1` with a

49 49
`brand-*` name, and follow the attribution and trademark rules in
50 50
`priv/brand/README.md`.
51 51
52
## Octicons
53
54
Issue-state glyphs live in `priv/octicons`, vendored verbatim from
55
[Primer Octicons](https://github.com/primer/octicons) (MIT) at the commit
56
recorded in `priv/octicons/README.md`. They render through `icon/1` under
57
`octicon-*` names.
58
59
OpenAgents is a GitHub-compatible forge, so issue state is domain vocabulary:
60
open takes `octicon-issue-opened` and a completed close takes
61
`octicon-issue-closed`. Neither governed tier carries those concepts — the Apps
62
SDK set has no issue glyph, and Heroicons has no circle-dot. State color comes
63
from palette tokens at the call site (`--success` for open, `--done` for
64
closed), never from the file.
65
66
Adding or upgrading an octicon follows the same rule as every other tier:
67
copy the upstream file at the pinned commit, update the README's commit, date,
68
and count, and run `mix precommit`.
69
52 70
## Accessibility
53 71
54 72
A glyph beside visible words is decorative and needs no label. Put
lib/openagents_web/components/circle.ex modified +8 -3

@@ -46,7 +46,7 @@ defmodule OpenAgentsWeb.UI.Circle do

46 46
  alias OpenAgentsWeb.UI
47 47
  alias Phoenix.LiveView.JS
48 48
49
  @categories [:triage, :backlog, :unstarted, :started, :completed, :canceled]
49
  @categories [:open, :triage, :backlog, :unstarted, :started, :completed, :canceled]
50 50
  @priorities [:none, :low, :medium, :high, :urgent]
51 51
  @tones [:neutral, :primary, :info, :success, :warning, :danger]
52 52
  @presences [:none, :online, :away, :offline]

@@ -1261,7 +1261,7 @@ defmodule OpenAgentsWeb.UI.Circle do

1261 1261
    do: :canceled
1262 1262
1263 1263
  defp state_category("closed", _reason), do: :completed
1264
  defp state_category(_state, _reason), do: :unstarted
1264
  defp state_category(_state, _reason), do: :open
1265 1265
1266 1266
  defp state_label("closed", "not_planned"), do: "Closed as not planned"
1267 1267
  defp state_label("closed", "duplicate"), do: "Closed as duplicate"

@@ -1272,10 +1272,15 @@ defmodule OpenAgentsWeb.UI.Circle do

1272 1272
  # opposing arrows in a disc, which `compare-arrows` says exactly; the dashed
1273 1273
  # gear it uses for backlog has no equivalent and `circle-dashed` carries the
1274 1274
  # same "not yet real" reading without vendoring a glyph for one state.
1275
  # GitHub's issue-state glyphs. Open is the green circle-dot and a completed
1276
  # close is the purple check-circle; the other categories keep the triage
1277
  # glyphs because GitHub itself has no opinion about them.
1278
  defp category_icon(:open), do: "octicon-issue-opened"
1275 1279
  defp category_icon(:triage), do: "compare-arrows"
1276 1280
  defp category_icon(:backlog), do: "circle-dashed"
1277 1281
  defp category_icon(:unstarted), do: "empty-circle"
1278
  defp category_icon(:completed), do: "check-circle-filled"
1282
  # :started draws the conic-gradient arc in CSS, never a glyph.
1283
  defp category_icon(:completed), do: "octicon-issue-closed"
1279 1284
  defp category_icon(:canceled), do: "x-circle-filled"
1280 1285
1281 1286
  defp priority_name(:none), do: "No priority"
lib/openagents_web/components/issue_presentation.ex modified +5 -4

@@ -55,14 +55,15 @@ defmodule OpenAgentsWeb.Components.IssuePresentation do

55 55
  end
56 56
57 57
  @doc """
58
  GitHub's two states, and nothing invented on top of them.
58
  GitHub's state iconography, and nothing invented on top of it.
59 59
60
  `not_planned` is the one close reason with a distinct reading, so it takes
61
  the cancelled glyph; every other close is a completion.
60
  An open issue takes the green circle-dot. `not_planned` is the one close
61
  reason with a distinct reading, so it keeps the cancelled glyph; every other
62
  close is the purple check-circle.
62 63
  """
63 64
  def category(%{state: "closed", state_reason: "not_planned"}), do: :canceled
64 65
  def category(%{state: "closed"}), do: :completed
65
  def category(_issue), do: :unstarted
66
  def category(_issue), do: :open
66 67
67 68
  @doc "The word beside the glyph, for assistive technology and for a label."
68 69
  def status_label(%{state: "closed", state_reason: "not_planned"}), do: "Closed as not planned"
lib/openagents_web/icons.ex modified +13 -2

@@ -18,8 +18,12 @@ defmodule OpenAgentsWeb.Icons do

18 18
  # generic icon library. They are namespaced `brand-*`. See priv/brand/README.md.
19 19
  @brand_dir Path.join([__DIR__, "..", "..", "priv", "brand"]) |> Path.expand()
20 20
21
  # Issue-state glyphs from Primer Octicons. See priv/octicons/README.md.
22
  @octicons_dir Path.join([__DIR__, "..", "..", "priv", "octicons"]) |> Path.expand()
23
21 24
  @paths (@icons_dir |> Path.join("*.svg") |> Path.wildcard()) ++
22
           (@brand_dir |> Path.join("*.svg") |> Path.wildcard())
25
           (@brand_dir |> Path.join("*.svg") |> Path.wildcard()) ++
26
           (@octicons_dir |> Path.join("*.svg") |> Path.wildcard())
23 27
24 28
  for path <- @paths do
25 29
    @external_resource path

@@ -27,7 +31,14 @@ defmodule OpenAgentsWeb.Icons do

27 31
28 32
  @icons Map.new(@paths, fn path ->
29 33
           base = Path.basename(path, ".svg")
30
           name = if Path.dirname(path) == @brand_dir, do: "brand-" <> base, else: base
34
35
           name =
36
             cond do
37
               Path.dirname(path) == @brand_dir -> "brand-" <> base
38
               Path.dirname(path) == @octicons_dir -> "octicon-" <> base
39
               true -> base
40
             end
41
31 42
           contents = path |> File.read!() |> String.trim()
32 43
33 44
           view_box =
lib/openagents_web/live/issue_show_live.ex modified +1 -1

@@ -596,7 +596,7 @@ defmodule OpenAgentsWeb.IssueShowLive do

596 596
  defp close_icon(%{state_reason: reason}) when reason in ["not_planned", "duplicate"],
597 597
    do: "x-circle-filled"
598 598
599
  defp close_icon(_issue), do: "check-circle-filled"
599
  defp close_icon(_issue), do: "octicon-issue-closed"
600 600
601 601
  defp close_tone(%{state_reason: reason}) when reason in ["not_planned", "duplicate"],
602 602
    do: :danger
priv/octicons/README.md added +29

@@ -0,0 +1,29 @@

1
# Octicons (vendored)
2
3
Generated files. **Do not edit these by hand.**
4
5
- Upstream: [primer/octicons](https://github.com/primer/octicons)
6
- Commit: `0e21a4c2d8449102f10e533d241f04797af0914c`
7
- License: MIT, Copyright GitHub, Inc. — see upstream repository
8
- Vendored: 2026-08-22
9
- Count: 2 glyphs
10
11
## Why this set exists
12
13
OpenAgents is a GitHub-compatible forge. Issue state iconography is a domain
14
contract — the green circle-dot for open and the purple check-circle for closed
15
are what users expect on an issues surface — and neither governed tier carries
16
it:
17
18
- The Apps SDK UI set (`priv/icons`) has no issue-state glyph.
19
- Heroicons (the `hero-*` fallback) has no circle-dot and its check-circle
20
  reads as success feedback, not issue state.
21
22
These two glyphs are vendored verbatim from Primer Octicons at the pinned
23
commit above. To add or upgrade one, copy the matching `*-16.svg` from the
24
upstream checkout, update the commit, date, and count here, update
25
`docs/ICONS.md`, and run `mix precommit`.
26
27
`OpenAgentsWeb.Icons` embeds these files under `octicon-*` names, so call
28
sites render `<.icon name="octicon-issue-opened" />`. State color comes from
29
palette tokens at the call site, never from the file.
priv/octicons/issue-closed.svg added +1

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

1
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M11.28 6.78a.75.75 0 0 0-1.06-1.06L7.25 8.69 5.78 7.22a.75.75 0 0 0-1.06 1.06l2 2a.75.75 0 0 0 1.06 0l3.5-3.5Z"/><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0Zm-1.5 0a6.5 6.5 0 1 0-13 0 6.5 6.5 0 0 0 13 0Z"/></svg>
priv/octicons/issue-opened.svg added +1

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

1
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"/><path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Z"/></svg>
test/openagents_web/components/circle_test.exs modified +2 -2

@@ -93,7 +93,7 @@ defmodule OpenAgentsWeb.UI.CircleTest do

93 93
    end
94 94
95 95
    test "every category resolves to a shape" do
96
      for category <- [:triage, :backlog, :unstarted, :started, :completed, :canceled] do
96
      for category <- [:open, :triage, :backlog, :unstarted, :started, :completed, :canceled] do
97 97
        rendered = status(category: category, label: "Any", progress: 50)
98 98
99 99
        assert query(rendered, ~s{.issue-status[data-category="#{category}"]}) != []

@@ -214,7 +214,7 @@ defmodule OpenAgentsWeb.UI.CircleTest do

214 214
    # looks like.
215 215
    test "GitHub's two states, plus the one close reason that reads differently" do
216 216
      for {state, reason, category, label} <- [
217
            {"open", nil, "unstarted", "Open"},
217
            {"open", nil, "open", "Open"},
218 218
            {"closed", "completed", "completed", "Closed"},
219 219
            {"closed", nil, "completed", "Closed"},
220 220
            {"closed", "not_planned", "canceled", "Closed as not planned"},
test/openagents_web/icons_test.exs modified +5 -2

@@ -27,12 +27,15 @@ defmodule OpenAgentsWeb.IconsTest do

27 27
    test "is present and complete" do
28 28
      # 755 vendored glyphs plus the brand marks, which live apart because a
29 29
      # logo cannot come from a generic set and because the vendoring task
30
      # clears priv/icons on every run.
31
      {brand, vendored} = Enum.split_with(Icons.names(), &String.starts_with?(&1, "brand-"))
30
      # clears priv/icons on every run, plus the octicon state glyphs in
31
      # priv/octicons, which are governed by their own pinned upstream.
32
      {brand, rest} = Enum.split_with(Icons.names(), &String.starts_with?(&1, "brand-"))
33
      {octicons, vendored} = Enum.split_with(rest, &String.starts_with?(&1, "octicon-"))
32 34
33 35
      assert length(vendored) == 755
34 36
      assert brand != []
35 37
      assert "brand-github" in brand
38
      assert octicons == ["octicon-issue-closed", "octicon-issue-opened"]
36 39
      assert length(Icons.names()) == Icons.count()
37 40
    end
38 41

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