Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
58f7c41
Detect if an image/SVG is predominantly dark or light by sampling pixels
jaygeorge Feb 17, 2026
d216d39
Round the checkerboard corners a little
jaygeorge Feb 17, 2026
3e581ff
Account for dark mode in the image editor
jaygeorge Feb 17, 2026
236b24e
Detect image tone (light/dark) server-side on upload. The asset edito…
jaygeorge Feb 18, 2026
1d6ab58
Correct SVG tone detection - now uses native Imagick (when available)…
jaygeorge Feb 18, 2026
46374aa
Add a badge to the editor to show if image tones are Light or Dark
jaygeorge Feb 19, 2026
26088ec
Show light/dark background on asset tiles to make more use of it
jaygeorge Feb 19, 2026
c5f99ed
Remove checkerboard on hover, now that we're typically using a toggle…
jaygeorge Feb 19, 2026
864daac
Add transparency toggle button for Asset fieldtype too
jaygeorge Feb 19, 2026
aee3283
Add some fallbacks for understanding whether SVGs are light or dark
jaygeorge Feb 19, 2026
c2cc4e8
Tidy - move tone detection into a separate file
jaygeorge Feb 19, 2026
6b7199a
Add a comment to explain why we're using canvas as a fallback in the …
jaygeorge Feb 19, 2026
00d606e
remove the alternate transparency toggle - it's fixed on the main bra…
jasonvarga Mar 5, 2026
13ef55f
Merge branch '6.x' into image-brightness-detection
jasonvarga Mar 5, 2026
9ec10d4
fix transparency checks based on threshold
jasonvarga Mar 5, 2026
c4f1a3e
fix tests
jasonvarga Mar 5, 2026
707884f
tone detection tests
jasonvarga Mar 5, 2026
722a2c7
explicitly enable imagick extension
jasonvarga Mar 5, 2026
ea1842b
skip instead of exception (but doesnt get skipped on ci now)
jasonvarga Mar 5, 2026
64ac738
wire up proper checkerboard bg in browser listing (it was already on …
jasonvarga Mar 5, 2026
77270e5
Merge branch '6.x' into image-brightness-detection
jaygeorge Mar 6, 2026
45638c9
Revert the checkerboard background always showing, it's too noisy, Ja…
jaygeorge Mar 6, 2026
eb7d32b
Revert "Show light/dark background on asset tiles to make more use of…
jaygeorge Mar 6, 2026
e89b1a0
Add a tone override option
jaygeorge Mar 6, 2026
4ebe4f1
Fix SVG brightness detection
jaygeorge Mar 6, 2026
d3cf785
Revert "Fix SVG brightness detection"
jaygeorge Mar 6, 2026
7afb4b8
Remove image brightness detection and merge the transparency button w…
jaygeorge Mar 6, 2026
b4da719
Improve dark separators
jaygeorge Mar 6, 2026
02ae955
Rename classes to reflect background rather than image tone
jaygeorge Mar 6, 2026
f8db944
Default the checkerboard to whatever the cp theme is
jaygeorge Mar 6, 2026
c4e64bf
Cycle the checkerboard like this: Current CP color, Alt CP color, Tra…
jaygeorge Mar 6, 2026
d3b5884
Fix "loading" background of dark asset editor
jaygeorge Mar 6, 2026
66c3327
Make dark checkerboard less harsh/contrasty
jaygeorge Mar 6, 2026
8d9819e
Improve dark disabled filled color (based on asset pagination)
jaygeorge Mar 6, 2026
ede245f
Add transparency toggle for editor and browser grid, defaulted to off…
jaygeorge Mar 6, 2026
b6b30c6
Hard code the checkerboard values, I don't think these should be them…
jaygeorge Mar 6, 2026
8231ffb
Merge branch '6.x' into image-brightness-detection
jasonvarga Mar 6, 2026
5e363a9
revert php changes
jasonvarga Mar 6, 2026
f2a476b
missed one
jasonvarga Mar 6, 2026
bcf60ba
checkerboard button should be the browser component, not in the primi…
jasonvarga Mar 6, 2026
90b6c00
Remove the transparency label from the assets container, instead give…
jaygeorge Mar 6, 2026
0530251
Fix margin to the right of the slider
jaygeorge Mar 6, 2026
c58d3c8
unnecessary changes
jasonvarga Mar 6, 2026
d915b63
more
jasonvarga Mar 6, 2026
764d08c
Keep the hover behaviour from before
jaygeorge Mar 6, 2026
9ecd776
Nicer CSS
jaygeorge Mar 6, 2026
f6dc594
Toggle classes rather than add more CSS
jaygeorge Mar 7, 2026
89ec930
Remove unnecessary CSS
jaygeorge Mar 7, 2026
dc7beff
Simplify CSS
jaygeorge Mar 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions resources/css/components/assets.css
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,8 @@
========================================================================== */

.asset-editor {

.loading {
@apply absolute inset-0 flex items-center justify-center rounded-sm bg-white/70;
@apply absolute inset-0 flex items-center justify-center rounded-sm bg-gray-800 dark:bg-gray-900;
z-index: var(--z-index-portal);
}

Expand All @@ -184,6 +183,18 @@
}
}

/* Checkerboard light/dark by tone */
.asset-editor .light .bg-checkerboard,
.asset-browser-grid .light.bg-checkerboard {
background-color: #fff;
&::before { background-image: var(--checkerboard-light); }
}
.asset-editor .dark .bg-checkerboard,
.asset-browser-grid .dark.bg-checkerboard {
background-color: #1c1c1e;
&::before { background-image: var(--checkerboard-dark); }
}

/* Lazyloading
========================================================================== */

Expand Down
4 changes: 2 additions & 2 deletions resources/css/core/utilities.css
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@
=================================================== */
@utility bg-checkerboard {
--checkerboard-light: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0h12v12H0zm12 12h12v12H12z' fill='%23f3f4f6'/%3E%3C/svg%3E");
--checkerboard-dark: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0h12v12H0zm12 12h12v12H12z' fill='%23000000'/%3E%3C/svg%3E");
--checkerboard-dark: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0h12v12H0zm12 12h12v12H12z' fill='%230a0a0a'/%3E%3C/svg%3E");

position: relative;
background-color: #fff;
Expand All @@ -357,7 +357,7 @@
}

.dark & {
background-color: var(--color-gray-850);
background-color: #1c1c1e;

&::before {
background-image: var(--checkerboard-dark);
Expand Down
51 changes: 39 additions & 12 deletions resources/js/components/assets/Browser/Browser.vue
Original file line number Diff line number Diff line change
Expand Up @@ -98,23 +98,33 @@
/>

<Panel v-else :class="{ 'relative overflow-x-auto overscroll-x-contain': mode === 'table' }">
<PanelHeader class="flex items-center justify-between px-1!">
<PanelHeader class="flex items-center justify-between gap-2 px-1!">
<Breadcrumbs
v-if="!restrictFolderNavigation"
:path="path"
@navigated="selectFolder"
/>

<Slider
v-if="mode === 'grid'"
size="sm"
class="me-2 w-24!"
variant="subtle"
v-model="gridThumbnailSize"
:min="60"
:max="300"
:step="25"
/>
<div v-if="mode === 'grid'" class="flex items-center gap-2 mr-2">
<ui-button
inset
size="sm"
variant="ghost"
icon-only
:icon="checkerboardIcon"
v-tooltip="__('Transparency')"
:aria-label="__('Transparency')"
@click="onCheckerboardToggled((checkerboardMode + 1) % 3)"
/>
<Slider
size="sm"
class="w-24!"
variant="subtle"
v-model="gridThumbnailSize"
:min="60"
:max="300"
:step="25"
/>
</div>
</PanelHeader>

<Uploads
Expand Down Expand Up @@ -144,6 +154,7 @@
:action-url="actionUrl"
:thumbnail-size="gridThumbnailSize"
:selected-assets="selectedAssets"
:checkerboard-mode="checkerboardMode"
v-bind="sharedAssetProps"
v-on="sharedAssetEvents"
/>
Expand Down Expand Up @@ -290,6 +301,7 @@ export default {
lastItemClicked: null,
preventDragging: false,
gridThumbnailSize: this.$preferences.get('assets.browser_thumbnail_size', 200),
checkerboardMode: this.$preferences.get('assets.browser_checkerboard_mode', 2),
};
},

Expand Down Expand Up @@ -386,6 +398,16 @@ export default {
return Boolean(this.editedAssetId);
},

isCpDark() {
return typeof document !== 'undefined' && document.documentElement.classList.contains('dark');
},

checkerboardIcon() {
if (this.checkerboardMode === 0) return this.isCpDark ? 'moon' : 'sun';
if (this.checkerboardMode === 1) return this.isCpDark ? 'sun' : 'moon';
return 'eye-slash';
},

sharedAssetProps() {
return {
actionUrl: this.actionUrl,
Expand Down Expand Up @@ -484,6 +506,11 @@ export default {
},

methods: {
onCheckerboardToggled(mode) {
this.checkerboardMode = mode;
this.$preferences.set('assets.browser_checkerboard_mode', mode);
},

filtersUpdated(filters) {
this.activeFilters = filters;
},
Expand Down
29 changes: 24 additions & 5 deletions resources/js/components/assets/Browser/Grid.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<ui-card :class="{
<ui-card class="asset-browser-grid" :class="{
'space-y-8': folders.length || assets.length || creatingFolder,
'!p-0': folders.length === 0 && assets.length === 0 && !creatingFolder
}">
Expand Down Expand Up @@ -102,10 +102,16 @@
<template #trigger>
<div
class="asset-tile group relative bg-white dark:bg-gray-900"
:class="{
'bg-checkerboard!': asset.can_be_transparent,
'opacity-50!': draggingAsset === asset.id,
}"
:class="[
{
'opacity-50!': draggingAsset === asset.id,
},
asset.can_be_transparent
? (showCheckerboard
? `${previewBackgroundClass} bg-checkerboard before:opacity-100`
: ['bg-checkerboard', 'isolate', asset.tone === 'dark' ? 'light' : asset.tone === 'light' ? 'dark' : null].filter(Boolean))
: '',
]"
>
<button
class="size-full"
Expand Down Expand Up @@ -227,6 +233,8 @@ export default {
assets: { type: Array },
selectedAssets: { type: Array },
thumbnailSize: { type: Number },
/** 0 = current CP color, 1 = alt CP color, 2 = transparent */
checkerboardMode: { type: Number, default: 2 },
},

data() {
Expand All @@ -240,6 +248,17 @@ export default {
gridSize() {
return `repeat(auto-fill, minmax(${this.thumbnailSize}px, 1fr))`;
},
showCheckerboard() {
return this.checkerboardMode !== 2;
},
isCpDark() {
return typeof document !== 'undefined' && document.documentElement.classList.contains('dark');
},
previewBackgroundClass() {
if (this.checkerboardMode === 0) return this.isCpDark ? 'dark' : 'light';
if (this.checkerboardMode === 1) return this.isCpDark ? 'light' : 'dark';
return '';
},
},

setup() {
Expand Down
42 changes: 37 additions & 5 deletions resources/js/components/assets/Editor/Editor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,16 @@
@completed="actionCompleted"
v-slot="{ actions }"
>
<ui-button
inset size="sm" variant="ghost"
v-if="asset.can_be_transparent"
:icon="checkerboardIcon"
class="[&_svg]:!opacity-45"
:text="__('Transparency')"
@click="cycleCheckerboard"
/>
<ui-button inset size="sm" v-if="asset.isEditable && isImage && isFocalPointEditorEnabled" @click.prevent="openFocalPointEditor" icon="focus" variant="ghost" class="[&_svg]:!opacity-45" :text="__('Focal Point')" />
<ui-button inset size="sm" v-if="canCrop" @click.prevent="openCropEditor" icon="crop" variant="ghost" class="[&_svg]:!opacity-45" :text="__('Crop')" />
<ui-button inset size="sm" v-if="asset.can_be_transparent" @click="showCheckerboard = !showCheckerboard" icon="eye" variant="ghost" :class="[showCheckerboard ? '[&_svg]:!opacity-45' : '[&_svg]:!opacity-100']" :text="__('Transparency')" />
<ui-button inset size="sm" v-if="canRunAction('rename_asset')" @click.prevent="runAction(actions, 'rename_asset')" icon="rename" variant="ghost" class="[&_svg]:!opacity-45" :text="__('Rename')" />
<ui-button inset size="sm" v-if="canRunAction('move_asset')" @click.prevent="runAction(actions, 'move_asset')" icon="move-folder" variant="ghost" class="[&_svg]:!opacity-45" :text="__('Move to Folder')" />
<ui-button inset size="sm" v-if="canRunAction('replace_asset')" @click.prevent="runAction(actions, 'replace_asset')" icon="replace" variant="ghost" class="[&_svg]:!opacity-45" :text="__('Replace')" />
Expand All @@ -67,14 +74,15 @@
<div
v-if="asset.isImage || asset.isSvg || asset.isAudio || asset.isVideo || asset.preview"
class="flex flex-1 flex-col justify-center items-center p-8 h-full min-h-0"
:class="previewBackgroundClass"
>
<!-- Image -->
<div v-if="asset.isImage" class="max-w-full max-h-full" :class="{ 'bg-checkerboard before:opacity-100': asset.can_be_transparent && showCheckerboard }">
<div v-if="asset.isImage" class="max-w-full max-h-full" :class="{ 'bg-checkerboard before:opacity-100 rounded-md': asset.can_be_transparent && showCheckerboard }">
<img :src="asset.preview" class="relative asset-thumb shadow-ui-xl max-w-full max-h-full object-contain" />
</div>

<!-- SVG -->
<div v-else-if="asset.isSvg" class="flex h-full w-full flex-col shadow-ui-xl">
<div v-else-if="asset.isSvg" class="flex h-full w-full flex-col shadow-ui-xl dark:bg-gray-800">
<div class="grid grid-cols-3 gap-1">
<div class="flex items-center justify-center p-3 aspect-square" :class="{ 'bg-checkerboard before:opacity-100': showCheckerboard }">
<img :src="asset.url" class="asset-thumb relative z-10 w-4" />
Expand Down Expand Up @@ -201,7 +209,7 @@ import {
PublishContainer,
PublishTabs,
Icon,
Stack,
Stack,
} from '@ui';
import ItemActions from '@/components/actions/ItemActions.vue';

Expand Down Expand Up @@ -252,7 +260,7 @@ export default {
fieldset: null,
showFocalPointEditor: false,
showCropEditor: false,
showCheckerboard: true,
checkerboardMode: 0, // 0 = current CP color, 1 = alt CP color, 2 = transparent
error: null,
errors: {},
actions: [],
Expand Down Expand Up @@ -290,6 +298,26 @@ export default {
isFocalPointEditorEnabled() {
return Statamic.$config.get('focalPointEditorEnabled');
},

isCpDark() {
return typeof document !== 'undefined' && document.documentElement.classList.contains('dark');
},

showCheckerboard() {
return this.checkerboardMode !== 2;
},

checkerboardIcon() {
if (this.checkerboardMode === 0) return this.isCpDark ? 'moon' : 'sun';
if (this.checkerboardMode === 1) return this.isCpDark ? 'sun' : 'moon';
return 'eye-slash';
},

previewBackgroundClass() {
if (this.checkerboardMode === 0) return this.isCpDark ? 'dark' : 'light';
if (this.checkerboardMode === 1) return this.isCpDark ? 'light' : 'dark';
return '';
},
},

mounted() {
Expand Down Expand Up @@ -399,6 +427,10 @@ export default {
this.$dirty.add(this.publishContainer);
},

cycleCheckerboard() {
this.checkerboardMode = (this.checkerboardMode + 1) % 3;
},

openCropEditor() {
this.showCropEditor = true;
},
Expand Down
2 changes: 1 addition & 1 deletion resources/js/components/ui/Button/Button.vue
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ const buttonClasses = computed(() => {
'bg-linear-to-b from-primary/90 to-primary hover:bg-primary-hover text-white disabled:opacity-60 disabled:text-white dark:disabled:text-white border border-primary-border shadow-ui-md inset-shadow-2xs inset-shadow-white/25 disabled:inset-shadow-none dark:disabled:inset-shadow-none [&_svg]:text-white [&_svg]:opacity-60',
],
danger: 'bg-linear-to-b from-red-600/90 to-red-600 hover:bg-red-600/90 text-white border border-red-600 inset-shadow-xs inset-shadow-red-300 [&_svg]:text-red-200 disabled:text-white! disabled:opacity-60 disabled:inset-shadow-none',
filled: 'bg-gray-950/5 hover:bg-gray-950/10 hover:text-gray-900 dark:hover:text-white dark:bg-white/15 dark:hover:bg-white/20 [&_svg]:opacity-70',
filled: 'bg-gray-950/5 hover:bg-gray-950/10 hover:text-gray-900 dark:hover:text-white dark:bg-white/4 dark:hover:bg-white/20 [&_svg]:opacity-70',
ghost: 'bg-transparent hover:bg-gray-400/10 text-gray-900 dark:text-gray-300 dark:hover:bg-white/7 dark:hover:text-gray-200',
'ghost-pressed': 'bg-transparent hover:bg-gray-400/10 text-gray-925 dark:text-white dark:hover:bg-white/7 dark:hover:text-white [&_svg]:opacity-100',
subtle: 'bg-transparent hover:bg-gray-400/10 text-gray-500 hover:text-gray-700 dark:text-gray-300 dark:hover:bg-white/7 dark:hover:text-gray-200 [&_svg]:opacity-35',
Expand Down