Move the Recently sent description into the card header (PMS-765) #520

Merged
longjacksonle merged 2 commits from fix/PMS-765-card-subtitle into main 2026-08-12 05:52:14 +02:00

PMS-765, feedback on the "Recently sent" panel from #519: the line under the heading reads as cramped. It was.

Why

It rendered as the first child of the card body:

Card { title: "Recently sent".to_string(), padding: false,
    p { class: "px-6 pb-4 text-xs text-muted", "Links you have emailed clients, ..." }
    Table { ... }
}

Three things at once:

  1. No space above it. Card's header is px-6 pt-6 pb-4 border-b border-line, and a padding: false card gives its body no padding at all, so the paragraph carried px-6 pb-4 and nothing at the top. It butted straight against the header rule.
  2. A rule immediately below. The next element is the table's own header row, with its background and border.
  3. A size smaller than its neighbours. text-xs, where the app's descriptive copy is text-sm.

Small grey text, no room above, a line right underneath.

The fix

The sentence is the card's subtitle, so it goes in the heading, above the rule.

  • Card and CardHeader gain an optional subtitle, rendered under the title as mt-1 text-sm text-muted.
  • The header switches from items-center to items-start when it has one, so a card's header actions stay level with the title rather than floating beside the description.
  • Defaulted and empty renders nothing, so every existing card is untouched.

Why not just add pt-4 and bump the size. It relieves the squeeze and leaves the sentence below the rule, reading as a stray row of the table rather than as part of the heading. And the next card wanting a description hand-rolls the same three classes against whatever padding that card happens to have, since a padding: false card has none and a padded one has pt-4. That is how spacing drifts across a codebase one card at a time.

Tests

Two, rendered through the real Card: the subtitle appears before the body element even opens, it renders at text-sm with room above it, a two-line heading aligns its actions to the title, and a card without a subtitle keeps the header it had.

The card under test is deliberately padded. "Inside the header" and "first thing in the body" are indistinguishable by document order, which is exactly what my first draft compared, so it passed against both the fixed and the broken shape. A padded body carries a class of its own, and the subtitle landing before that class is what actually separates the two. Verified by putting the subtitle back in the body and watching the test fail.

Scope

Presentational. One optional prop on a shared component, one call site. Nothing in the panel's fetch, cap or refresh behaviour. just check clean, just test 318 pass.

PMS-765, feedback on the "Recently sent" panel from #519: the line under the heading reads as cramped. It was. ## Why It rendered as the first child of the card body: ```rust Card { title: "Recently sent".to_string(), padding: false, p { class: "px-6 pb-4 text-xs text-muted", "Links you have emailed clients, ..." } Table { ... } } ``` Three things at once: 1. **No space above it.** `Card`'s header is `px-6 pt-6 pb-4 border-b border-line`, and a `padding: false` card gives its body no padding at all, so the paragraph carried `px-6 pb-4` and nothing at the top. It butted straight against the header rule. 2. **A rule immediately below.** The next element is the table's own header row, with its background and border. 3. **A size smaller than its neighbours.** `text-xs`, where the app's descriptive copy is `text-sm`. Small grey text, no room above, a line right underneath. ## The fix The sentence is the card's subtitle, so it goes in the heading, above the rule. - `Card` and `CardHeader` gain an optional `subtitle`, rendered under the title as `mt-1 text-sm text-muted`. - The header switches from `items-center` to `items-start` when it has one, so a card's header actions stay level with the title rather than floating beside the description. - Defaulted and empty renders nothing, so every existing card is untouched. **Why not just add `pt-4` and bump the size.** It relieves the squeeze and leaves the sentence below the rule, reading as a stray row of the table rather than as part of the heading. And the next card wanting a description hand-rolls the same three classes against whatever padding that card happens to have, since a `padding: false` card has none and a padded one has `pt-4`. That is how spacing drifts across a codebase one card at a time. ## Tests Two, rendered through the real `Card`: the subtitle appears before the body element even opens, it renders at `text-sm` with room above it, a two-line heading aligns its actions to the title, and a card without a subtitle keeps the header it had. The card under test is deliberately **padded**. "Inside the header" and "first thing in the body" are indistinguishable by document order, which is exactly what my first draft compared, so it passed against both the fixed and the broken shape. A padded body carries a class of its own, and the subtitle landing before that class is what actually separates the two. Verified by putting the subtitle back in the body and watching the test fail. ## Scope Presentational. One optional prop on a shared component, one call site. Nothing in the panel's fetch, cap or refresh behaviour. `just check` clean, `just test` 318 pass.
PMS-765: the line under "Recently sent" read as cramped, and it was. It rendered as the first child of the card body, which on a `padding: false` card has no padding of its own, so the paragraph carried `px-6 pb-4` and nothing at the top and butted straight against the header rule. Directly below it came the table's own header row with its background and rule. Small grey text, no room above, a line immediately under: a band wedged between two rules. It was also `text-xs`, a size below the `text-sm` the rest of the app uses for descriptive copy.

The sentence is the card's subtitle, so it belongs in the heading, above the rule. `Card` and `CardHeader` gain an optional `subtitle`, rendered under the title as `mt-1 text-sm text-muted`, and the header switches from `items-center` to `items-start` when it has one, so header actions stay level with the title instead of floating beside the description. Defaulted, so every existing card renders exactly as before.

Adding `pt-4` and bumping the size would have relieved the squeeze and left the sentence below the rule, reading as a stray row of the table rather than as part of the heading, and the next card wanting a description would hand-roll the same three classes against whatever padding that card happens to have. That is how spacing drifts one card at a time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U7UHqTQ4tXXoHQ1sbXcavx
test(card): keep a subtitle out of the card body
All checks were successful
Check / fmt + clippy + tests (pull_request) Successful in 3m44s
Create release / Create release from merged PR (pull_request) Has been skipped
89b19e39b3
PMS-765 regression, rendered through the real `Card`: the subtitle must appear before the body element even opens, it renders at `text-sm` with room above it, and a two-line heading aligns its actions to the title.

The card under test is padded on purpose. "Inside the header" and "first thing in the body" are indistinguishable by document order, which is what the first draft of this test compared and why it passed against both shapes; a padded body carries a class of its own, so the subtitle landing before that class is what actually separates the two. Checked by putting the subtitle back in the body and watching the test fail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U7UHqTQ4tXXoHQ1sbXcavx
longjacksonle scheduled this pull request to auto merge when all checks succeed 2026-08-12 05:49:24 +02:00
longjacksonle deleted branch fix/PMS-765-card-subtitle 2026-08-12 05:52:14 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/mokosh-apps!520
No description provided.