Skip to content

refactor(cuesheet): improve image scrolling - #2190

Open
cpvalente wants to merge 5 commits into
masterfrom
claude/cuesheet-image-unload-scroll-k26i4u
Open

refactor(cuesheet): improve image scrolling#2190
cpvalente wants to merge 5 commits into
masterfrom
claude/cuesheet-image-unload-scroll-k26i4u

Conversation

@cpvalente

Copy link
Copy Markdown
Owner

No description provided.

claude added 5 commits July 25, 2026 19:28
The cuesheet is a virtualised table: rows are unmounted once they leave
the viewport, taking the <img> elements with them. On the way back the
image has no dimensions until the browser makes it available, so the row
grows under the user as it loads.

We now remember the aspect ratio of the images we have seen and use it to
reserve the space the image will take. This holds two numbers per image:
the image data itself is left to the browser cache, which is better
placed than us to decide when memory should be released.

Also drops the lazy loading attribute: the row is only mounted when it is
already close to the viewport, so it only adds a gate before the request.

Whether the image is fetched again on scroll back is decided by the cache
headers of the host serving it, and cannot be worked around from here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CKXcDrZoQXbJpaXiLi1aff
A value which is not a link was discarded on blur without any feedback:
the text stayed in the field, so it looked like it had been saved. We now
mark the field and say what is expected. Paths served by ontime itself
(eg. /user/slide.png) are now accepted, they were rejected before.

An image which cannot be loaded showed a broken icon with no explanation.
This is what a dropbox share link does, since it serves an html page
rather than the image, so it is worth naming the problem.

We also reserve the space of an image while it is loading, using the
aspect ratio of the last time we saw it. Rows are unmounted while out of
view, so without it the row collapses and grows again on the way back,
shifting the table under the user.

Smaller items in the same cell: the lazy loading attribute only added a
gate before the request (the row is only mounted when it is already close
to the viewport), the image had no alt text, and two expressions could
never run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CKXcDrZoQXbJpaXiLi1aff
…oll-k26i4u' into claude/cuesheet-image-unload-scroll-k26i4u

# Conflicts:
#	apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/EditableImage.tsx
A path to a local file resolves on the machine running ontime, but not
for the clients we serve the cuesheet to, so we no longer accept it.

Validation now parses the value as a URL and checks the protocol, which
also rejects malformed values that the previous prefix check let through.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CKXcDrZoQXbJpaXiLi1aff
We were remembering the aspect ratio and reserving the full width of the
column, which over-reserves for an image smaller than the column: the row
would settle to a smaller size once the image was shown.

We now remember the size of the image and express the reservation in CSS
as min(100%, width), which is what the image itself resolves to at any
column width. Column sizes are applied as CSS variables and do not
re-render the cells, so the reservation follows a resize on its own.

Also groups the tests around the behaviour they describe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CKXcDrZoQXbJpaXiLi1aff
@cpvalente
cpvalente requested a lite review from Copilot August 30, 2026 11:32
@coderabbitai

coderabbitai Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 310e8c24-a31b-4f8d-9868-34016dac20fe

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cpvalente cpvalente changed the title Claude/cuesheet image unload scroll k26i4u refactor(cuesheet): improve image scrolling Aug 30, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

There are concrete security/performance regressions to address (scheme validation for window.open, and restoring lazy-loading behavior for images).

Pull request overview

This PR refactors cuesheet image handling to reduce table layout shifting during scroll (virtualized rows) and to improve URL validation/feedback for image sources.

Changes:

  • Add isValidImageSource validation (http/https) with UI feedback for rejected inputs.
  • Cache previously loaded image dimensions and reserve space on re-mount to prevent row-height shifts.
  • Add tests for URL validation and the image-dimensions LRU cache, and pass a column label through for alt text.
File summaries
File Description
apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/EditableImage.tsx Adds URL validation, load/error state, reserved-space rendering using remembered dimensions, and alt text support.
apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/EditableImage.module.scss Adds styling for invalid input state and user-visible error/rejection messages.
apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/cuesheetColsFactory.tsx Passes fieldLabel into EditableImage for accessibility (alt).
apps/client/src/views/cuesheet/tests/EditableImage.test.ts Adds unit tests for isValidImageSource.
apps/client/src/common/utils/imageDimensions.ts Introduces an in-memory (LRU-capped) cache for image dimensions to support reserved-space rendering.
apps/client/src/common/utils/tests/imageDimensions.test.ts Adds tests for remembering dimensions and LRU eviction behavior.
Review details

Suppressed comments (3)

apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/EditableImage.tsx:81

  • The validation error message says "(https://...)" but isValidImageSource accepts both http and https; this is misleading for users who paste an http URL and see it accepted.
        {isRejected && <span className={style.message}>Images are referenced by link (https://...)</span>}

apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/EditableImage.tsx:119

  • The previous implementation used loading='lazy', but the new <img> removed it. In a table view with many images, this can cause unnecessary network/CPU work and degrade scrolling performance.
        <img
          src={initialValue}
          alt={fieldLabel}
          className={style.image}
          onLoad={(event) => {

apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/EditableImage.tsx:58

  • openInNewTab opens whatever string is stored in initialValue; since this value can come from persisted data, it should be constrained to http(s) before calling window.open to avoid opening unexpected/unsupported URL schemes.

This issue also appears in the following locations of the same file:

  • line 81
  • line 115
  const openInNewTab = () => {
    if (initialValue) {
      window.open(initialValue, '_blank', 'noopener,noreferrer');
    }
  };
  • Files reviewed: 6/6 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants