Use nldd-icon-button and nldd-activity-indicator for the card actions #236

Closed
robbertbos wants to merge 4 commits from style/nldd-icon-buttons into main
Owner

Replaces five hand-rolled icon buttons and one hand-rolled spinner with the NLDD components, then fixes the three regressions that only showed up in the running app.

The swaps

Where Was Now
CardRow quick actions + "Open in Mattermost", both variants (5 sites) 24x24 hand-rolled <button> / <a> nldd-icon-button size="xs" variant="neutral-transparent"
@-mention loading row border-top-color + @keyframes rotate nldd-activity-indicator size="16"

The geometry is a dead-on match at xs: 24x24 box, 16px icon, 4px radius, all three unchanged.

The three regressions, and what they taught

The hover plate came back. neutral-transparent hard-codes a transparent hover background, so the quick actions lost their grey chip while BubbleMenuButton beside them kept one. The value is a literal in the shadow stylesheet, so no variant and no --semantics-buttons-* token reaches it. But NLDD sets no background on the host, and a transparent inner control lets the host's own background through - so this is public CSS on our own element, not the private --_is-hovered-background-color. Measured back to oklch(0.923 0.009 257.4) light and oklch(0.348 0.02 257.4) dark, identical to what BubbleMenuButton paints.

display: flex on the host fixes three things at once. :host is inline-block, so it built a 27px line box around a 24px control. That overhang produced a strip of the card's pointer cursor under every button, a 1.5px misalignment against BubbleMenuButton, and would have made the hover plate 3px too tall. The hosts measure 24x24 again.

The cursor. Dragging along the row flipped arrow, hand, arrow, hand. Two causes: the card's own pointer leaking through the 2px gaps and the 3px overhang, and the href variant taking --semantics-controls-link-cursor, which base.css:99 sets to pointer. Waggle's convention (base.css:94) is that buttons keep the arrow and the pointer is for links; an icon button is a button whatever element it renders. Scoping the token on the group settles all four at default and leaves every real text link alone. Measured: all four default, all four 24x24.

A skipped tab stop. useCardKeyboardNav's FOCUSABLE_SELECTOR only knew light-DOM elements, so none of the three hosts counted and the jump to the next section fired one control early - "Open in Mattermost" was unreachable by Tab.

What has no test, and why

The tab-stop fix has no unit test. getFocusableWithin filters on offsetParent, which happy-dom does not populate, so a custom element that is not already the active element is filtered out before any assertion can see it. Writing a test that passes anyway would be worse than none. Verified in Chromium on the built bundle instead, together with the cursor and the plate.

Left out on purpose

The analysis found 19 hand-rolled icon buttons, not 10. These five are the ones where the box does not change, so only colour and hover move. The rest each have a reason to wait: ReactionBar's add button is a bordered pill matching its badge siblings; MmSourceBreadcrumb's overflow trigger is inline text with no box; ImageLightbox sits on a black backdrop where neutral-transparent inverts to near-black; the two preview modals have 36px neighbours that neither size step matches; BubbleMenuButton serves two variants from one element.

Verification

vue-tsc, eslint (0 errors), vitest 691 in 94 files, vite build, Playwright e2e 66 passed / 3 skipped, pre-commit run --all-files. Cursor, hover plate and box size measured in Chromium against the built bundle.

