Skip to content

Widgets: let a floating card grow to fit content that arrives after its size was saved #988

Open

Description

@juanlentino

Split out of #913 (case 1 in my comment there), since #914 handles repositioning and this is about height.

What happens

A floating widget keeps the exact rectangle the user last saved, and its body scrolls (.os-widgets__card-body { overflow-y: auto }). When the widget's content grows later, the new rows land below the fold of the card. Controls at the bottom (a "Sweep now" button, a "Check for updates" button) and any line that only appears when something needs attention are hidden until the user notices and drags the card taller.

On a six-card desktop this happens with any status card:

  • a status card that adds "1 watch due" or "Credential refused" only when it applies;
  • a queue card that adds a gap warning;
  • a provenance card that lists failing documents while a sweep runs.

These are exactly the lines a user most needs to see, and they are the ones the saved height hides.

Docked cards don't have this problem: without a persisted height they size to their content. Floating cards have no equivalent, and a widget has no way to ask for more room (WidgetContext is { id, pluginUrl, storage }, and wp.os.widgets exposes only redock).

Proposal

An opt-in on the widget definition. It is off by default, so no existing widget changes.

  • Registration: openstation_register_widget( $id, array( 'fit_content' => true ) ) in PHP and fitContent: true on the JS WidgetDef, carried through the existing server payload like maxHeight.
  • Behavior, for a floating card that opts in:
    • When its content needs more height than the user's saved height, the card grows to fit.
    • It is capped at maxHeight when set, otherwise at the bottom of the work area, where a resize already stops (above the dock pill).
    • When the content shrinks, it shrinks back, never below the height the user set.
    • Width and position never change.
  • Persistence: none. The saved geometry stays the user's, so the grown height is never written to desktop-mode-widgets-geometry. A user resize sets a new floor, and a drag saves the user's height, not the grown one.
  • Docked cards: a docked card with a persisted height follows the same rule. One without a persisted height already sizes naturally.

Out of scope, and how it meets #914

Moving the cards below a grown card out of its way is a layout question, and it fits with the reflow in #914 rather than here.

Because the grown height is not persisted, #914's grouping, which reads stored geometry, keeps seeing the user's heights. A grown card can therefore overlap a card directly below it until the content shrinks or the user moves one of them. I'd rather leave that visible than write geometry outside a user gesture. Happy to discuss either direction.

Questions for you

  1. Is the name right? fitContent reads as what it does. autoHeight is the other candidate.
  2. Should the cap default to the work-area bottom or to maxHeight only, so a widget that sets neither never grows?
  3. Should the growth be animated? I'd say no, matching the resize handle.

I have this working on a branch in my fork, with tests and docs: frame-local logic in frame.ts, staying out of the files #914 touches. I'll open the PR once you're happy with the shape.

Engineered with Claude as a pair programmer.

Activity

  1. juanlentino commented on Oct 12, 2026

    @juanlentino
    ContributorAuthor

    A related bug on the same code path, now fixed in #989 (e8dd5d5): the card is box-sizing: content-box with a 1 px border, and currentGeometry() in src/widgets/frame.ts (:661-662 at trunk 572ce21) saves offsetWidth / offsetHeight, which are the border-box size. So every drag end and resize end stores the size plus 2 px, and the next apply draws it 2 px larger. The resize start (:564), the liberate snapshot (:420, :435) and the docked height (:618) read the border box the same way. Seen live: an inline width of 364 px against an offsetWidth of 366, with every stored width even and well above the registered 312. The PR now saves the content box, with a test that round-trips save, apply, save.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions