| 1 | diff --git a/internal/webui/collaboration.go b/internal/webui/collaboration.go |
| 2 | index 0e2e2c3..06f8afe 100644 |
| 3 | --- a/internal/webui/collaboration.go |
| 4 | +++ b/internal/webui/collaboration.go |
| 5 | @@ -20,6 +20,32 @@ func collaborationSlice(value any, field string) []any { |
| 6 | func collaborationItems(value any) []any { return collaborationSlice(value, "items") } |
| 7 | func collaborationReplies(value any) []any { return collaborationSlice(value, "replies") } |
| 8 | |
| 9 | +// collaborationProposalView is how an issue, pull request or comment's |
| 10 | +// proposal state reads for one viewer, with what the decision buttons need. |
| 11 | +// Shown, Decide, State, At and By follow wikiProposalView. |
| 12 | +type collaborationProposalView struct { |
| 13 | + wikiProposalView |
| 14 | + ID, Author, Kind string |
| 15 | +} |
| 16 | + |
| 17 | +// collaborationProposal reads an item's proposal state for the viewer. The |
| 18 | +// item is a list or detail item, whose author is under author, or a reply, |
| 19 | +// whose author is under pubkey. canApprove is the browse result's |
| 20 | +// can_approve flag. |
| 21 | +func collaborationProposal(item any, actor string, canApprove any) collaborationProposalView { |
| 22 | + m := valueMap(item) |
| 23 | + if m == nil || m["proposal"] != true { |
| 24 | + return collaborationProposalView{} |
| 25 | + } |
| 26 | + author := plainString(m["author"]) |
| 27 | + if author == "" { |
| 28 | + author = plainString(m["pubkey"]) |
| 29 | + } |
| 30 | + view := collaborationProposalView{ID: plainString(m["id"]), Author: author, Kind: plainString(m["kind"])} |
| 31 | + view.wikiProposalView = wikiProposal(map[string]any{"proposal": true, "author": author, "approval": m["approval"], "approval_at": m["approval_at"], "approval_by": m["approval_by"]}, actor, canApprove) |
| 32 | + return view |
| 33 | +} |
| 34 | + |
| 35 | func collaborationLabels(value any) []string { |
| 36 | var labels []string |
| 37 | encoded, err := json.Marshal(valueMap(value)["labels"]) |
| 38 | diff --git a/internal/webui/collaboration.html b/internal/webui/collaboration.html |
| 39 | index 34fcdc2..1647cd9 100644 |
| 40 | --- a/internal/webui/collaboration.html |
| 41 | +++ b/internal/webui/collaboration.html |
| 42 | @@ -8,7 +8,7 @@ |
| 43 | </form> |
| 44 | {{$items := collabItems .Event}}{{if $items}}<table id="rows"> |
| 45 | <thead><tr><th>status</th><th>title</th><th>author</th><th>opened</th></tr></thead> |
| 46 | -<tbody>{{range $item := $items}}{{$item := asMap $item}}<tr><td>{{index $item "status"}}</td><td><a href="{{if $issues}}{{repoURL $.Query "issue" (str (index $item "id"))}}{{else}}{{repoURL $.Query "pr" (str (index $item "id"))}}{{end}}">{{index $item "title"}}</a>{{with collabLabels $item}} <small>{{join . ", "}}</small>{{end}}</td><td>{{template "nostrName" (str (index $item "author"))}}</td><td><time datetime="{{datetime (index $item "created_at")}}">{{when (index $item "created_at")}}</time></td></tr> |
| 47 | +<tbody>{{range $item := $items}}{{$item := asMap $item}}{{$s := collabProposal $item $.Actor (index $data "can_approve")}}<tr{{if $s.Shown}} data-state="{{$s.State}}"{{end}}><td>{{index $item "status"}}</td><td><a href="{{if $issues}}{{repoURL $.Query "issue" (str (index $item "id"))}}{{else}}{{repoURL $.Query "pr" (str (index $item "id"))}}{{end}}">{{index $item "title"}}</a>{{if $s.Shown}} <small>proposed | {{$s.State}}</small>{{end}}{{with collabLabels $item}} <small>{{join . ", "}}</small>{{end}}</td><td>{{template "nostrName" (str (index $item "author"))}}</td><td><time datetime="{{datetime (index $item "created_at")}}">{{when (index $item "created_at")}}</time></td></tr> |
| 48 | {{end}}</tbody></table>{{else}}<p>No matching {{if $issues}}issues{{else}}pull requests{{end}}.</p>{{end}} |
| 49 | {{with index $data "next_cursor"}}<p id="pages"><a href="{{browsePageURL "/repo" $.Query "cursor" .}}">next page</a></p>{{end}} |
| 50 | {{if .Actor}}<details id="collaboration-new"><summary>{{if $issues}}New issue{{else}}New pull request{{end}}</summary> |
| 51 | @@ -23,14 +23,14 @@ |
| 52 | <h1>{{index $item "title"}}</h1> |
| 53 | <p>{{index $item "status"}} by {{template "nostrName" (str (index $item "author"))}} <time datetime="{{datetime (index $item "created_at")}}">{{when (index $item "created_at")}}</time></p> |
| 54 | {{with collabLabels $item}}<p>Labels: {{join . ", "}}</p>{{end}} |
| 55 | -<article id="collaboration-body">{{markdown (str (index $item "content"))}}</article> |
| 56 | +{{$can := index $data "can_approve"}}{{template "collaborationProposal" (collabProposal $item .Actor $can)}}<article id="collaboration-body">{{markdown (str (index $item "content"))}}</article> |
| 57 | {{if not $issues}}<h2>Changes</h2>{{if index $data "diff_unavailable"}}<p>{{index $data "diff_unavailable"}}. The diff will be available after the required commits have been synchronized.</p>{{else}}<diff-view><table><tbody> |
| 58 | {{range $line := $diff.Lines}}<tr id="D{{$line.Number}}"><td><a href="#D{{$line.Number}}">{{$line.Number}}</a></td><td><code>{{$line.HTML}}</code>{{with $line.CommentURL}} <small><a href="{{.}}">comment</a></small>{{end}}</td></tr> |
| 59 | -{{range $c := $line.Comments}}{{$r := asMap $c}}<tr><td></td><td>{{template "nostrEvent" $c}}{{if $.Actor}}<details><summary>Reply to this comment</summary><nostr-compose kind="1111" coordinate="{{$coordinate}}" root="{{index $item "id"}}" root-pubkey="{{index $item "author"}}" root-kind="{{index $item "kind"}}" parent="{{index $r "id"}}" parent-pubkey="{{index $r "pubkey"}}" parent-kind="1111"><label>Reply <textarea name="content" required></textarea></label><button>Sign and reply</button></nostr-compose></details>{{end}}</td></tr> |
| 60 | +{{range $c := $line.Comments}}{{$r := asMap $c}}<tr><td></td><td>{{template "collaborationProposal" (collabProposal $r $.Actor $can)}}{{template "nostrEvent" $c}}{{if $.Actor}}<details><summary>Reply to this comment</summary><nostr-compose kind="1111" coordinate="{{$coordinate}}" root="{{index $item "id"}}" root-pubkey="{{index $item "author"}}" root-kind="{{index $item "kind"}}" parent="{{index $r "id"}}" parent-pubkey="{{index $r "pubkey"}}" parent-kind="1111"><label>Reply <textarea name="content" required></textarea></label><button>Sign and reply</button></nostr-compose></details>{{end}}</td></tr> |
| 61 | {{end}}{{end}}</tbody>{{if $diff.Truncated}}<tfoot><tr><td colspan="2">Diff preview truncated (256 KiB or 10,000 lines).</td></tr></tfoot>{{end}}</table></diff-view>{{if index $data "diff_truncated"}}<p>This diff is truncated. Clone the repository to review the complete change.</p>{{end}}{{end}}{{end}} |
| 62 | <h2>Conversation</h2> |
| 63 | {{$replies := $diff.Thread}}{{if $replies}}<ol id="events"> |
| 64 | -{{range $reply := $replies}}{{$r := asMap $reply}}<li>{{template "nostrEvent" $reply}}{{with reviewLabel $reply}}<p><small>on {{.}}</small></p>{{end}} |
| 65 | +{{range $reply := $replies}}{{$r := asMap $reply}}<li>{{template "collaborationProposal" (collabProposal $r $.Actor $can)}}{{template "nostrEvent" $reply}}{{with reviewLabel $reply}}<p><small>on {{.}}</small></p>{{end}} |
| 66 | {{if $.Actor}}<details><summary>Reply to this comment</summary><nostr-compose kind="1111" coordinate="{{$coordinate}}" root="{{index $item "id"}}" root-pubkey="{{index $item "author"}}" root-kind="{{index $item "kind"}}" parent="{{index $r "id"}}" parent-pubkey="{{index $r "pubkey"}}" parent-kind="1111"><label>Reply <textarea name="content" required></textarea></label><button>Sign and reply</button></nostr-compose></details>{{end}}</li> |
| 67 | {{end}}</ol>{{else}}<p>No replies yet.</p>{{end}} |
| 68 | {{with index $data "reply_cursor"}}<p id="pages"><a href="{{browsePageURL "/repo" $.Query "cursor" .}}">older replies</a></p>{{end}} |
| 69 | @@ -38,3 +38,7 @@ |
| 70 | {{if index $data "can_status"}}<details id="status-controls"><summary>Change status</summary><nostr-compose kind="status" coordinate="{{$coordinate}}" root="{{index $item "id"}}" root-pubkey="{{index $item "author"}}" root-kind="{{index $item "kind"}}"><label>Status <select name="status"><option value="1630">Reopen</option><option value="1631">{{if $issues}}Resolve{{else}}Mark merged{{end}}</option><option value="1632">Close</option><option value="1633">Draft</option></select></label><label>Comment <textarea name="content"></textarea></label><button>Sign and update</button></nostr-compose></details>{{end}} |
| 71 | <noscript><p>JavaScript and a connected signer are required to publish.</p></noscript>{{else}}<p><a href="/signin">Sign in</a> to reply.</p>{{end}} |
| 72 | </section>{{end}}{{end}} |
| 73 | + |
| 74 | +{{/* collaborationProposal is the bar above a proposed issue, pull request or comment: its state for the people who may see it, with the decision for those who may make it. */}} |
| 75 | +{{define "collaborationProposal"}}{{if .Shown}}<collaboration-proposal data-state="{{.State}}">Proposed by {{template "nostrName" .Author}} | {{.State}}{{if .By}} {{when .At}} by {{template "nostrName" .By}}{{end}}{{if .Decide}}<nostr-react event="{{.ID}}" pubkey="{{.Author}}" kind="{{.Kind}}"><button name="reaction" value="+">Accept</button><button name="reaction" value="-">Reject</button></nostr-react>{{end}}</collaboration-proposal> |
| 76 | +{{end}}{{end}} |
| 77 | diff --git a/internal/webui/collaboration_test.go b/internal/webui/collaboration_test.go |
| 78 | index 90762b2..065e6b8 100644 |
| 79 | --- a/internal/webui/collaboration_test.go |
| 80 | +++ b/internal/webui/collaboration_test.go |
| 81 | @@ -85,3 +85,130 @@ func TestCollaborationPagesUseTypedResultsAndWorkingDetailLinks(t *testing.T) { |
| 82 | t.Fatalf("wrong detail request: %s %v", backend.call, request) |
| 83 | } |
| 84 | } |
| 85 | + |
| 86 | +const ( |
| 87 | + proposalAgent = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" |
| 88 | + proposalMember = "6666666666666666666666666666666666666666666666666666666666666666" |
| 89 | + proposalIssue = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" |
| 90 | + proposalComment = "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" |
| 91 | + proposalPull = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" |
| 92 | + proposalReview = "1111111111111111111111111111111111111111111111111111111111111111" |
| 93 | +) |
| 94 | + |
| 95 | +// proposalBackend serves one repository with an issue and a pull request |
| 96 | +// proposed by an agent, each with a proposed comment. state is the |
| 97 | +// proposals' approval and approver marks the caller as allowed to decide. |
| 98 | +type proposalBackend struct { |
| 99 | + fakeBackend |
| 100 | + state string |
| 101 | + approver bool |
| 102 | +} |
| 103 | + |
| 104 | +func (b *proposalBackend) Query(_ context.Context, method string, params []json.RawMessage, actor string) (any, error) { |
| 105 | + b.call = method |
| 106 | + b.params = params |
| 107 | + proposal := func(m map[string]any) map[string]any { |
| 108 | + m["proposal"] = true |
| 109 | + m["approval"] = b.state |
| 110 | + if b.state != "pending" { |
| 111 | + m["approval_event"], m["approval_at"], m["approval_by"] = strings.Repeat("9", 64), float64(1788882000), b.policy.Owner |
| 112 | + } |
| 113 | + return m |
| 114 | + } |
| 115 | + ownerIssue := map[string]any{"id": strings.Repeat("b", 64), "title": "Owner issue", "kind": float64(1621), "author": b.policy.Owner, "content": "Owner body", "status": "open", "created_at": float64(1788800000)} |
| 116 | + agentIssue := proposal(map[string]any{"id": proposalIssue, "title": "Agent issue", "kind": float64(1621), "author": proposalAgent, "content": "Agent body", "status": "open", "created_at": float64(1788800100), "labels": []any{"bug"}}) |
| 117 | + agentPull := proposal(map[string]any{"id": proposalPull, "title": "Agent pull", "kind": float64(1618), "author": proposalAgent, "content": "Pull body", "status": "open", "created_at": float64(1788800200)}) |
| 118 | + comment := proposal(map[string]any{"id": proposalComment, "kind": float64(1111), "pubkey": proposalAgent, "content": "Agent comment", "created_at": float64(1788800300), "tags": []any{}}) |
| 119 | + plain := map[string]any{"id": strings.Repeat("2", 64), "kind": float64(1111), "pubkey": proposalMember, "content": "Member comment", "created_at": float64(1788800400), "tags": []any{}} |
| 120 | + review := proposal(map[string]any{"id": proposalReview, "kind": float64(1111), "pubkey": proposalAgent, "content": "Agent review", "created_at": float64(1788800500), "tags": []any{}, "file": "f.txt", "line": float64(1), "side": "new"}) |
| 121 | + repository := map[string]any{"owner": b.policy.Owner, "clone": []any{}, "maintainers": []any{}} |
| 122 | + var request struct { |
| 123 | + Event string `json:"event"` |
| 124 | + } |
| 125 | + if len(params) > 0 { |
| 126 | + _ = json.Unmarshal(params[0], &request) |
| 127 | + } |
| 128 | + switch method { |
| 129 | + case "browseissues": |
| 130 | + return map[string]any{"repository": repository, "items": []any{agentIssue, ownerIssue}, "can_approve": b.approver}, nil |
| 131 | + case "browsepulls": |
| 132 | + return map[string]any{"repository": repository, "items": []any{agentPull}, "can_approve": b.approver}, nil |
| 133 | + case "browseissue": |
| 134 | + item := ownerIssue |
| 135 | + if request.Event == proposalIssue { |
| 136 | + item = agentIssue |
| 137 | + } |
| 138 | + return map[string]any{"repository": repository, "item": item, "replies": []any{comment, plain}, "can_approve": b.approver, "can_status": false}, nil |
| 139 | + case "browsepull": |
| 140 | + return map[string]any{"repository": repository, "item": agentPull, "replies": []any{review, plain}, "diff": "diff --git a/f.txt b/f.txt\n--- a/f.txt\n+++ b/f.txt\n@@ -1 +1 @@\n-old\n+new\n", "can_approve": b.approver, "can_status": false}, nil |
| 141 | + } |
| 142 | + return map[string]any{}, nil |
| 143 | +} |
| 144 | + |
| 145 | +func proposalPage(t *testing.T, b *proposalBackend, actor, view, id string) string { |
| 146 | + t.Helper() |
| 147 | + app, err := New(b, Options{Actor: func(*http.Request) (string, error) { return actor, nil }}) |
| 148 | + if err != nil { |
| 149 | + t.Fatal(err) |
| 150 | + } |
| 151 | + path := repoURL(url.Values{"owner": {b.policy.Owner}, "repo": {"test"}}, view, id) |
| 152 | + return wikiGet(t, app, path) |
| 153 | +} |
| 154 | + |
| 155 | +func TestCollaborationProposalBarOffersTheDecisionToApprovers(t *testing.T) { |
| 156 | + b := &proposalBackend{fakeBackend: fakeBackend{policy: policy.Defaults(strings.Repeat("a", 64))}, state: "pending", approver: true} |
| 157 | + owner := b.policy.Owner |
| 158 | + body := proposalPage(t, b, owner, "issue", proposalIssue) |
| 159 | + wantAll(t, "issue", body, |
| 160 | + `<collaboration-proposal data-state="pending">Proposed by <nostr-name pubkey="`+proposalAgent+`"`, |
| 161 | + `| pending<nostr-react event="`+proposalIssue+`" pubkey="`+proposalAgent+`" kind="1621"><button name="reaction" value="+">Accept</button><button name="reaction" value="-">Reject</button></nostr-react></collaboration-proposal> |
| 162 | +<article id="collaboration-body">`, |
| 163 | + `<li><collaboration-proposal data-state="pending">Proposed by <nostr-name pubkey="`+proposalAgent+`"`, |
| 164 | + `<nostr-react event="`+proposalComment+`" pubkey="`+proposalAgent+`" kind="1111">`, |
| 165 | + `<li><nostr-event id="event-`+strings.Repeat("2", 64)+`">`) |
| 166 | + if strings.Count(body, "<collaboration-proposal") != 2 { |
| 167 | + t.Fatalf("issue page bars: %s", body) |
| 168 | + } |
| 169 | + // A comment placed on a diff line carries its bar inside the diff. |
| 170 | + body = proposalPage(t, b, owner, "pr", proposalPull) |
| 171 | + wantAll(t, "pr", body, |
| 172 | + `<nostr-react event="`+proposalPull+`" pubkey="`+proposalAgent+`" kind="1618">`, |
| 173 | + `<td></td><td><collaboration-proposal data-state="pending">Proposed by <nostr-name pubkey="`+proposalAgent+`"`, |
| 174 | + `<nostr-react event="`+proposalReview+`" pubkey="`+proposalAgent+`" kind="1111">`) |
| 175 | + // The lists mark the rows for those who may see them. |
| 176 | + body = proposalPage(t, b, owner, "issues", "") |
| 177 | + wantAll(t, "issues", body, `<tr data-state="pending"><td>open</td><td><a href="`, `">Agent issue</a> <small>proposed | pending</small> <small>bug</small></td>`, `<tr><td>open</td><td><a href="`) |
| 178 | + body = proposalPage(t, b, owner, "prs", "") |
| 179 | + wantAll(t, "prs", body, `<tr data-state="pending"><td>open</td><td><a href="`, `">Agent pull</a> <small>proposed | pending</small></td>`) |
| 180 | + // A decided proposal names the decision, its time and the deciding key, |
| 181 | + // and offers no buttons. |
| 182 | + for _, state := range []string{"approved", "rejected"} { |
| 183 | + b.state = state |
| 184 | + body = proposalPage(t, b, owner, "issue", proposalIssue) |
| 185 | + wantAll(t, state, body, `<collaboration-proposal data-state="`+state+`">Proposed by <nostr-name pubkey="`+proposalAgent+`"`, `| `+state+` 2026-09-08 15:40 UTC by <nostr-name pubkey="`+owner+`"`) |
| 186 | + wantNone(t, state, body, "<nostr-react", ">Accept<") |
| 187 | + body = proposalPage(t, b, owner, "issues", "") |
| 188 | + wantAll(t, state, body, `<tr data-state="`+state+`">`, `<small>proposed | `+state+`</small>`) |
| 189 | + } |
| 190 | +} |
| 191 | + |
| 192 | +func TestCollaborationProposalAuthorSeesTheStateWithoutButtons(t *testing.T) { |
| 193 | + b := &proposalBackend{fakeBackend: fakeBackend{policy: policy.Defaults(strings.Repeat("a", 64))}, state: "pending"} |
| 194 | + body := proposalPage(t, b, proposalAgent, "issue", proposalIssue) |
| 195 | + wantAll(t, "issue", body, `<collaboration-proposal data-state="pending">Proposed by <nostr-name pubkey="`+proposalAgent+`"`, `| pending</collaboration-proposal>`) |
| 196 | + wantNone(t, "issue", body, "<nostr-react", ">Accept<", ">Reject<") |
| 197 | + if strings.Count(body, "<collaboration-proposal") != 2 { |
| 198 | + t.Fatalf("author bars: %s", body) |
| 199 | + } |
| 200 | + body = proposalPage(t, b, proposalAgent, "issues", "") |
| 201 | + wantAll(t, "issues", body, `<tr data-state="pending">`, `">Agent issue</a> <small>proposed | pending</small>`) |
| 202 | +} |
| 203 | + |
| 204 | +func TestCollaborationProposalStateIsHiddenFromMembers(t *testing.T) { |
| 205 | + b := &proposalBackend{fakeBackend: fakeBackend{policy: policy.Defaults(strings.Repeat("a", 64))}, state: "approved"} |
| 206 | + for _, page := range [][2]string{{"issue", proposalIssue}, {"pr", proposalPull}, {"issues", ""}, {"prs", ""}} { |
| 207 | + body := proposalPage(t, b, proposalMember, page[0], page[1]) |
| 208 | + wantAll(t, page[0], body, "Agent ") |
| 209 | + wantNone(t, page[0], body, "<collaboration-proposal", "<tr data-state=", "proposed | ", "<nostr-react") |
| 210 | + } |
| 211 | +} |
| 212 | diff --git a/internal/webui/style.css b/internal/webui/style.css |
| 213 | index 40e8524..5f1bb31 100644 |
| 214 | --- a/internal/webui/style.css |
| 215 | +++ b/internal/webui/style.css |
| 216 | @@ -226,9 +226,10 @@ diff-view td>nostr-event{white-space:normal;max-width:720px;border:0;padding:6px |
| 217 | wiki-article{display:block;max-width:720px;overflow-wrap:anywhere}wiki-article>header{font:12px var(--mono);color:var(--muted);margin-bottom:8px}wiki-article>header nostr-key,wiki-article>header nostr-name{color:var(--ink)} |
| 218 | wiki-article h1{font-size:20px;margin:18px 0 8px}wiki-article h2{font-size:17px}wiki-article h3{font-size:15px}wiki-article p{margin:0 0 10px} |
| 219 | wiki-article pre{background:var(--rail);border:1px solid var(--line);padding:10px 12px;overflow:auto;font-size:12.5px;line-height:1.5;white-space:pre}wiki-article ul,wiki-article ol{padding-left:20px;margin:0 0 10px} |
| 220 | -merge-request,wiki-proposal{display:block;border:1px solid var(--line);background:var(--soft);padding:8px 10px;margin:0 0 14px;max-width:720px;font:13px var(--sans)}merge-request nostr-key,merge-request nostr-name,wiki-proposal nostr-key,wiki-proposal nostr-name{color:var(--ink);font-weight:500}merge-request>a,merge-request button,wiki-proposal button{margin-left:6px;font-size:12px} |
| 221 | -merge-request nostr-react,merge-request nostr-react form,wiki-proposal nostr-react,wiki-proposal nostr-react form{display:inline}merge-request nostr-react output,wiki-proposal nostr-react output{display:inline-block;margin-left:6px;font:12px var(--mono);color:var(--muted)}merge-request nostr-react output[data-error],wiki-proposal nostr-react output[data-error]{color:var(--alert)} |
| 222 | -wiki-proposal[data-state=pending]{border-color:var(--bark)}wiki-proposal[data-state=rejected]{border-color:var(--alert)}#wiki-list #rows td:first-child small,#panel li[data-state]{font:12px var(--mono)}#wiki-list #rows td:first-child small{margin-left:6px}#wiki-list #rows tr[data-state=pending] small,#panel li[data-state=pending]{color:var(--bark)}#wiki-list #rows tr[data-state=approved] small,#panel li[data-state=approved]{color:var(--accent)}#wiki-list #rows tr[data-state=rejected] small,#panel li[data-state=rejected]{color:var(--alert)} |
| 223 | +merge-request,wiki-proposal,collaboration-proposal{display:block;border:1px solid var(--line);background:var(--soft);padding:8px 10px;margin:0 0 14px;max-width:720px;font:13px var(--sans)}merge-request nostr-key,merge-request nostr-name,wiki-proposal nostr-key,wiki-proposal nostr-name,collaboration-proposal nostr-key,collaboration-proposal nostr-name{color:var(--ink);font-weight:500}merge-request>a,merge-request button,wiki-proposal button,collaboration-proposal button{margin-left:6px;font-size:12px} |
| 224 | +merge-request nostr-react,merge-request nostr-react form,wiki-proposal nostr-react,wiki-proposal nostr-react form,collaboration-proposal nostr-react,collaboration-proposal nostr-react form{display:inline}merge-request nostr-react output,wiki-proposal nostr-react output,collaboration-proposal nostr-react output{display:inline-block;margin-left:6px;font:12px var(--mono);color:var(--muted)}merge-request nostr-react output[data-error],wiki-proposal nostr-react output[data-error],collaboration-proposal nostr-react output[data-error]{color:var(--alert)} |
| 225 | +wiki-proposal[data-state=pending],collaboration-proposal[data-state=pending]{border-color:var(--bark)}wiki-proposal[data-state=rejected],collaboration-proposal[data-state=rejected]{border-color:var(--alert)}#wiki-list #rows td:first-child small,#panel li[data-state]{font:12px var(--mono)}#wiki-list #rows td:first-child small{margin-left:6px}#wiki-list #rows tr[data-state=pending] small,#panel li[data-state=pending]{color:var(--bark)}#wiki-list #rows tr[data-state=approved] small,#panel li[data-state=approved]{color:var(--accent)}#wiki-list #rows tr[data-state=rejected] small,#panel li[data-state=rejected]{color:var(--alert)} |
| 226 | +#events li>collaboration-proposal,diff-view td>collaboration-proposal{margin:6px 0 8px;max-width:none}#collaboration-list #rows tr[data-state] td:nth-child(2)>small:first-of-type{font:12px var(--mono)}#collaboration-list #rows tr[data-state=pending] td:nth-child(2)>small:first-of-type{color:var(--bark)}#collaboration-list #rows tr[data-state=approved] td:nth-child(2)>small:first-of-type{color:var(--accent)}#collaboration-list #rows tr[data-state=rejected] td:nth-child(2)>small:first-of-type{color:var(--alert)} |
| 227 | #merge-status{font:12px var(--mono);color:var(--muted)}#wiki-compare h2{margin-top:18px}#wiki-redirects{font:12px var(--mono);color:var(--muted)} |
| 228 | wiki-compose{display:block;max-width:720px;border-bottom:1px solid var(--line);padding:10px 0} |
| 229 | wiki-compose label{display:inline-block;margin:0 10px 6px 0;vertical-align:top;font:13px var(--sans);color:var(--muted)}wiki-compose label input{display:block;margin-top:2px;font:13px var(--mono);min-width:200px} |
| 230 | diff --git a/internal/webui/templates.go b/internal/webui/templates.go |
| 231 | index f09ae87..12ef017 100644 |
| 232 | --- a/internal/webui/templates.go |
| 233 | +++ b/internal/webui/templates.go |
| 234 | @@ -257,6 +257,7 @@ func parseTemplates() (*template.Template, error) { |
| 235 | "collabItems": collaborationItems, |
| 236 | "collabReplies": collaborationReplies, |
| 237 | "collabLabels": collaborationLabels, |
| 238 | + "collabProposal": collaborationProposal, |
| 239 | "reviewDiff": reviewDiffView, |
| 240 | "reviewAnchor": reviewAnchorQuery, |
| 241 | "reviewLabel": reviewAnchorLabel, |