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). |
| 48 | 48 | active, rather than leaving the cell short, and the page is wide. At |
| 49 | 49 | 48rem the Actions cell clipped the second form's button, which left |
| 50 | 50 | 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. |
| 52 | 57 | - The profile's inactive tabs draw their underline in =--line=, so the |
| 53 | 58 | strip reads as a track the current tab is marked in. |
| 54 | 59 | - =.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; } |
| 920 | 920 | keep one line each and .tablewrap scrolls what does not fit */ |
| 921 | 921 | table.keys.nowrap th, table.keys.nowrap td { white-space: nowrap; } |
| 922 | 922 | /* The account list types a username to confirm, which is longer than |
| 923 | | the key prefixes the confirm field is sized for elsewhere. */ |
| 924 | | table.accounts input[name="confirm"] { width: 11rem; } |
| 925 | | table.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. */ |
| 938 | table.accounts { --acct-field: 14rem; --acct-btn: 6rem; } |
| 939 | table.accounts td.acts { display: flex; justify-content: flex-end; align-items: center; gap: var(--sp-2); } |
| 940 | table.accounts input[name="confirm"] { width: var(--acct-field); } |
| 941 | table.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 */ |
| 956 | table.accounts form.actions button { flex: none; width: var(--acct-btn); justify-content: center; } |
| 926 | 957 | table.assets td.name { width: 100%; font-family: var(--mono); font-size: var(--fs-1); } |
| 927 | 958 | table.assets { margin-top: var(--sp-2); } |
| 928 | 959 | |
internal/web/templates/adminusers.html
+5 −4
| @@ -25,7 +25,7 @@ |
| 25 | 25 | <td><span class="chip {{if eq .State "active"}}chip-open{{else if eq .State "disabled"}}chip-closed{{else}}chip-neutral{{end}}">{{.State}}</span></td> |
| 26 | 26 | <td>{{when .CreatedAt}}</td> |
| 27 | 27 | <td>{{if .LastSeen}}{{when .LastSeen}}{{else}}never{{end}}</td> |
| 28 | | <td> |
| 28 | <td class="acts"> |
| 29 | 29 | {{if .Admin}} |
| 30 | 30 | <form method="post" action="/admin/users" class="actions"> |
| 31 | 31 | <input type="hidden" name="field" value="demote"> |
| @@ -42,10 +42,11 @@ |
| 42 | 42 | </form> |
| 43 | 43 | {{else}} |
| 44 | 44 | {{/* 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. */}} |
| 47 | 49 | <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> |
| 49 | 50 | <button type="submit" class="btn" disabled>Promote</button> |
| 50 | 51 | </form> |
| 51 | 52 | {{end}} |