fabrica

hanna/fabrica

feat(web): use the chip dropdown for editing issue/PR labels

dbb14b3 · hanna committed on 2026-07-26

The existing-issue/PR label editor was a flat checkbox list; it now uses
the same `<details>` dropdown as the new-issue picker, whose toggle shows
the applied labels as chips (JS replaces "Select labels"). Checking
labels and pressing Apply POSTs the set; the button works without JS.
Writers see the dropdown in place of the separate label-set display.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed by hannaSSH key fingerprint: SHA256:4g9cWhkAAw8gwqhJUcVbSnGEvdGwklW+1Aa/fMUu59k
3 files changed · +41 −23UnifiedSplit
assets/base.css +12 −8
@@ -706,15 +706,19 @@ body.drawer-open .drawer-backdrop {
706706 flex-wrap: wrap;
707707 gap: 0.35rem;
708708 }
709-.label-picker {
710- flex-direction: column;
711- align-items: stretch;
709+/* Existing-issue label editor: the dropdown fills the sidebar width, its toggle
710+ shows the applied labels as chips, and Apply sits at the foot of the popover
711+ (the column popover stretches it full width). */
712+.side-form .label-dropdown {
713+ flex: 1;
714+ margin-bottom: 0;
712715 }
713-.label-picker .checkbox {
714- display: flex;
715- align-items: center;
716- gap: 0.35rem;
717- margin: 0.15rem 0;
716+.side-form .label-dropdown > summary {
717+ width: 100%;
718+ justify-content: space-between;
719+}
720+.label-apply {
721+ margin-top: 0.5rem;
718722 }
719723 .label-swatch {
720724 width: 0.8rem;
crates/web/src/issues.rs +25 −13
@@ -285,10 +285,12 @@ pub(crate) async fn view(
285285 }
286286 section {
287287 h3 { "Labels" }
288- @if issue_labels.is_empty() { p class="muted" { "None" } }
289- @else { div class="label-set" { @for l in &issue_labels { (label_chip(l)) } } }
290288 @if can_write && !repo_labels.is_empty() {
291289 (label_form(&issue, &repo_labels, &issue_labels, &csrf))
290+ } @else if issue_labels.is_empty() {
291+ p class="muted" { "None" }
292+ } @else {
293+ div class="label-set" { @for l in &issue_labels { (label_chip(l)) } }
292294 }
293295 }
294296 (deps_section(&issue, &ctx, &deps, &dependents, &csrf, can_write))
@@ -478,7 +480,10 @@ pub(crate) async fn assignee_form(
478480 }
479481 }
480482
481-/// The label-toggle form: checkboxes for each repo label.
483+/// The label editor for an existing issue/PR: a `<details>` dropdown whose toggle
484+/// shows the applied labels as chips (JS replaces the "Select labels" text), the
485+/// same control as the new-issue picker. Checking labels and pressing Apply POSTs
486+/// the whole set; the button also works without JS.
482487 pub(crate) fn label_form(
483488 issue: &Issue,
484489 repo_labels: &[Label],
@@ -487,19 +492,26 @@ pub(crate) fn label_form(
487492 ) -> Markup {
488493 let is_on = |id: &str| applied.iter().any(|l| l.id == id);
489494 html! {
490- // Each checkbox auto-submits the whole set (the handler replaces the
491- // applied labels); a noscript Apply button keeps it working without JS.
492- form method="post" action=(format!("/issue/{}/labels", issue.id)) class="side-form label-picker" {
495+ form method="post" action=(format!("/issue/{}/labels", issue.id)) class="side-form" {
493496 input type="hidden" name="_csrf" value=(csrf);
494- @for l in repo_labels {
495- label class="checkbox" {
496- input type="checkbox" name="label" value=(l.id) checked[is_on(&l.id)]
497- data-autosubmit;
498- " " span class="label-swatch" style=(format!("--label-color: {}", css_color(&l.color))) {}
499- " " (l.name)
497+ details class="menu label-dropdown" data-menu data-label-dropdown {
498+ summary class="btn" {
499+ span class="label-dropdown-label" data-label-summary { "Select labels" }
500+ (icon(Icon::ChevronDown))
501+ }
502+ div class="menu-popover label-menu" {
503+ @for l in repo_labels {
504+ label class="checkbox" {
505+ input type="checkbox" name="label" value=(l.id) checked[is_on(&l.id)]
506+ data-label-name=(l.name)
507+ data-label-color=(css_color(&l.color));
508+ " " span class="label-swatch" style=(format!("--label-color: {}", css_color(&l.color))) {}
509+ " " (l.name)
510+ }
511+ }
512+ button class="btn btn-primary inline-btn label-apply" type="submit" { "Apply" }
500513 }
501514 }
502- noscript { button class="btn inline-btn" type="submit" { "Apply labels" } }
503515 }
504516 }
505517 }
crates/web/src/pulls.rs +4 −2
@@ -183,10 +183,12 @@ pub(crate) async fn view(
183183 }
184184 section {
185185 h3 { "Labels" }
186- @if issue_labels.is_empty() { p class="muted" { "None" } }
187- @else { div class="label-set" { @for l in &issue_labels { (label_chip(l)) } } }
188186 @if can_write && !repo_labels.is_empty() {
189187 (label_form(&issue, &repo_labels, &issue_labels, &csrf))
188+ } @else if issue_labels.is_empty() {
189+ p class="muted" { "None" }
190+ } @else {
191+ div class="label-set" { @for l in &issue_labels { (label_chip(l)) } }
190192 }
191193 }
192194 (deps_section(&issue, &ctx, &deps, &dependents, &csrf, can_write))