Skip to content

[6.x] Element index Thumb Size setting - #20124

Merged
brandonkelly merged 54 commits into
6.xfrom
element-index-thumb-size
Oct 27, 2026
Merged

brandonkelly merged 54 commits into
6.xfrom
element-index-thumb-size

Conversation

@brandonkelly

@brandonkelly brandonkelly commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

Description

Adds a Thumb Size setting to element index View menus, and redraws thumbnail view tiles as chips so they share the same sizes. Resolves #20083.

View menu ─ Thumb Size ─▶ ?thumbSize=md ─▶ ContentIndexViewModel::thumbSize()
                                              │
                                              ▼
                       <craft-chip thumb-size="md">   (size picked to suit: medium)
View Thumb sizes Default
Table / Structure sm md lg sm
Thumbnails sm md lg xl 2xl lg (unchanged from before)
Cards — —

<craft-chip> changes:

  • size stays on the shared small | medium | large scale.
  • New thumb-size="sm|md|lg|xl|2xl" sets the thumbnail (26, 60, 120, 180, 240px). A chip without its own size takes the one that suits it: sm→small, md→medium, lg+→large.
  • New prefix-placement="block-start" stacks the thumbnail above the label, with the checkbox and suffix along the top. Thumbnail views now use this instead of bespoke tile markup.
  • New select-disabled.

PHP gets a matching ChipThumbSize enum and a thumbSize option for ElementHtml::chipHtml() / elementChipHtml(), so transforms are requested at the thumbnail's actual size. Each view remembers its own size in the index's view state; switching views carries the new view's size along.

Commits:

  1. Orient fit thumbnails by their numeric size — <craft-thumbnail> compared string width/height ("80" >= "120"), so portrait images got a letterboxed square box and their rounded corners didn't show.
  2. Add thumb-size and prefix-placement to craft-chip — component + PHP API, file/folder icons scale with the thumbnail, plain chips no longer inset a leading thumbnail.
  3. Add a Thumb Size setting to element index View menus — view state, thumbSize param, ElementThumbs drawn as stacked chips.

Evidence

View menu, thumbnail view:

View menu with the Thumb Size setting

Thumbnail view at sm, md, lg (default), xl, 2xl:

Tile, sm Tile, md Tile, lg Tile, xl Tile, 2xl

Table view title chips at sm, md, lg:

Title chip, sm

Title chip, md

Title chip, lg

Tests:

thumbnail.test: orients a fit image by its numeric size, even when given strings
  before: ✗ (width="80", height="120" → landscape)   after: ✓ portrait
chip.browser.test: sizes the thumbnail by thumb-size, and the chip to suit it   ✓
chip.browser.test: stacks the prefix above the label, with the checkbox and suffix on top   ✓
ContentIndexControllerTest: renders title chips at the requested thumbnail size   ✓ (default, md, lg, xl→sm, unknown→sm)
ContentIndexControllerTest: renders tile thumbnails at the requested size   ✓ (default 120, sm 26, 2xl 240)
useElementIndexThumbSize.test: per-view defaults, persistence, restore, URL precedence   ✓

Related issues

