拆分图片画布素材拖拽桥接

新增素材拖拽桥接 hook,承接素材拖向画布或文件夹的全局 pointer 监听

恢复认证弹窗 portal 渲染,避免全屏画布遮住账号入口

优化画布背景设置面板,补回当前色、色域、色相、预设、HEX 和恢复默认

补充素材拖拽、认证弹窗和背景面板回归测试并更新文档与 TRACKING
This commit is contained in:
2026-06-17 12:20:04 +08:00
parent 5d6be7fd66
commit cdc823611b
11 changed files with 544 additions and 133 deletions

View File

@@ -2,7 +2,7 @@
import { act, render, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
import type { AuthSessionSummary, AuthUser } from '../../services/authService';
@@ -250,6 +250,16 @@ function AccountPanelProbe() {
);
}
function AutoOpenLoginProbe() {
const authUi = useAuthUi();
useEffect(() => {
authUi?.openLoginModal();
}, [authUi]);
return <div></div>;
}
test('auth gate keeps platform content visible when phone login is available', async () => {
authMocks.getAuthLoginOptions.mockResolvedValue({
availableLoginMethods: ['phone'],
@@ -266,6 +276,22 @@ test('auth gate keeps platform content visible when phone login is available', a
expect(screen.queryByText('先登录账号,再同步你的冒险进度。')).toBeNull();
});
test('auth gate portals page-level login requests above fullscreen content', async () => {
const { container } = render(
<AuthGate>
<div className="image-canvas-editor"></div>
<AutoOpenLoginProbe />
</AuthGate>,
);
expect(await screen.findByText('编辑器内容')).toBeTruthy();
const dialog = await screen.findByRole('dialog', { name: '账号入口' });
expect(container.querySelector('[role="dialog"]')).toBeNull();
expect(dialog.parentElement?.parentElement).toBe(document.body);
expect(dialog.parentElement?.className).toContain('z-[120]');
});
test('auth gate waits for refresh cookie rotation before exposing restored user content', async () => {
let resolveToken!: (token: string) => void;
const tokenPromise = new Promise<string>((resolve) => {

View File

@@ -8,7 +8,7 @@ import { PlatformAuthModalShell } from './PlatformAuthModalShell';
test('renders auth modal shell with platform theme and auth card chrome', () => {
const onClose = vi.fn();
render(
const { container } = render(
<PlatformAuthModalShell
title="账号入口"
platformTheme="light"
@@ -22,6 +22,8 @@ test('renders auth modal shell with platform theme and auth card chrome', () =>
const dialog = screen.getByRole('dialog', { name: '账号入口' });
expect(container.querySelector('[role="dialog"]')).toBeNull();
expect(document.body.contains(dialog)).toBe(true);
expect(dialog.parentElement?.className).toContain('platform-theme--light');
expect(dialog.className).toContain('platform-modal-shell');
expect(dialog.className).toContain('platform-auth-card');

View File

@@ -55,7 +55,6 @@ export function PlatformAuthModalShell({
closeVariant="platformIcon"
closeOnBackdrop
closeOnEscape={false}
portal={false}
size={size}
showHeader={showHeader}
zIndexClassName={zIndexClassName}

View File

@@ -2025,7 +2025,11 @@ describe('ImageCanvasEditorView', () => {
const settingsPanel = screen.getByRole('dialog', {
name: '画布背景设置',
});
expect(within(settingsPanel).getByText('画布背景')).toBeTruthy();
expect(within(settingsPanel).getByText('画布背景')).toBeTruthy();
expect(within(settingsPanel).getByLabelText('画布背景色相')).toBeTruthy();
expect(
within(settingsPanel).getByLabelText('画布背景十六进制颜色'),
).toBeTruthy();
fireEvent.click(
within(settingsPanel).getByRole('button', { name: '暖灰' }),

View File

@@ -51,6 +51,7 @@ import { useCanvasHistory } from './useCanvasHistory';
import { useCanvasGenerationDialogs } from './useCanvasGenerationDialogs';
import { useImageCanvasAssetLibrary } from './useImageCanvasAssetLibrary';
import { useImageCanvasAssetExportWorkflow } from './useImageCanvasAssetExportWorkflow';
import { useImageCanvasAssetPointerDragBridge } from './useImageCanvasAssetPointerDragBridge';
import { useImageCanvasCanvasDropWorkflow } from './useImageCanvasCanvasDropWorkflow';
import { useImageCanvasEditorChrome } from './useImageCanvasEditorChrome';
import { useImageCanvasGenerationWorkflow } from './useImageCanvasGenerationWorkflow';
@@ -124,13 +125,6 @@ export function ImageCanvasEditorView() {
selectedLayerIdsRef.current = selectedLayerIds;
layersRef.current = layers;
viewportRef.current = viewport;
const assetsRef = useRef<EditorAsset[]>([]);
const addAssetLayerRef = useRef<
(asset: EditorAsset, screenCenter?: { x: number; y: number }) => void
>(() => {});
const moveAssetToFolderRef = useRef<
(assetId: string, folderId: string) => void
>(() => {});
authUiRef.current = authUi;
const openEditorLoginModal = useCallback(
(postLoginAction?: (() => void) | null) => {
@@ -268,10 +262,6 @@ export function ImageCanvasEditorView() {
onDeleteAssets: removeCanvasLayersLinkedToAssets,
});
useEffect(() => {
assetsRef.current = assets;
}, [assets]);
const handleActivateCanvasGenerationDialog = useCallback(() => {
setSelectedLayerId(null);
setSelectedLayerIds([]);
@@ -723,74 +713,6 @@ export function ImageCanvasEditorView() {
};
}, []);
useEffect(() => {
const updatePointerDrag = (event: PointerEvent) => {
const currentDrag = assetPointerDragRef.current;
if (!currentDrag || currentDrag.pointerId !== event.pointerId) {
return;
}
const distance = Math.hypot(
event.clientX - currentDrag.startClientX,
event.clientY - currentDrag.startClientY,
);
const dropFolderId = resolveAssetFolderId(event.clientX, event.clientY);
const nextDrag: AssetPointerDragState = {
...currentDrag,
currentClientX: event.clientX,
currentClientY: event.clientY,
active: currentDrag.active || distance > 4,
dropFolderId,
};
assetPointerDragRef.current = nextDrag;
setAssetPointerDrag(nextDrag);
setUploadDropTarget(
resolveCanvasPoint(event.clientX, event.clientY) ? 'canvas' : null,
);
updateAssetMoveDropFolder(dropFolderId);
};
const finishPointerDrag = (event: PointerEvent) => {
const currentDrag = assetPointerDragRef.current;
if (!currentDrag || currentDrag.pointerId !== event.pointerId) {
return;
}
const canvasPoint = resolveCanvasPoint(event.clientX, event.clientY);
const dropFolderId =
resolveAssetFolderId(event.clientX, event.clientY) ??
currentDrag.dropFolderId;
const draggedAsset = assetsRef.current.find(
(asset) => asset.id === currentDrag.assetId,
);
assetPointerDragRef.current = null;
setAssetPointerDrag(null);
setUploadDropTarget(null);
updateAssetMoveDropFolder(null);
if (!currentDrag.active || !draggedAsset) {
return;
}
suppressAssetClickRef.current = true;
window.setTimeout(() => {
suppressAssetClickRef.current = false;
}, 0);
if (dropFolderId && dropFolderId !== draggedAsset.folderId) {
moveAssetToFolderRef.current(draggedAsset.id, dropFolderId);
return;
}
if (canvasPoint) {
addAssetLayerRef.current(draggedAsset, canvasPoint);
}
};
window.addEventListener('pointermove', updatePointerDrag);
window.addEventListener('pointerup', finishPointerDrag);
window.addEventListener('pointercancel', finishPointerDrag);
return () => {
window.removeEventListener('pointermove', updatePointerDrag);
window.removeEventListener('pointerup', finishPointerDrag);
window.removeEventListener('pointercancel', finishPointerDrag);
};
}, []);
const addAssetLayer = (
asset: EditorAsset,
position?: { x: number; y: number },
@@ -811,9 +733,19 @@ export function ImageCanvasEditorView() {
selectSingleLayer(nextLayer.id);
setHoveredLayerId(null);
};
addAssetLayerRef.current = addAssetLayer;
moveAssetToFolderRef.current = moveAssetToFolder;
useImageCanvasAssetPointerDragBridge({
assetPointerDragRef,
suppressAssetClickRef,
assets,
resolveAssetFolderId,
resolveCanvasPoint,
setAssetPointerDrag,
setUploadDropTarget,
updateAssetMoveDropFolder,
moveAssetToFolder,
addAssetLayer,
});
deleteLayerByIdRef.current = deleteLayerById;

View File

@@ -881,7 +881,7 @@ export function ImageCanvasStageView({
aria-label="画布背景设置"
>
<div className="image-canvas-editor__background-panel-head">
<span></span>
<span></span>
<button
type="button"
className="image-canvas-editor__background-close"
@@ -891,6 +891,14 @@ export function ImageCanvasStageView({
<X className="h-4 w-4" aria-hidden="true" />
</button>
</div>
<div className="image-canvas-editor__background-current-row">
<span
className="image-canvas-editor__background-current-preview"
style={{ backgroundColor: canvasBackgroundColor }}
aria-hidden="true"
/>
<span>{canvasBackgroundColor}</span>
</div>
<label className="image-canvas-editor__background-spectrum">
<input
type="color"
@@ -939,27 +947,29 @@ export function ImageCanvasStageView({
</button>
))}
</div>
<label className="image-canvas-editor__background-hex-field">
<span>HEX</span>
<input
aria-label="画布背景十六进制颜色"
value={canvasBackgroundHexValue}
spellCheck={false}
onChange={(event) =>
onCanvasBackgroundHexChange(event.currentTarget.value)
<div className="image-canvas-editor__background-footer">
<label className="image-canvas-editor__background-hex-field">
<span>HEX</span>
<input
aria-label="画布背景十六进制颜色"
value={canvasBackgroundHexValue}
spellCheck={false}
onChange={(event) =>
onCanvasBackgroundHexChange(event.currentTarget.value)
}
/>
</label>
<button
type="button"
className="image-canvas-editor__background-reset"
onClick={() =>
onApplyCanvasBackgroundColor(DEFAULT_CANVAS_BACKGROUND_COLOR)
}
/>
</label>
<button
type="button"
className="image-canvas-editor__background-reset"
onClick={() =>
onApplyCanvasBackgroundColor(DEFAULT_CANVAS_BACKGROUND_COLOR)
}
>
<RotateCcw className="h-3.5 w-3.5" aria-hidden="true" />
</button>
>
<RotateCcw className="h-3.5 w-3.5" aria-hidden="true" />
</button>
</div>
</div>
) : null}
</div>

View File

@@ -0,0 +1,263 @@
/* @vitest-environment jsdom */
import { act, render, screen } from '@testing-library/react';
import { useRef, useState } from 'react';
import { describe, expect, it, vi } from 'vitest';
import type {
AssetPointerDragState,
EditorAsset,
} from './ImageCanvasEditorTypes';
import { useImageCanvasAssetPointerDragBridge } from './useImageCanvasAssetPointerDragBridge';
const defaultAssets: EditorAsset[] = [
{
id: 'asset-1',
label: '素材一',
src: 'data:image/png;base64,asset-1',
width: 320,
height: 240,
folderId: 'project',
sourceKind: 'uploaded',
sourceType: 'uploaded',
persisted: true,
},
];
function dispatchPointerEvent(
type: string,
init: MouseEventInit & { pointerId: number },
) {
const event = new MouseEvent(type, {
bubbles: true,
cancelable: true,
...init,
});
Object.defineProperty(event, 'pointerId', { value: init.pointerId });
window.dispatchEvent(event);
}
function AssetPointerDragBridgeHarness({
initialDrag,
assets = defaultAssets,
resolveAssetFolderId = vi.fn(() => null),
resolveCanvasPoint = vi.fn(() => null),
moveAssetToFolder = vi.fn(),
addAssetLayer = vi.fn(),
}: {
initialDrag?: AssetPointerDragState | null;
assets?: EditorAsset[];
resolveAssetFolderId?: (clientX: number, clientY: number) => string | null;
resolveCanvasPoint?: (
clientX: number,
clientY: number,
) => { x: number; y: number } | null;
moveAssetToFolder?: (assetId: string, folderId: string) => void;
addAssetLayer?: (asset: EditorAsset, position?: { x: number; y: number }) => void;
}) {
const assetPointerDragRef = useRef<AssetPointerDragState | null>(
initialDrag ?? {
assetId: 'asset-1',
pointerId: 7,
startClientX: 10,
startClientY: 10,
currentClientX: 10,
currentClientY: 10,
active: false,
dropFolderId: null,
},
);
const suppressAssetClickRef = useRef(false);
const [assetPointerDrag, setAssetPointerDrag] =
useState<AssetPointerDragState | null>(assetPointerDragRef.current);
const [uploadDropTarget, setUploadDropTarget] = useState<
'canvas' | 'assets' | null
>(null);
const [dropFolderId, updateAssetMoveDropFolder] = useState<string | null>(
null,
);
useImageCanvasAssetPointerDragBridge({
assetPointerDragRef,
suppressAssetClickRef,
assets,
resolveAssetFolderId,
resolveCanvasPoint,
setAssetPointerDrag,
setUploadDropTarget,
updateAssetMoveDropFolder,
moveAssetToFolder,
addAssetLayer,
});
return (
<div>
<span data-testid="drag-active">
{assetPointerDrag ? String(assetPointerDrag.active) : 'none'}
</span>
<span data-testid="drag-x">
{assetPointerDrag ? assetPointerDrag.currentClientX : 'none'}
</span>
<span data-testid="drop-target">{uploadDropTarget ?? '-'}</span>
<span data-testid="drop-folder">{dropFolderId ?? '-'}</span>
<span data-testid="suppressed">{String(suppressAssetClickRef.current)}</span>
</div>
);
}
describe('useImageCanvasAssetPointerDragBridge', () => {
it('activates sidebar asset drags and updates canvas and folder drop hints', () => {
const resolveAssetFolderId = vi.fn(() => 'folder-1');
const resolveCanvasPoint = vi.fn(() => ({ x: 100, y: 80 }));
render(
<AssetPointerDragBridgeHarness
resolveAssetFolderId={resolveAssetFolderId}
resolveCanvasPoint={resolveCanvasPoint}
/>,
);
act(() => {
dispatchPointerEvent('pointermove', {
pointerId: 7,
clientX: 18,
clientY: 18,
});
});
expect(screen.getByTestId('drag-active').textContent).toBe('true');
expect(screen.getByTestId('drag-x').textContent).toBe('18');
expect(screen.getByTestId('drop-target').textContent).toBe('canvas');
expect(screen.getByTestId('drop-folder').textContent).toBe('folder-1');
});
it('moves an active asset drag into a different folder on pointer up', () => {
const moveAssetToFolder = vi.fn();
render(
<AssetPointerDragBridgeHarness
initialDrag={{
assetId: 'asset-1',
pointerId: 7,
startClientX: 10,
startClientY: 10,
currentClientX: 30,
currentClientY: 30,
active: true,
dropFolderId: 'folder-1',
}}
resolveAssetFolderId={() => 'folder-1'}
moveAssetToFolder={moveAssetToFolder}
/>,
);
act(() => {
dispatchPointerEvent('pointerup', {
pointerId: 7,
clientX: 30,
clientY: 30,
});
});
expect(moveAssetToFolder).toHaveBeenCalledWith('asset-1', 'folder-1');
expect(screen.getByTestId('drag-active').textContent).toBe('none');
expect(screen.getByTestId('drop-target').textContent).toBe('-');
expect(screen.getByTestId('drop-folder').textContent).toBe('-');
expect(screen.getByTestId('suppressed').textContent).toBe('true');
});
it('adds an active dragged asset to the canvas when released over the canvas', () => {
const addAssetLayer = vi.fn();
render(
<AssetPointerDragBridgeHarness
initialDrag={{
assetId: 'asset-1',
pointerId: 7,
startClientX: 10,
startClientY: 10,
currentClientX: 48,
currentClientY: 64,
active: true,
dropFolderId: null,
}}
resolveCanvasPoint={() => ({ x: 480, y: 640 })}
addAssetLayer={addAssetLayer}
/>,
);
act(() => {
dispatchPointerEvent('pointerup', {
pointerId: 7,
clientX: 48,
clientY: 64,
});
});
expect(addAssetLayer).toHaveBeenCalledWith(
expect.objectContaining({ id: 'asset-1' }),
{ x: 480, y: 640 },
);
});
it('cleans up inactive drags without moving or adding assets', () => {
const moveAssetToFolder = vi.fn();
const addAssetLayer = vi.fn();
render(
<AssetPointerDragBridgeHarness
initialDrag={{
assetId: 'asset-1',
pointerId: 7,
startClientX: 10,
startClientY: 10,
currentClientX: 10,
currentClientY: 10,
active: false,
dropFolderId: 'folder-1',
}}
moveAssetToFolder={moveAssetToFolder}
addAssetLayer={addAssetLayer}
/>,
);
act(() => {
dispatchPointerEvent('pointerup', {
pointerId: 7,
clientX: 10,
clientY: 10,
});
});
expect(moveAssetToFolder).not.toHaveBeenCalled();
expect(addAssetLayer).not.toHaveBeenCalled();
expect(screen.getByTestId('drag-active').textContent).toBe('none');
});
it('uses the same finish path for pointer cancellation', () => {
const moveAssetToFolder = vi.fn();
const addAssetLayer = vi.fn();
render(
<AssetPointerDragBridgeHarness
initialDrag={{
assetId: 'asset-1',
pointerId: 8,
startClientX: 10,
startClientY: 10,
currentClientX: 24,
currentClientY: 24,
active: true,
dropFolderId: 'folder-1',
}}
moveAssetToFolder={moveAssetToFolder}
addAssetLayer={addAssetLayer}
/>,
);
act(() => {
dispatchPointerEvent('pointercancel', {
pointerId: 8,
clientX: 24,
clientY: 24,
});
});
expect(moveAssetToFolder).toHaveBeenCalledWith('asset-1', 'folder-1');
});
});

View File

@@ -0,0 +1,140 @@
import { type RefObject, useEffect, useRef } from 'react';
import type {
AssetPointerDragState,
EditorAsset,
} from './ImageCanvasEditorTypes';
type CanvasPoint = { x: number; y: number };
type UseImageCanvasAssetPointerDragBridgeOptions = {
assetPointerDragRef: RefObject<AssetPointerDragState | null>;
suppressAssetClickRef: RefObject<boolean>;
assets: EditorAsset[];
resolveAssetFolderId: (clientX: number, clientY: number) => string | null;
resolveCanvasPoint: (clientX: number, clientY: number) => CanvasPoint | null;
setAssetPointerDrag: (dragState: AssetPointerDragState | null) => void;
setUploadDropTarget: (target: 'canvas' | 'assets' | null) => void;
updateAssetMoveDropFolder: (folderId: string | null) => void;
moveAssetToFolder: (assetId: string, folderId: string) => void;
addAssetLayer: (asset: EditorAsset, position?: CanvasPoint) => void;
};
export function useImageCanvasAssetPointerDragBridge({
assetPointerDragRef,
suppressAssetClickRef,
assets,
resolveAssetFolderId,
resolveCanvasPoint,
setAssetPointerDrag,
setUploadDropTarget,
updateAssetMoveDropFolder,
moveAssetToFolder,
addAssetLayer,
}: UseImageCanvasAssetPointerDragBridgeOptions) {
const assetsRef = useRef(assets);
const callbacksRef = useRef({
resolveAssetFolderId,
resolveCanvasPoint,
setAssetPointerDrag,
setUploadDropTarget,
updateAssetMoveDropFolder,
moveAssetToFolder,
addAssetLayer,
});
useEffect(() => {
assetsRef.current = assets;
}, [assets]);
callbacksRef.current = {
resolveAssetFolderId,
resolveCanvasPoint,
setAssetPointerDrag,
setUploadDropTarget,
updateAssetMoveDropFolder,
moveAssetToFolder,
addAssetLayer,
};
useEffect(() => {
const updatePointerDrag = (event: PointerEvent) => {
const currentDrag = assetPointerDragRef.current;
if (!currentDrag || currentDrag.pointerId !== event.pointerId) {
return;
}
const {
resolveAssetFolderId: resolveFolder,
resolveCanvasPoint: resolvePoint,
setAssetPointerDrag: setPointerDrag,
setUploadDropTarget: setDropTarget,
updateAssetMoveDropFolder: updateDropFolder,
} = callbacksRef.current;
const distance = Math.hypot(
event.clientX - currentDrag.startClientX,
event.clientY - currentDrag.startClientY,
);
const dropFolderId = resolveFolder(event.clientX, event.clientY);
const nextDrag: AssetPointerDragState = {
...currentDrag,
currentClientX: event.clientX,
currentClientY: event.clientY,
active: currentDrag.active || distance > 4,
dropFolderId,
};
assetPointerDragRef.current = nextDrag;
setPointerDrag(nextDrag);
setDropTarget(resolvePoint(event.clientX, event.clientY) ? 'canvas' : null);
updateDropFolder(dropFolderId);
};
const finishPointerDrag = (event: PointerEvent) => {
const currentDrag = assetPointerDragRef.current;
if (!currentDrag || currentDrag.pointerId !== event.pointerId) {
return;
}
const {
resolveAssetFolderId: resolveFolder,
resolveCanvasPoint: resolvePoint,
setAssetPointerDrag: setPointerDrag,
setUploadDropTarget: setDropTarget,
updateAssetMoveDropFolder: updateDropFolder,
moveAssetToFolder: moveToFolder,
addAssetLayer: addLayer,
} = callbacksRef.current;
const canvasPoint = resolvePoint(event.clientX, event.clientY);
const dropFolderId =
resolveFolder(event.clientX, event.clientY) ?? currentDrag.dropFolderId;
const draggedAsset = assetsRef.current.find(
(asset) => asset.id === currentDrag.assetId,
);
assetPointerDragRef.current = null;
setPointerDrag(null);
setDropTarget(null);
updateDropFolder(null);
if (!currentDrag.active || !draggedAsset) {
return;
}
suppressAssetClickRef.current = true;
window.setTimeout(() => {
suppressAssetClickRef.current = false;
}, 0);
if (dropFolderId && dropFolderId !== draggedAsset.folderId) {
moveToFolder(draggedAsset.id, dropFolderId);
return;
}
if (canvasPoint) {
addLayer(draggedAsset, canvasPoint);
}
};
window.addEventListener('pointermove', updatePointerDrag);
window.addEventListener('pointerup', finishPointerDrag);
window.addEventListener('pointercancel', finishPointerDrag);
return () => {
window.removeEventListener('pointermove', updatePointerDrag);
window.removeEventListener('pointerup', finishPointerDrag);
window.removeEventListener('pointercancel', finishPointerDrag);
};
}, [assetPointerDragRef, suppressAssetClickRef]);
}

View File

@@ -4469,12 +4469,12 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
bottom: calc(100% + 0.55rem);
z-index: 24;
display: grid;
width: min(16rem, calc(100vw - 1.5rem));
gap: 0.5rem;
width: min(17.5rem, calc(100vw - 1.5rem));
gap: 0.62rem;
border: 1px solid rgba(203, 213, 225, 0.72);
border-radius: 0.86rem;
background: rgba(255, 255, 255, 0.98);
padding: 0.72rem;
border-radius: 0.92rem;
background: #ffffff;
padding: 0.75rem;
color: #1f2937;
box-shadow: 0 18px 42px rgba(15, 23, 42, 0.15);
}
@@ -4489,6 +4489,29 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
font-weight: 880;
}
.image-canvas-editor__background-current-row {
display: flex;
align-items: center;
gap: 0.5rem;
min-height: 2rem;
border: 1px solid #e2e8f0;
border-radius: 0.6rem;
background: #f8fafc;
padding: 0.35rem 0.45rem;
color: #475569;
font-size: 0.74rem;
font-weight: 820;
}
.image-canvas-editor__background-current-preview {
width: 1.24rem;
height: 1.24rem;
flex: 0 0 auto;
border: 1px solid #cbd5e1;
border-radius: 0.36rem;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.72);
}
.image-canvas-editor__background-close {
display: inline-flex;
width: 1.65rem;
@@ -4514,9 +4537,10 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
.image-canvas-editor__background-spectrum {
position: relative;
display: block;
height: 8.2rem;
height: 7.6rem;
overflow: hidden;
border-radius: 0.5rem;
border: 1px solid #e2e8f0;
border-radius: 0.58rem;
background:
linear-gradient(to top, #000000, transparent),
linear-gradient(to right, #ffffff, transparent),
@@ -4575,38 +4599,32 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
}
.image-canvas-editor__background-presets {
display: flex;
gap: 0.62rem;
overflow-x: auto;
padding: 0.08rem 0.05rem 0.18rem;
scrollbar-width: none;
}
.image-canvas-editor__background-presets::-webkit-scrollbar {
display: none;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.5rem;
padding: 0.02rem;
}
.image-canvas-editor__background-preset {
display: inline-flex;
width: 2rem;
height: 2rem;
flex: 0 0 auto;
width: 100%;
height: 2.15rem;
align-items: center;
justify-content: center;
border: 2px solid transparent;
border-radius: 999px;
background: transparent;
border: 1px solid #e2e8f0;
border-radius: 0.58rem;
background: #f8fafc;
padding: 0;
}
.image-canvas-editor__panel-dock .image-canvas-editor__background-preset {
width: 2rem;
height: 2rem;
width: 100%;
height: 2.15rem;
}
.image-canvas-editor__background-preset[aria-pressed='true'] {
border-color: #38bdf8;
background: #f8fafc;
background: #e0f2fe;
}
.image-canvas-editor__background-preset .image-canvas-editor__background-swatch {
@@ -4616,6 +4634,13 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.7);
}
.image-canvas-editor__background-footer {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 0.5rem;
}
.image-canvas-editor__background-hex-field {
display: flex;
align-items: center;
@@ -4646,7 +4671,8 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
.image-canvas-editor__background-reset {
display: inline-flex;
justify-self: start;
height: 1.9rem;
white-space: nowrap;
align-items: center;
gap: 0.34rem;
border: 1px solid #d7dfe9;