Project

General

Profile

Feature #1527 » design-1527-mockup.html

Redmine Admin, 07/31/2026 06:11 PM

 
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>#1527 + #1528 — AFTER — staff action bar, four viewer states</title>
<style>
:root{
--background:#f8fafc; --surface:#fff; --surface-muted:#f1f5f9;
--border:#e2e8f0; --border-strong:#cbd5e1;
--text-primary:#0f172a; --text-secondary:#334155; --text-muted:#475569;
--primary:#047857; --primary-hover:#059669; --on-primary:#fff;
--brand-50:#ecfdf5; --brand-500:#10b981; --brand-700:#047857;
--spec-fg:#6d28d9; --spec-bg:#f3eefe;
--design-fg:#6d28d9; --design-bg:#f3eefe;
--building-fg:#b45309; --building-bg:#fef3e2;
--quality-fg:#0e7490; --quality-bg:#e0f2f5;
--prod-fg:#15803d; --prod-bg:#e7f6ec;
--proposed-fg:#475569; --proposed-bg:#f1f5f9;
--danger-fg:#b91c1c; --danger-bg:#fcebeb;
--accent-50:#e0f2f5; --accent-700:#0e7490;
--focus-ring:#047857;
}
*{box-sizing:border-box}
body{margin:0;background:var(--background);color:var(--text-primary);
font-family:Inter,ui-sans-serif,system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
font-size:14px;line-height:1.45;-webkit-font-smoothing:antialiased}
.page{max-width:840px;margin:0 auto;padding:22px 20px 44px}
h2.state{margin:30px 0 4px;font-size:15px;font-weight:700;letter-spacing:-.01em}
h2.state span.num{display:inline-flex;align-items:center;justify-content:center;
height:22px;width:22px;border-radius:9999px;background:var(--brand-50);color:var(--brand-700);
font-size:12px;margin-right:8px}
p.note{margin:0 0 12px;font-size:12.5px;color:var(--text-muted);max-width:64ch}
p.note b{color:var(--text-primary)}
code{font-family:ui-monospace,"JetBrains Mono",Menlo,monospace;font-size:.92em;
background:var(--surface-muted);border-radius:4px;padding:1px 4px}
.banner{border:1px solid var(--border);background:var(--surface);border-radius:12px;
padding:12px 14px;margin-bottom:6px;font-size:12.5px;color:var(--text-muted)}
.banner b{color:var(--text-primary)}
.card{background:var(--surface);border:1px solid var(--border);border-radius:16px;
padding:18px;box-shadow:0 1px 2px rgba(15,23,42,.05)}
.hero{position:relative;overflow:hidden}
.wash{position:absolute;right:-40px;top:-48px;height:176px;width:176px;border-radius:9999px;
background:var(--brand-50);filter:blur(28px)}
.hero>.inner{position:relative;display:flex;flex-direction:column;gap:12px}
.pillrow{display:flex;flex-wrap:wrap;align-items:center;gap:8px}
.idchip{display:inline-flex;align-items:center;border:1px solid var(--border);
background:var(--background);border-radius:8px;padding:2px 8px;
font-family:ui-monospace,"JetBrains Mono",Menlo,monospace;font-size:12px;color:var(--text-muted)}
.badge{display:inline-flex;align-items:center;gap:6px;border-radius:9999px;padding:3px 10px;
font-size:12px;font-weight:600}
.badge.spec,.badge.design{background:var(--spec-bg);color:var(--spec-fg)}
.badge.dev{background:var(--building-bg);color:var(--building-fg)}
.badge.qa{background:var(--quality-bg);color:var(--quality-fg)}
.badge.backlog{background:var(--proposed-bg);color:var(--proposed-fg)}
.badge.ship{background:var(--prod-bg);color:var(--prod-fg)}
.tag{display:inline-flex;align-items:center;gap:5px;border-radius:9999px;padding:3px 9px;
font-size:11.5px;font-weight:600;background:var(--accent-50);color:var(--accent-700)}
h1{margin:0;max-width:34rem;font-size:23px;line-height:1.2;font-weight:600;letter-spacing:-.02em}
.metarow{display:flex;flex-wrap:wrap;align-items:center;gap:10px 18px}
.meta{font-size:13px;color:var(--text-muted);display:inline-flex;align-items:center;gap:8px}
.avatar{height:24px;width:24px;border-radius:9999px;background:var(--accent-50);color:var(--accent-700);
display:inline-flex;align-items:center;justify-content:center;font-size:10px;font-weight:700}
.avatar.sm{height:20px;width:20px;font-size:9px}
.actions{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-left:auto}
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;height:36px;
padding:0 12px;border-radius:8px;font-size:13px;font-weight:500;white-space:nowrap;
border:1px solid transparent;cursor:default}
.btn.secondary{background:var(--surface);border-color:var(--border);color:var(--text-primary)}
.btn.primary{background:var(--primary);color:var(--on-primary)}
.btn.square{width:36px;padding:0}
.btn.muted{background:var(--surface);border-color:var(--border);color:var(--text-muted);opacity:.55}
.btn.focused{outline:2px solid var(--focus-ring);outline-offset:2px}

/* ---- IR.1 staff action bar ---- */
.staffbar{margin-top:14px;border:1px solid var(--border);background:var(--surface-muted);
border-radius:12px;padding:12px 14px;display:flex;flex-direction:column;gap:10px}
.staffbar .head{display:flex;align-items:center;gap:8px;font-size:11px;font-weight:600;
text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted)}
.staffbar .row{display:flex;flex-wrap:wrap;align-items:center;gap:10px}
.select{display:inline-flex;align-items:center;justify-content:space-between;gap:10px;height:36px;
min-width:15rem;border:1px solid var(--border);background:var(--surface);border-radius:8px;
padding:0 12px;font-size:13px;font-weight:500;color:var(--text-primary)}
.select .lead{display:inline-flex;align-items:center;gap:8px}
.select .caret{color:var(--text-muted);font-size:11px}
.select.disabled{opacity:.6}
.menu{position:relative}
.menupanel{margin-top:6px;border:1px solid var(--border);background:var(--surface);border-radius:8px;
padding:4px;box-shadow:0 8px 24px rgba(15,23,42,.12);min-width:15rem;display:flex;
flex-direction:column;gap:2px}
.menupanel .item{display:flex;align-items:center;gap:8px;border-radius:6px;padding:7px 10px;
font-size:13px;color:var(--text-secondary)}
.menupanel .item.hi{background:var(--surface-muted)}
.menupanel .sep{height:1px;background:var(--border);margin:3px 2px}
.menupanel .note{padding:7px 10px;font-size:12px;color:var(--text-muted)}
.tipwrap{position:relative;display:inline-flex}
.tip{position:absolute;bottom:calc(100% + 8px);left:50%;transform:translateX(-50%);
width:max-content;max-width:15rem;border:1px solid var(--border);background:var(--surface);
border-radius:8px;box-shadow:0 8px 24px rgba(15,23,42,.14);padding:8px 12px;font-size:12px;
color:var(--text-secondary);z-index:5}
.tip:after{content:"";position:absolute;top:100%;left:50%;transform:translateX(-50%) rotate(45deg);
margin-top:-5px;height:9px;width:9px;background:var(--surface);border-right:1px solid var(--border);
border-bottom:1px solid var(--border)}
.alert{display:flex;align-items:flex-start;gap:8px;border-radius:8px;padding:8px 10px;
font-size:12.5px;background:var(--danger-bg);color:var(--danger-fg);border:1px solid #f3cccc}
.hint{font-size:12px;color:var(--text-muted)}

.rail{width:19rem}
.railcard{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:14px;
box-shadow:0 1px 2px rgba(15,23,42,.05)}
.railcard h3{margin:0 0 4px;font-size:11px;font-weight:600;text-transform:uppercase;
letter-spacing:.06em;color:var(--text-muted)}
dl{margin:0}
.dlrow{display:flex;align-items:center;justify-content:space-between;gap:12px;
border-top:1px solid var(--border);padding:9px 0;font-size:13px}
.dlrow dt{color:var(--text-muted)}
.dlrow dd{margin:0;font-weight:500;text-align:right;display:inline-flex;align-items:center;gap:7px}
.mono{font-family:ui-monospace,"JetBrains Mono",Menlo,monospace}
.two{display:grid;grid-template-columns:minmax(0,1fr) 19rem;gap:18px;align-items:start}
.callout{border-left:3px solid var(--brand-500);background:var(--brand-50);border-radius:0 8px 8px 0;
padding:9px 12px;font-size:12.5px;color:var(--text-secondary);margin:10px 0 0}
.payload{margin:8px 0 0;border:1px solid var(--border);background:var(--surface);border-radius:8px;
padding:9px 11px;font-family:ui-monospace,"JetBrains Mono",Menlo,monospace;font-size:11.5px;
color:var(--text-secondary);white-space:pre-wrap}
.payload b{color:var(--text-primary)}
</style>
</head>
<body>
<div class="page">

<div class="banner">
<b>AFTER — #1527 (Move Forward + Change Status) and #1528 (Assign + assignee) designed together.</b><br>
Both land in the same hero/action region of <code>TaskDetailPane.tsx</code>, so they get one
design-system section (§IR) and <b>one mount point</b>: a <code>StaffActionBar</code> band
directly under the hero card. The hero action row itself is <b>untouched</b> — that is how state 1
stays byte-for-byte today's pane.<br>
No new colour: everything below is §2/§3 tokens already in the system.
</div>

<!-- ============ 1. ORDINARY CLIENT ============ -->
<h2 class="state"><span class="num">1</span>Ordinary client — <code>internal_role: null</code></h2>
<p class="note">Nothing is added. No staff bar, no Move Forward, no Change Status, no Assign, and
<b>no assignee name anywhere</b> — the payload does not carry <code>assigned_to_name</code> at all
for this viewer, so the name is absent from the HTTP response, not merely hidden in the DOM
(spec decision 7). Identical to the BEFORE capture.</p>
<div class="two">
<div>
<section class="card hero">
<div class="wash"></div>
<div class="inner">
<div class="pillrow"><span class="idchip">#1527</span><span class="badge spec">Spec</span><span class="tag">✦ Feature</span></div>
<h1>Ticket panel: Move Forward and a role-correct Change Status control</h1>
<div class="metarow">
<span class="meta"><span class="avatar">RA</span>Requested by Redmine Admin</span>
<span class="meta">Updated 2 hours ago</span>
<span class="actions">
<span class="btn secondary square"></span>
<span class="btn secondary">✎ Edit</span>
<span class="btn secondary">⏸ Pause</span>
</span>
</div>
</div>
</section>
<div class="payload">GET …/tasks/1527 → <b>{ status_id: 8, allowed_statuses: [] }</b>
no <b>can_move_forward</b> · no <b>can_assign</b> · no <b>assigned_to_name</b> key</div>
</div>
<div class="rail">
<div class="railcard">
<h3>At a glance</h3>
<dl>
<div class="dlrow"><dt>Reference</dt><dd class="mono">#1527</dd></div>
<div class="dlrow"><dt>Stage</dt><dd><span class="badge spec">Spec</span></dd></div>
<div class="dlrow"><dt>Type</dt><dd>Feature</dd></div>
</dl>
</div>
</div>
</div>

<!-- ============ 2. DEVELOPER, ASSIGNED ============ -->
<h2 class="state"><span class="num">2</span>Developer, assigned to this ticket</h2>
<p class="note">One control, not two. The server sends <code>allowed_statuses</code> already narrowed
to what this viewer may set — for a developer that is exactly <code>[next_status]</code>, so
<b>length &lt; 2 ⇒ Change Status is not rendered</b> and Move Forward is the only door. A
one-option dropdown would be a menu offering a choice that does not exist, three clicks for what
the button does in one. The label <b>names the target stage</b>, from
<code>next_status_id</code> + <code>next_status_name</code>.</p>
<div class="two">
<div>
<section class="card hero">
<div class="wash"></div>
<div class="inner">
<div class="pillrow"><span class="idchip">#1527</span><span class="badge spec">Spec</span><span class="tag">✦ Feature</span></div>
<h1>Ticket panel: Move Forward and a role-correct Change Status control</h1>
<div class="metarow">
<span class="meta"><span class="avatar">RA</span>Requested by Redmine Admin</span>
<span class="actions">
<span class="btn secondary square"></span>
<span class="btn secondary">✎ Edit</span>
<span class="btn secondary">⏸ Pause</span>
</span>
</div>
</div>
</section>

<section class="staffbar">
<div class="head">⚙ Staff controls</div>
<div class="row">
<span class="btn primary">→ Move to Design</span>
<span class="hint">You are assigned to this ticket.</span>
</div>
</section>
<div class="payload">can_move_forward: <b>true</b> · next_status_id: <b>15</b> ("Design")
allowed_statuses: <b>[ {id:15, name:"Design"} ]</b> → 1 option ⇒ no dropdown</div>
</div>
<div class="rail">
<div class="railcard">
<h3>At a glance</h3>
<dl>
<div class="dlrow"><dt>Reference</dt><dd class="mono">#1527</dd></div>
<div class="dlrow"><dt>Stage</dt><dd><span class="badge spec">Spec</span></dd></div>
<div class="dlrow"><dt>Assigned to</dt><dd><span class="avatar sm">GW</span>Guillaume W.</dd></div>
</dl>
</div>
</div>
</div>

<!-- ============ 3. DEVELOPER, NOT ASSIGNED ============ -->
<h2 class="state"><span class="num">3</span>Developer, <b>not</b> assigned — disabled + tooltip</h2>
<p class="note">The button is present and <b>keyboard-focusable</b>: <code>aria-disabled="true"</code>
with the click handler short-circuited, never a bare <code>disabled</code> (which drops it out of
the tab order, so the explanation becomes unreachable exactly for the people who need it most).
The tooltip is the <code>aria-describedby</code> target, shown on hover <i>and</i> on
<code>:focus-visible</code>. Below: the same button focused by keyboard, with the second reason
shape — nothing left to advance to.</p>
<div class="two">
<div>
<section class="card hero">
<div class="wash"></div>
<div class="inner">
<div class="pillrow"><span class="idchip">#1531</span><span class="badge dev">In development</span></div>
<h1>Backend: the progression rule and the move-forward route</h1>
<div class="metarow">
<span class="meta"><span class="avatar">RA</span>Requested by Redmine Admin</span>
<span class="actions"><span class="btn secondary square"></span><span class="btn secondary">⏸ Pause</span></span>
</div>
</div>
</section>

<section class="staffbar">
<div class="head">⚙ Staff controls</div>
<div class="row" style="padding-top:56px">
<span class="tipwrap">
<span class="tip" id="tip-notassignee">Only the developer assigned to this ticket can move it forward.</span>
<span class="btn muted" aria-describedby="tip-notassignee">→ Move to QA</span>
</span>
</div>
</section>

<section class="staffbar">
<div class="head">⚙ Staff controls — variant: no next stage (ticket is Shipped)</div>
<div class="row" style="padding-top:56px">
<span class="tipwrap">
<span class="tip" id="tip-nonext">This ticket is at the end of the pipeline.</span>
<span class="btn muted focused" aria-describedby="tip-nonext">→ Move forward</span>
</span>
<span class="hint">focus-visible ring — the tooltip opens on keyboard focus too</span>
</div>
</section>
<div class="payload">can_move_forward: <b>false</b>, next_status_id: <b>10</b> → “not assigned to you”
can_move_forward: <b>false</b>, next_status_id: <b>null</b> → “end of the pipeline”
allowed_statuses: <b>[]</b> in both cases ⇒ no Change Status control</div>
</div>
<div class="rail">
<div class="railcard">
<h3>At a glance</h3>
<dl>
<div class="dlrow"><dt>Stage</dt><dd><span class="badge dev">In development</span></dd></div>
<div class="dlrow"><dt>Assigned to</dt><dd><span class="avatar sm">AL</span>Alice L.</dd></div>
</dl>
</div>
</div>
</div>

<!-- ============ 4. CTO / ADMIN ============ -->
<h2 class="state"><span class="num">4</span>CTO / platform admin — full set, backward included, plus Assign</h2>
<p class="note">Three controls on one row, in decreasing frequency of use:
<b>Move Forward</b> (primary — the one verb used twenty times a day) → <b>Change Status</b>
(the correction path, forward <i>and</i> backward) → <b>Assign</b>. The Change Status menu lists
<code>allowed_statuses</code> verbatim: the frontend holds no status order, so it cannot decide
what “forward” means and does not try. Backlog is absent because the server never puts it there.</p>
<div class="two">
<div>
<section class="card hero">
<div class="wash"></div>
<div class="inner">
<div class="pillrow"><span class="idchip">#1527</span><span class="badge spec">Spec</span><span class="tag">✦ Feature</span></div>
<h1>Ticket panel: Move Forward and a role-correct Change Status control</h1>
<div class="metarow">
<span class="meta"><span class="avatar">RA</span>Requested by Redmine Admin</span>
<span class="actions"><span class="btn secondary square"></span><span class="btn secondary">✎ Edit</span><span class="btn secondary">⏸ Pause</span></span>
</div>
</div>
</section>

<section class="staffbar">
<div class="head">⚙ Staff controls</div>
<div class="row">
<span class="btn primary">→ Move to Design</span>
<span class="select"><span class="lead"><span class="badge spec">Spec</span><span style="color:var(--text-secondary);font-weight:400">Change status</span></span><span class="caret"></span></span>
</div>
<div class="row">
<span class="select"><span class="lead"><span class="avatar sm">GW</span>Assigned to Guillaume W.</span><span class="caret"></span></span>
</div>
<div class="menu">
<div class="menupanel" style="max-width:16rem">
<div class="item hi"><span class="avatar sm" style="background:var(--surface-muted);color:var(--text-muted)"></span>Unassigned</div>
<div class="sep"></div>
<div class="item"><span class="avatar sm">GW</span>Guillaume W. <span style="color:var(--text-muted);font-size:11.5px">· current</span></div>
<div class="item"><span class="avatar sm">AL</span>Alice L.</div>
<div class="item"><span class="avatar sm">MD</span>Marc D.</div>
</div>
</div>
<p class="hint">Assign menu open — <code>GET /projects/9/developers</code>. “Unassigned” is an
explicit first option, separated by a hairline from the people.</p>
</section>

<div class="callout"><b>Change Status menu (same anatomy as the existing <code>TransitionControl</code>,
§TB.6):</b> each target is a <code>TaskStatusPill</code>, selecting one opens the inline
confirm step before the mutation fires. Backward targets are listed in the server's order,
unmarked — a “backward” badge would need a status order in <code>src/</code>.</div>
<div class="menu">
<div class="menupanel">
<div class="item"><span class="badge backlog">Submitted</span></div>
<div class="item hi"><span class="badge design">Design</span></div>
<div class="item"><span class="badge dev">In development</span></div>
<div class="item"><span class="badge qa">QA</span></div>
<div class="item"><span class="badge ship">Ready to ship</span></div>
<div class="item"><span class="badge backlog">Blocked</span></div>
<div class="note">No “Backlog” — the server never offers it, in either direction.</div>
</div>
</div>

<div class="payload">can_move_forward: <b>true</b> · can_assign: <b>true</b> · next_status_id: <b>15</b>
allowed_statuses: <b>7, 15, 9, 10, 11, 12, 13, 14</b> → ≥ 2 options ⇒ dropdown renders</div>
</div>
<div class="rail">
<div class="railcard">
<h3>At a glance</h3>
<dl>
<div class="dlrow"><dt>Reference</dt><dd class="mono">#1527</dd></div>
<div class="dlrow"><dt>Stage</dt><dd><span class="badge spec">Spec</span></dd></div>
<div class="dlrow"><dt>Assigned to</dt><dd><span class="avatar sm">GW</span>Guillaume W.</dd></div>
</dl>
</div>
</div>
</div>

<!-- ============ 5. BACKLOG ============ -->
<h2 class="state"><span class="num">5</span>Backlog ticket, viewed by a CTO — the credit gate holds</h2>
<p class="note"><b>No status control renders for anyone, including CTO and admin.</b> Not a disabled
Move Forward with an explanation — <i>absent</i>. Submit stays the only way out of the backlog,
and the pane must not suggest a second door exists. The <b>Assign control survives</b>: assigning
changes no status and costs no credit, so a CTO can line the work up before the client pays.</p>
<div class="two">
<div>
<section class="card hero">
<div class="wash"></div>
<div class="inner">
<div class="pillrow"><span class="idchip">#1528</span><span class="badge backlog">Backlog</span><span class="tag">✦ Feature</span></div>
<h1>Ticket panel: Assign to Developer and assignee visibility</h1>
<div class="metarow">
<span class="meta">Complexity <b style="color:var(--text-primary)">4/10</b></span>
<span class="actions">
<span class="btn primary">➤ Submit to the team</span>
<span class="btn secondary square"></span>
<span class="btn secondary">✎ Edit</span>
<span class="btn secondary">⏸ Pause</span>
</span>
</div>
</div>
</section>

<section class="staffbar">
<div class="head">⚙ Staff controls</div>
<div class="row">
<span class="select disabled"><span class="lead"><span class="avatar sm" style="background:var(--surface-muted);color:var(--text-muted)"></span>Unassigned</span><span class="caret"></span></span>
<span class="hint">No developer is configured for this project.</span>
</div>
<p class="hint">Empty-state variant of the Assign control: <code>GET …/developers</code> legitimately
returned <code>[]</code>. The trigger is <code>aria-disabled</code> (focusable, described by
the note), never an empty menu that opens onto nothing.</p>
</section>
<p class="hint" style="margin-top:10px">Filing is free — nothing starts until you submit this ticket.</p>
<div class="payload">status_id: <b>16</b> → Move Forward and Change Status are <b>not rendered</b>
can_move_forward: <b>false</b> · next_status_id: <b>null</b> · allowed_statuses: <b>[]</b>
can_assign: <b>true</b> — assignment is not a status change</div>
</div>
<div class="rail">
<div class="railcard">
<h3>At a glance</h3>
<dl>
<div class="dlrow"><dt>Stage</dt><dd><span class="badge backlog">Backlog</span></dd></div>
<div class="dlrow"><dt>Assigned to</dt><dd style="color:var(--text-muted);font-weight:400">Unassigned</dd></div>
</dl>
</div>
</div>
</div>

<!-- ============ 6. ERRORS ============ -->
<h2 class="state"><span class="num">6</span>Server refusals — inline, never blanking the pane</h2>
<p class="note">Every error is a <code>role="alert"</code> paragraph <b>inside the staff bar</b>, under
the control that produced it. The ticket, its description and its criteria stay on screen; the
status pill never showed an unconfirmed stage, because the controls are pessimistic by design —
there is no optimistic state to roll back. Each failure still invalidates the board and detail
queries, so a pane made stale by someone else converges on the next paint.</p>
<section class="staffbar" style="gap:12px">
<div class="head">⚙ Staff controls</div>
<div class="row">
<span class="btn primary">→ Move to QA</span>
<span class="select"><span class="lead"><span class="badge dev">In development</span><span style="color:var(--text-secondary);font-weight:400">Change status</span></span><span class="caret"></span></span>
</div>
<div class="alert"><span><b>403</b> — You're not allowed to make that change. The ticket has been refreshed.</span></div>
<div class="alert"><span><b>409</b> — Couldn't move this task. <span style="opacity:.85">(the existing invalid-transition message, reused verbatim)</span></span></div>
<div class="row">
<span class="select"><span class="lead"><span class="avatar sm">MD</span>Assigned to Marc D.</span><span class="caret"></span></span>
</div>
<div class="alert"><span><b>422</b> — Marc D. has no Redmine account linked yet. An admin can add one in the admin dashboard.</span></div>
<div class="alert"><span><b>upstream</b> — Couldn't change the assignee.</span></div>
</section>

</div>
</body>
</html>
(3-3/4)