brandonkelly and others added 30 commits October 26, 2026 07:16
Image analysis tasks add instructions and JSON schema properties to a
shared prompt, and apply their part of the response to the asset. Tasks
that resolve to the same provider and model share one request per image,
with a scaled-down copy of it. Images are queued for analysis when
they're uploaded, replaced, or indexed, and with a new "Analyze images"
action, all of which only fill in what images are missing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Craft writes alt text for images in the language of each site with its
own alt text, per the volume's alt text translation method, filling in
images that are missing it when they're analyzed. Alternative Text
fields get a "Generate alt text" action that fills in a suggestion for
editors to review before saving, through a new generic
craft:generate-field-value field action event.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Craft sets the focal points of images that don't have one when they're
analyzed, so their crops keep the subject in view. The point is kept
within the image, and the asset's transforms are invalidated when it
changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Assets get searchable `keywords` and `visibleText` attributes, stored in
new `assets` columns and exposed to GraphQL. The "Tag images" AI task
fills them in for images that haven't been tagged, with keywords in the
primary site's language and any text that appears in the image.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Image colors now include a palette of the image's main colors, sampled
on the server and in the browser for uploads, with labels() naming them
in plain words such as "red" or "teal" via a new ColorNames helper.
Images are searchable by those names, the palette and labels are exposed
to GraphQL, and images sampled before palettes were get sampled again
the next time they're indexed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saving and indexing assets now check which image analysis tasks can run,
which reads cached capability checks. Injecting AiManager there failed
where laravel/ai isn't booted, such as the Yii2 adapter's tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	src/Asset/Elements/Asset.php
#	src/Element/Queries/AssetQuery.php
# Conflicts:
#	src/Asset/Elements/Asset.php
#	src/Element/Queries/AssetQuery.php
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	src/Asset/Elements/Asset.php
#	src/Database/Migrations/Install.php
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PxhedKGW7eCLF4xLBVmh6o
They're listed in DEFAULT_TYPES, like other registries' built-in types,
rather than registered from the asset service provider.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PxhedKGW7eCLF4xLBVmh6o
An image whose palette is red can now be found by searching rot on a
German site, as well as red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PxhedKGW7eCLF4xLBVmh6o
Images' colors, and the keywords and text the Tag images AI task finds,
are now stored together in assets' analysis column, read through
Asset::$analysis (an ImageAnalysis) and the analysis GraphQL field.
Asset::$colors and the colors GraphQL field have moved there.

Replacing an image's file, or indexing one that's changed, now starts
its analysis over with only the new colors, so the new file gets tagged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PxhedKGW7eCLF4xLBVmh6o
Indexing only samples colors and queues AI analysis for new and changed
files, unless the Asset Indexes utility's Analyze images option, or the
index-assets commands' --analyzeImages option, is on. Then it also does
both for unchanged images that are missing something. It's stored on
indexing sessions, since the utility indexes over several requests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PxhedKGW7eCLF4xLBVmh6o
Image colors now include each region's 10th and 90th percentile WCAG
relative luminance, sampled on the server and in the browser alike, and
ImageColors::textColor() picks white or black text for the whole image or
one of its edges, with the contrast ratio, so templates can tell when text
needs a scrim. Also available as the luminance and textColor GraphQL fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PxhedKGW7eCLF4xLBVmh6o
Images now store the title, description, alt text, creator, credit,
copyright, keywords, date taken, and camera embedded in their XMP, IPTC, and
EXIF data in analysis.metadata, and whether the file has GPS coordinates in
it, without the coordinates. Uploads are read before they're cleaned, and the
GPS flag is checked again after cleaning, so it reflects the stored file.
Indexing reads unchanged images that are missing metadata when asked to
analyze images. Embedded keywords are searchable.

Alt text from metadata now comes from the same reader, which also converts
text that isn't UTF-8 without erroring.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PxhedKGW7eCLF4xLBVmh6o
Titles images that are still titled after their filename, such as
“IMG 4759”, in each site's language, sharing one title between sites that
share titles, like Generate alt text does for alt text. Asset::hasDefaultTitle()
says whether a title is still the filename one.

New uploads also take their title from their embedded metadata, if they have
one, so the metadata is now read in beforeSave(), where titles are set, which
is still before the file is cleaned.

AltTextSite is now TranslatedSite, shared by both tasks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PxhedKGW7eCLF4xLBVmh6o
Finds what kind of image each image is (photo, illustration, screenshot,
diagram, logo, document, or pattern) and stores it in analysis.type, as an
ImageType. It's searchable by the type's name and its label in the site's
language, and available as the type GraphQL field.