Replaces five hand-rolled icon buttons and one hand-rolled spinner with the NLDD components, then fixes the three regressions that only showed up in the running app. ## The swaps | Where | Was | Now | |---|---|---| | `CardRow` quick actions + "Open in Mattermost", both variants (5 sites) | 24x24 hand-rolled `<button>` / `<a>` | `nldd-icon-button size="xs" variant="neutral-transparent"` | | `@`-mention loading row | `border-top-color` + `@keyframes` rotate | `nldd-activity-indicator size="16"` | The geometry is a dead-on match at `xs`: 24x24 box, 16px icon, 4px radius, all three unchanged. ## The three regressions, and what they taught **The hover plate came back.** `neutral-transparent` hard-codes a transparent hover background, so the quick actions lost their grey chip while `BubbleMenuButton` beside them kept one. The value is a literal in the shadow stylesheet, so no variant and no `--semantics-buttons-*` token reaches it. But NLDD sets no background on the *host*, and a transparent inner control lets the host's own background through - so this is public CSS on our own element, not the private `--_is-hovered-background-color`. Measured back to `oklch(0.923 0.009 257.4)` light and `oklch(0.348 0.02 257.4)` dark, identical to what `BubbleMenuButton` paints. **`display: flex` on the host fixes three things at once.** `:host` is `inline-block`, so it built a 27px line box around a 24px control. That overhang produced a strip of the card's pointer cursor under every button, a 1.5px misalignment against `BubbleMenuButton`, and would have made the hover plate 3px too tall. The hosts measure 24x24 again. **The cursor.** Dragging along the row flipped arrow, hand, arrow, hand. Two causes: the card's own pointer leaking through the 2px gaps and the 3px overhang, and the `href` variant taking `--semantics-controls-link-cursor`, which `base.css:99` sets to `pointer`. Waggle's convention (`base.css:94`) is that buttons keep the arrow and the pointer is for links; an icon button is a button whatever element it renders. Scoping the token on the group settles all four at `default` and leaves every real text link alone. Measured: all four `default`, all four 24x24. **A skipped tab stop.** `useCardKeyboardNav`'s `FOCUSABLE_SELECTOR` only knew light-DOM elements, so none of the three hosts counted and the jump to the next section fired one control early - "Open in Mattermost" was unreachable by Tab. ## What has no test, and why The tab-stop fix has no unit test. `getFocusableWithin` filters on `offsetParent`, which happy-dom does not populate, so a custom element that is not already the active element is filtered out before any assertion can see it. Writing a test that passes anyway would be worse than none. Verified in Chromium on the built bundle instead, together with the cursor and the plate. ## Left out on purpose The analysis found 19 hand-rolled icon buttons, not 10. These five are the ones where the box does not change, so only colour and hover move. The rest each have a reason to wait: `ReactionBar`'s add button is a bordered pill matching its badge siblings; `MmSourceBreadcrumb`'s overflow trigger is inline text with no box; `ImageLightbox` sits on a black backdrop where `neutral-transparent` inverts to near-black; the two preview modals have 36px neighbours that neither size step matches; `BubbleMenuButton` serves two variants from one element. ## Verification `vue-tsc`, eslint (0 errors), vitest 691 in 94 files, `vite build`, Playwright e2e 66 passed / 3 skipped, `pre-commit run --all-files`. Cursor, hover plate and box size measured in Chromium against the built bundle.
robbertbos force-pushed style/nldd-icon-buttons from 1abf0badf8
All checks were successful
CI / release-scripts (pull_request) Successful in 10s
security-scan / JS SCA (npm audit) (pull_request) Successful in 21s
security-scan / SBOM (trivy) (pull_request) Successful in 12s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 17s
security-scan / Python SAST (bandit) (pull_request) Successful in 26s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 53s
CI / frontend-test (pull_request) Successful in 1m43s
CI / pre-commit (pull_request) Successful in 2m2s
test-build / build (backend) (pull_request) Successful in 1m52s
test-build / build (frontend) (pull_request) Successful in 1m53s
test-build / build (pull_request) Successful in 0s
CI / backend-test (pull_request) Successful in 3m47s
CI / e2e (pull_request) Successful in 6m13s
to 317b391985
All checks were successful
CI / release-scripts (pull_request) Successful in 10s
security-scan / SBOM (trivy) (pull_request) Successful in 13s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 16s
security-scan / JS SCA (npm audit) (pull_request) Successful in 20s
security-scan / Python SAST (bandit) (pull_request) Successful in 28s
CI / pre-commit (pull_request) Successful in 35s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 46s
test-build / build (backend) (pull_request) Successful in 1m18s
test-build / build (frontend) (pull_request) Successful in 1m19s
CI / frontend-test (pull_request) Successful in 1m28s
test-build / build (pull_request) Successful in 0s
CI / backend-test (pull_request) Successful in 2m48s
CI / e2e (pull_request) Successful in 4m42s
2026-08-06 18:20:20 +00:00
Compare
Author
Owner

Closing: this is being handled elsewhere.

For whoever picks it up, what was in here and what it cost to find:

  • Five hand-rolled icon buttons in CardRow (quick actions plus the Mattermost link, both variants) to nldd-icon-button size="xs", and the @-mention loading spinner to nldd-activity-indicator.
  • Three regressions the swap introduced, each only visible in the running app: the hover plate disappears (neutral-transparent hard-codes a transparent hover background, so the plate has to be painted on the host - public CSS, not the private --_is-hovered-background-color); the cursor flips along the row (the card's pointer leaks through the 2px gaps and through a 3px overhang from :host { display: inline-block }, fixed with display: flex on the host); and useCardKeyboardNav's FOCUSABLE_SELECTOR only knew light-DOM elements, so Tab skipped "Open in Mattermost".
  • BubbleMenuButton had to move to the same two --semantics-buttons-neutral-transparent-* tokens or it stayed visibly lighter than its swapped neighbours.

Worth knowing: none of that is unit-testable here. getFocusableWithin filters on offsetParent, which happy-dom does not populate, so a custom element that is not already the active element is filtered out before any assertion can see it. It was verified by measuring in Chromium on the built bundle.

Closing: this is being handled elsewhere. For whoever picks it up, what was in here and what it cost to find: - Five hand-rolled icon buttons in `CardRow` (quick actions plus the Mattermost link, both variants) to `nldd-icon-button size="xs"`, and the @-mention loading spinner to `nldd-activity-indicator`. - Three regressions the swap introduced, each only visible in the running app: the hover plate disappears (`neutral-transparent` hard-codes a transparent hover background, so the plate has to be painted on the host - public CSS, not the private `--_is-hovered-background-color`); the cursor flips along the row (the card's pointer leaks through the 2px gaps and through a 3px overhang from `:host { display: inline-block }`, fixed with `display: flex` on the host); and `useCardKeyboardNav`'s `FOCUSABLE_SELECTOR` only knew light-DOM elements, so Tab skipped "Open in Mattermost". - `BubbleMenuButton` had to move to the same two `--semantics-buttons-neutral-transparent-*` tokens or it stayed visibly lighter than its swapped neighbours. Worth knowing: none of that is unit-testable here. `getFocusableWithin` filters on `offsetParent`, which happy-dom does not populate, so a custom element that is not already the active element is filtered out before any assertion can see it. It was verified by measuring in Chromium on the built bundle.
robbertbos closed this pull request 2026-08-06 18:24:10 +00:00
Author
Owner

Opgegaan in #245, samen met #239 en #242. De commits zijn ongewijzigd overgenomen; #245 legt er de icoon-semantiek bovenop, die anders met #242 in conflict kwam.

Opgegaan in #245, samen met #239 en #242. De commits zijn ongewijzigd overgenomen; #245 legt er de icoon-semantiek bovenop, die anders met #242 in conflict kwam.
All checks were successful
CI / release-scripts (pull_request) Successful in 10s
Required
Details
security-scan / SBOM (trivy) (pull_request) Successful in 13s
Required
Details
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 16s
Required
Details
security-scan / JS SCA (npm audit) (pull_request) Successful in 20s
Required
Details
security-scan / Python SAST (bandit) (pull_request) Successful in 28s
Required
Details
CI / pre-commit (pull_request) Successful in 35s
Required
Details
security-scan / Python SCA (pip-audit) (pull_request) Successful in 46s
Required
Details
test-build / build (backend) (pull_request) Successful in 1m18s
Required
Details
test-build / build (frontend) (pull_request) Successful in 1m19s
Required
Details
CI / frontend-test (pull_request) Successful in 1m28s
Required
Details
test-build / build (pull_request) Successful in 0s
Required
Details
CI / backend-test (pull_request) Successful in 2m48s
Required
Details
CI / e2e (pull_request) Successful in 4m42s
Required
Details

Pull request closed

Sign in to join this conversation.
No reviewers
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
robbertbos/waggle!236
No description provided.