Commit ce5d6507cb

ce5d6507cbb3d35ff42bf802c0ec151bebced84d

parent: 00324124a5

Verified · cmc ci/build: success ci/test: success

cmc <hello@cleberg.net> · 2026-09-22 04:00 UTC

web: line the accounts row's controls up in columns

The disabled Promote loses its confirm field. It was there to keep the
cell's shape, but with the clipping gone both fields showed at once and
a pending row read as two identical "type <name> to confirm" boxes, one
of them inert. Dropping it is what the shape argument wanted anyway:
active and pending rows are now the same three controls in the same
places.

The cell is two fixed slots, each a field, a gap and a button wide, with
contents right-aligned. Sizing the slot rather than the form is what
holds the columns: the role button, the access field and the access
button each land on one rule whichever controls a row carries, and no
element stands in for the ones it does not. A form cannot span table
cells, so giving each control its own column and letting the table align
them needs form="" plumbing; this gets there without it.

Right-aligning the cell alone was not enough — the enable form has no
confirm field, so the disabled row's Promote sat where every other row
keeps its field. Two more things the generic form.actions rule did here:
it grew the button to fill the form, which ate the slot, and it wrapped,
which put the button on a line under the field. Both are overridden, and
the button takes a set width so the slot's arithmetic is exact.

The field goes 11rem to 14rem, which fits "type <name> to confirm" for
every account on the instance; the widest needs 176px of 190px. A long
enough name still truncates and the aria-label carries the instruction
in full either way.

Ref #247
CHANGELOG.org +6 −1
@@ -48,7 +48,12 @@ Eleven web findings, mostly alignment and affordance (#247).
4848 active, rather than leaving the cell short, and the page is wide. At
4949 48rem the Actions cell clipped the second form's button, which left
5050 its confirm field looking like it belonged to the button before it —
51 an active row read as though Promote wanted the name typed.
51 an active row read as though Promote wanted the name typed. The cell
52 is now two fixed slots, each right-aligned, so the role button, the
53 confirm field and the access button each hold one column whichever
54 controls a row carries: an admin's row has a field on both forms, a
55 disabled account's enable form has none. The field is 14rem, which
56 fits "type <name> to confirm" for a name of ordinary length.
5257- The profile's inactive tabs draw their underline in =--line=, so the
5358 strip reads as a track the current tab is marked in.
5459- =.listhead= sheds its children's block margins, which is what kept a
internal/web/static/style.css +34 −3
@@ -920,9 +920,40 @@ table.keys td.act, table.assets td.size { text-align: right; }
920920 keep one line each and .tablewrap scrolls what does not fit */
921921table.keys.nowrap th, table.keys.nowrap td { white-space: nowrap; }
922922/* The account list types a username to confirm, which is longer than
923 the key prefixes the confirm field is sized for elsewhere. */
924table.accounts input[name="confirm"] { width: 11rem; }
925table.accounts form.actions { display: inline-flex; gap: var(--sp-2); margin: 0 var(--sp-2) var(--sp-2) 0; }
923 the key prefixes the confirm field is sized for elsewhere. 14rem, not
924 11rem, because "type <name> to confirm" was cut mid-word for a name of
925 ordinary length; a long enough name still truncates and the field's
926 aria-label carries the instruction in full either way.
927
928 The cell is two fixed slots, each right-aligned. A form cannot span
929 table cells, so one column per control — which would let the table
930 align them — is out without form="" plumbing, and the forms hold
931 different numbers of controls: only an admin's role form has a
932 confirm field, and the enable form has none. Sizing the slot instead
933 of the form is what puts every button in one column and every field
934 in another; right-aligning the cell alone left the disabled row's
935 Promote where other rows keep their field. A slot is a field, a gap
936 and a button wide, so a form that is only a button still ends on the
937 same rule. No element stands in for the controls a row lacks. */
938table.accounts { --acct-field: 14rem; --acct-btn: 6rem; }
939table.accounts td.acts { display: flex; justify-content: flex-end; align-items: center; gap: var(--sp-2); }
940table.accounts input[name="confirm"] { width: var(--acct-field); }
941table.accounts form.actions {
942 flex: none;
943 display: inline-flex;
944 /* the generic form.actions wraps and grows its button to fill; both
945 have to go, or the slot takes the button to a second line and the
946 field sits above it */
947 flex-wrap: nowrap;
948 justify-content: flex-end;
949 align-items: center;
950 gap: var(--sp-2);
951 width: calc(var(--acct-field) + var(--acct-btn) + var(--sp-2));
952 margin: 0;
953}
954/* a set width, not a minimum: it makes the slot's arithmetic exact, and
955 6rem clears the widest of Promote, Demote, Disable and Enable */
956table.accounts form.actions button { flex: none; width: var(--acct-btn); justify-content: center; }
926957table.assets td.name { width: 100%; font-family: var(--mono); font-size: var(--fs-1); }
927958table.assets { margin-top: var(--sp-2); }
928959
internal/web/templates/adminusers.html +5 −4
@@ -25,7 +25,7 @@
2525 <td><span class="chip {{if eq .State "active"}}chip-open{{else if eq .State "disabled"}}chip-closed{{else}}chip-neutral{{end}}">{{.State}}</span></td>
2626 <td>{{when .CreatedAt}}</td>
2727 <td>{{if .LastSeen}}{{when .LastSeen}}{{else}}never{{end}}</td>
28 <td>
28 <td class="acts">
2929 {{if .Admin}}
3030 <form method="post" action="/admin/users" class="actions">
3131 <input type="hidden" name="field" value="demote">
@@ -42,10 +42,11 @@
4242 </form>
4343 {{else}}
4444 {{/* An account that is not active cannot be promoted, but the row
45 still draws the control so every Actions cell is the same
46 shape. Both parts are disabled, so the form never posts. */}}
45 still draws the button so the cell keeps its shape. It carries
46 no confirm field: nothing is confirmed, and a second field with
47 the same placeholder as the disable form's reads as a duplicate
48 of it. Disabled, so the form never posts. */}}
4749 <form method="post" action="/admin/users" class="actions" title="Only an active account can be promoted">
48 <input type="text" name="confirm" aria-label="Type {{.Username}} to confirm" placeholder="type {{.Username}} to confirm" size="{{len .Username}}" autocomplete="off" disabled>
4950 <button type="submit" class="btn" disabled>Promote</button>
5051 </form>
5152 {{end}}