Image analysis tasks that both change an asset's analysis in the same request,
like this and Tag images, now build on each other's changes through
ImageAnalysisChanges::get(), rather than the last one replacing the others'.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PxhedKGW7eCLF4xLBVmh6o
Now that missing metadata also means an image is sampled again, the tests
for images sampled before palettes or luminance store metadata, so they
only pass because of the part they're about.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PxhedKGW7eCLF4xLBVmh6o
Image types are indexed as the imageType search attribute, so searches like
imageType:logo find only images classified as logos, rather than as part of
keywords, where they mixed with AI and embedded tags. Plain searches still
find them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PxhedKGW7eCLF4xLBVmh6o
Image types are no longer a search attribute; keywords hold only AI and
embedded keywords. Instead, a generated, indexed assets.imageType column
mirrors analysis.type, and it backs AssetQuery::imageType(), the “Image
Type” asset condition rule, and the imageType GraphQL argument.
Asset::getImageType() returns an image's type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PxhedKGW7eCLF4xLBVmh6o
Asset search keywords now only add the keywords and text found in images to
what was already indexed, so the colors search attribute is gone.
ColorNames::label() still translates color names for display.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PxhedKGW7eCLF4xLBVmh6o
brandonkelly and others added 11 commits October 27, 2026 05:48
The browser sampler seeded its k-means++ clustering with mulberry32, while
Intervention draws from PHP's Mt19937 Randomizer, so the two started from
different centroids and found different palettes and dominant colors for the
same image. Port PHP's Randomizer over Mt19937 and use it in the browser,
and test both samplers against the same fixture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PxhedKGW7eCLF4xLBVmh6o
Control panel uploads now read images' embedded metadata in the browser,
with a port of ImageMetadataReader, and send it with their colors, so the
server can store both without reading the file.

Images staged on a remote disk are no longer downloaded unless they're
cleaned (metadata is read first, so cleaning can't lose it) or their
dimensions can't be read from the start of the file. If the uploader sent
no colors or metadata, the AnalyzeImage job samples them instead; it's
otherwise only queued when AI tasks are pending. New assets for files
already in place, as direct uploads leave them, are queued the same way.

Staged uploads are copied into their volume within S3 when both disks
share credentials and an endpoint, instead of being downloaded and
uploaded again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PxhedKGW7eCLF4xLBVmh6o
Upload completion requests now send the browser's colors and metadata as
`analysis`, shaped like the analysis they're stored in, and
`Asset::$uploadColors` is replaced by `$uploadAnalysis`. Only the colors
and metadata are taken from it; what AI tasks find can't be set from a
request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PxhedKGW7eCLF4xLBVmh6o
It samples missing metadata as well as colors. Shortened the help text and
command option descriptions, and added the option's strings to en/app.php.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PxhedKGW7eCLF4xLBVmh6o
Vue sets <craft-thumbnail>'s width and height as properties straight from
the template's strings, and compared as strings "80" >= "120", so a
portrait image got a landscape box: a square that letterboxed it, hiding
its rounded corners. Compare them as numbers.

Amp-Thread-ID: https://ampcode.com/threads/T-01a17da9-ff5c-7129-89d3-a833d37fee76
thumb-size sizes a chip's thumbnail on its own scale (sm through 2xl, with
lg matching a card's thumbnail), and a chip without a size of its own takes
the one that suits it. prefix-placement="block-start" stacks the prefix
above the label, for tiles in a grid, with the checkbox and suffix in a row
along the top. select-disabled disables the selection checkbox.

ElementHtml gains a matching thumbSize config option, backed by the new
ChipThumbSize enum, so server-rendered chips request transforms at their
thumbnail's size. File and folder icons grow with the thumbnail, and plain
chips no longer inset a leading thumbnail.

Amp-Thread-ID: https://ampcode.com/threads/T-01a17da9-ff5c-7129-89d3-a833d37fee76
Table and structure views offer sm, md, and lg thumbnails, small by
default; thumbnail views offer all five sizes, lg by default, matching
their previous size. Each view's size is kept in the index's view state
and sent as a thumbSize param, so the server renders the thumbnails at it,
and switching views brings the new view's size along.

Thumbnail tiles are now <craft-chip prefix-placement="block-start">,
sized by thumb-size, with the grid's columns widening to suit.

Amp-Thread-ID: https://ampcode.com/threads/T-01a17da9-ff5c-7129-89d3-a833d37fee76
# Conflicts:
#	resources/js/modules/uploader/asset-upload-queue.ts
@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

📚 Storybook previews

@craftcms/ui — open Storybook

Changed components:

resources/js — open Storybook

Changed components:

brandonkelly and others added 13 commits October 27, 2026 11:08
Asset edit pages' Info panels now show what was found in an image as a
separate group after the rest of its metadata: its type, a bar of its main
colors sized by how much of the image each covers, its embedded title, alt
text, creator, credit, copyright, date taken, and camera, and where it was
taken, with a map drawn from OpenStreetMap tiles (inverted for the dark
theme) and a Google Maps link. The asset's folder is now labeled "Folder"
everywhere. Asset indexes get matching table columns and card attributes.

Image metadata now includes GPS coordinates, read before uploads are
cleaned (kept out of GraphQL and sandboxed templates), and the city, state,
and country, in both the PHP reader and its browser port; `hasLocation` is
gone. Image colors now include palette weights, sampled the same way on the
server and in the browser. Embedded descriptions are searchable along with
embedded keywords. The PHP reader no longer misses XMP packets that end
past the first 8 KB of a file, since fread() can return less than asked.

Elements can show groups of metadata, separated by a rule, through
ElementInterface::getMetadataGroups(), and ContentHtml::metadataHtml()
takes several groups and lists of values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PxhedKGW7eCLF4xLBVmh6o
…t text

Uploads and the analysis queue job now fill in an image's alt text and
title from its embedded metadata through Asset::valuesFromEmbeddedMetadata(),
with ImageMetadata::suggestedAlt(), so they follow the same rules: titles
are filled in whenever they're still the filename one, including when a
file is replaced.

Adds the assets.analyze MCP tool, which queues images for the image
analysis tasks that are set up, and lets content-model audit checks return
notes; the missing-alt-text check's note says whether "Generate alt text"
is set up, and to use assets.analyze if so.

The AI docs and docblock examples use a plugin task that doesn't overlap
with Craft's own image tasks, and point image tasks at ImageAnalysisTask.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PxhedKGW7eCLF4xLBVmh6o
Covers thumb-size at each step, an explicit size overriding the one
thumb-size picks, select-disabled on the Selectable story, and a portrait
thumbnail with string width/height in chips at each thumb-size.

Amp-Thread-ID: https://ampcode.com/threads/T-01a17da9-ff5c-7129-89d3-a833d37fee76
Co-authored-by: Amp <amp@ampcode.com>
The View menu listed table columns in the thumbnail and cards views too,
where they do nothing. The legacy index hides them there.

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a17da9-ff5c-7129-89d3-a833d37fee76
craft-popover capped its pane at 40vh, so a View menu opening near the
top of the page scrolled its Table Columns behind the footer with most
of the window free below it. A Popper modifier now measures the room
between the pane and the viewport edge it opens toward, and the pane
uses that as its max height. --popover-max-block-size still wins, and
40vh remains the fallback when there's nothing to measure.

Amp-Thread-ID: https://ampcode.com/threads/T-01a17da9-ff5c-7129-89d3-a833d37fee76
Co-authored-by: Amp <amp@ampcode.com>
Changing the Thumb Size reloaded the whole result view. The View menu now
resizes the chips already on screen and scales their thumbnails to fill
the new size, then quietly re-requests the rows so thumbnails rendered
for that size replace them. The element selector modal's visitor now
honors silent visits too.

Amp-Thread-ID: https://ampcode.com/threads/T-01a17da9-ff5c-7129-89d3-a833d37fee76
Co-authored-by: Amp <amp@ampcode.com>
The Chip docs page embeds stories by hand, so the new ones only showed
in the sidebar. Adds Thumbnail Size and Prefix Placement sections, and
the select-disabled example under Accessibility. The thumbnail's string
width/height check moves from a story's play function to a browser
test, leaving the story as a docs example.

Amp-Thread-ID: https://ampcode.com/threads/T-01a17da9-ff5c-7129-89d3-a833d37fee76
Co-authored-by: Amp <amp@ampcode.com>
A medium chip's thumbnail was inset to keep the chip as tall as a medium
button, which with medium's larger padding left it at 22px, smaller than
a small chip's 26px. It now keeps its own 32px. The chip docs say how
the thumbnail steps with size, and that thumb-size sets a larger one.

Amp-Thread-ID: https://ampcode.com/threads/T-01a17da9-ff5c-7129-89d3-a833d37fee76
Co-authored-by: Amp <amp@ampcode.com>
@brandonkelly
brandonkelly merged commit dd8917f into 6.x Oct 27, 2026
20 checks passed
@brandonkelly
brandonkelly deleted the element-index-thumb-size branch October 27, 2026 23:18
@linear-code

linear-code Bot commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

CMS-2578

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant