diff --git a/frontend/src/components/FloatingPanel.tsx b/frontend/src/components/FloatingPanel.tsx index 3164db9..c21e519 100644 --- a/frontend/src/components/FloatingPanel.tsx +++ b/frontend/src/components/FloatingPanel.tsx @@ -1,5 +1,6 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import type { MutableRefObject, PointerEvent as ReactPointerEvent, ReactNode } from 'react'; +import { createPortal } from 'react-dom'; import { DockSide, FloatingPanelFrame, FloatingPanelLayout } from '../hooks/useFloatingPanels'; import { computeEdgeHighlight, SnapPreview, WorkspaceSize } from '../hooks/usePanelDocking'; import { IconClose } from './Icons'; @@ -205,6 +206,39 @@ export default function FloatingPanel({ // Exclude panels with zero geometry (e.g. a closed-but-persisted leftover) && (frames[otherId]?.width || 0) > 0 && (frames[otherId]?.height || 0) > 0)); + // Snap geometry is relative to the workspace. Render guides at that same + // level rather than inside the moving panel, whose own position and overflow + // would otherwise offset and clip the preview. + const dockGuide = (highlight || (snapPreview && snapPreview.side)) && workspaceNodeRef?.current + ? createPortal( + <> + {highlight && ( +
+ )} + {snapPreview && snapPreview.side && ( +
+ )} + , + workspaceNodeRef.current, + ) + : null; + return (
); })} - {highlight && ( -
- )} - {snapPreview && snapPreview.side && ( -
- )} + {dockGuide}
); } diff --git a/frontend/src/lib/canvasDocument.ts b/frontend/src/lib/canvasDocument.ts index 7cf0321..f8d7692 100644 --- a/frontend/src/lib/canvasDocument.ts +++ b/frontend/src/lib/canvasDocument.ts @@ -115,6 +115,53 @@ export function normalizeDocument(input: CanvasDocument): CanvasDocument { }; } +/** + * Return elements in their final paint order: the document background is + * always underneath, followed by canvas layers from bottom to top. Elements + * inside one layer retain their own relative order. + */ +export function orderCanvasElementsByLayer(documentModel: CanvasDocument): CanvasElement[] { + const document = normalizeDocument(documentModel); + const layerOrder = new Map((document.layers || []).map((layer, index) => [layer.id, index])); + + return document.elements + .map((element, elementIndex) => ({ + element, + elementIndex, + layerIndex: layerOrder.get(element.layerId || '') ?? -1, + })) + .sort((a, b) => a.layerIndex - b.layerIndex || a.elementIndex - b.elementIndex) + .map(({ element }) => element); +} + +/** + * Changes the stacking order of an element without allowing it to cross a + * canvas-layer boundary. Canvas-layer ordering is managed separately. + */ +export function moveCanvasElementWithinLayer( + documentModel: CanvasDocument, + elementId: string, + direction: -1 | 1, +): CanvasDocument { + const document = normalizeDocument(documentModel); + const elementIndex = document.elements.findIndex(element => element.id === elementId); + if (elementIndex < 0) return document; + + const layerId = document.elements[elementIndex].layerId; + const siblingIndexes = document.elements + .map((element, index) => ({ element, index })) + .filter(({ element }) => element.layerId === layerId) + .map(({ index }) => index); + const siblingIndex = siblingIndexes.indexOf(elementIndex); + const nextSiblingIndex = siblingIndex + direction; + if (nextSiblingIndex < 0 || nextSiblingIndex >= siblingIndexes.length) return document; + + const nextElements = [...document.elements]; + const swapIndex = siblingIndexes[nextSiblingIndex]; + [nextElements[elementIndex], nextElements[swapIndex]] = [nextElements[swapIndex], nextElements[elementIndex]]; + return { ...document, elements: nextElements }; +} + function normalizeFolders(folders: CanvasLayerFolder[], layers: CanvasLayer[]) { const layerIds = new Set(layers.map(layer => layer.id)); return folders diff --git a/frontend/src/lib/svgExport.ts b/frontend/src/lib/svgExport.ts index 8893c2e..b6949c1 100644 --- a/frontend/src/lib/svgExport.ts +++ b/frontend/src/lib/svgExport.ts @@ -1,6 +1,6 @@ import { CanvasDocument, StickerAsset } from '../types'; import { apiUrl } from './api'; -import { hasCanvasBackground, normalizeDocument, pxToMm } from './canvasDocument'; +import { hasCanvasBackground, normalizeDocument, orderCanvasElementsByLayer, pxToMm } from './canvasDocument'; import { createZip } from './zip'; export interface SerializeOptions { @@ -66,7 +66,7 @@ export async function serializeDocument( parts.push(``); } - for (const element of doc.elements) { + for (const element of orderCanvasElementsByLayer(doc)) { const layerId = element.layerId || doc.layers?.[0]?.id; if (layerFilter && (!layerId || !layerFilter.has(layerId))) continue; if (!layerFilter && layerId && !visibleLayers.has(layerId)) continue; diff --git a/frontend/src/pages/CanvasStudio.tsx b/frontend/src/pages/CanvasStudio.tsx index 248df0a..b367de8 100644 --- a/frontend/src/pages/CanvasStudio.tsx +++ b/frontend/src/pages/CanvasStudio.tsx @@ -28,7 +28,9 @@ import { layerIsLocked, makeId, mmToPx, + moveCanvasElementWithinLayer, normalizeDocument, + orderCanvasElementsByLayer, pxToMm, TRANSPARENT_BACKGROUND, } from '../lib/canvasDocument'; @@ -206,6 +208,12 @@ export default function CanvasStudio({ return map; }, [stickers]); + const visibleCanvasElements = useMemo( + () => orderCanvasElementsByLayer(normalizedDocument) + .filter(element => layers.find(layer => layer.id === element.layerId)?.visible !== false), + [layers, normalizedDocument], + ); + useEffect(() => { const normalized = normalizeDocument(documentModel); if (JSON.stringify(normalized) !== JSON.stringify(documentModel)) { @@ -391,26 +399,12 @@ export default function CanvasStudio({ const bringForward = () => { if (!selectedId) return; - setDocumentModel(prev => { - const index = prev.elements.findIndex(item => item.id === selectedId); - if (index < 0 || index === prev.elements.length - 1) return prev; - const next = [...prev.elements]; - const [item] = next.splice(index, 1); - next.splice(index + 1, 0, item); - return { ...prev, elements: next }; - }); + setDocumentModel(prev => moveCanvasElementWithinLayer(prev, selectedId, 1)); }; const sendBackward = () => { if (!selectedId) return; - setDocumentModel(prev => { - const index = prev.elements.findIndex(item => item.id === selectedId); - if (index <= 0) return prev; - const next = [...prev.elements]; - const [item] = next.splice(index, 1); - next.splice(index - 1, 0, item); - return { ...prev, elements: next }; - }); + setDocumentModel(prev => moveCanvasElementWithinLayer(prev, selectedId, -1)); }; const handleSvgImport = async (file: File | null) => { @@ -1060,16 +1054,16 @@ export default function CanvasStudio({ style={{ width: normalizedDocument.width, height: normalizedDocument.height, - background: normalizedDocument.background, + '--canvas-background': normalizedDocument.background, transform: `scale(${zoom})`, - }} + } as CSSProperties} > - {normalizedDocument.elements - .filter(element => layers.find(layer => layer.id === element.layerId)?.visible !== false) - .map(element => ( + {visibleCanvasElements + .map((element, stackOrder) => ( setIdx(i => Math.min(slides.length - 1, i + 1)); const [largeUrl, setLargeUrl] = useState(slides.length > 0 ? assetUrl(slides[safeIdx]) : ''); + const [closing, setClosing] = useState(false); + const closeTimerRef = useRef(0); + const pendingActionRef = useRef(() => onClose()); useEffect(() => { if (slides.length > 0) { setLargeUrl(assetUrl(slides[safeIdx])); @@ -248,11 +251,23 @@ function TemplateModal({ return () => { cancelled = true; }; }, [slides, safeIdx, template, stickerById]); + useEffect(() => () => window.clearTimeout(closeTimerRef.current), []); + + const requestClose = (action: () => void = onClose) => { + if (closing) return; + pendingActionRef.current = action; + setClosing(true); + closeTimerRef.current = window.setTimeout(() => { + setClosing(false); + pendingActionRef.current(); + }, 160); + }; + return ( -
-
e.stopPropagation()}> +
requestClose()}> +
e.stopPropagation()}> {/* Large preview with prev/next arrows */} -
+
{template.name} {slides.length > 1 && ( <> @@ -270,42 +285,82 @@ function TemplateModal({ )}
-
-
{template.name}
- {template.description &&

{template.description}

} -
- {formatMm(template.document.width)} x {formatMm(template.document.height)} mm - {template.document.elements.length} 个元素 - {formatDate(templateUpdatedAt(template))} -
- - {/* Thumbnail strip */} - {slides.length > 1 && ( -
- {slides.map((asset, i) => ( - {asset.name} setIdx(i)} - style={{ cursor: 'pointer', outline: i === safeIdx ? '2px solid var(--color-primary, #6c63ff)' : 'none', borderRadius: 4 }} - /> - ))} -
- )} - -
- - - -
-
+ requestClose(onUse)} + onClose={() => requestClose()} + onDelete={onDelete} + referenceImages={slides.length > 1 ? slides.map((asset, i) => ( + {asset.name} setIdx(i)} + /> + )) : undefined} + />
); } +function TemplateDetailPanel({ + template, + referenceImages, + onUse, + onClose, + onDelete, +}: { + template: CanvasTemplate; + referenceImages?: ReactNode; + onUse: () => void; + onClose: () => void; + onDelete: () => void; +}) { + return ( + + ); +} + function TemplateMasonryCard({ template, cover, diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 83f5e93..c11e74d 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -36,6 +36,17 @@ --nav-height: 56px; --panel-width: 240px; --transition: 200ms cubic-bezier(0.4, 0, 0.2, 1); + --modal-backdrop-duration: 260ms; + --modal-backdrop-opacity: 0.05; + --modal-backdrop-blur: 8px; + --modal-open-duration: 320ms; + --modal-open-delay: 60ms; + --modal-open-easing: cubic-bezier(0.22, 1, 0.36, 1); + --modal-content-duration: 200ms; + --modal-content-delay: 120ms; + --modal-content-stagger: 55ms; + --modal-exit-duration: 160ms; + --modal-exit-easing: cubic-bezier(0.4, 0, 0.2, 1); --font-main: 'DM Sans', sans-serif; --font-mono: 'DM Mono', monospace; /* 液态玻璃 */ @@ -1577,7 +1588,19 @@ body.resizing { flex-shrink: 0; box-shadow: 0 16px 50px rgba(0,0,0,0.18); transform-origin: center center; - overflow: hidden; + /* 画布背景仅限于本身,设计元素允许越出画布继续编辑。 */ + overflow: visible; + isolation: isolate; + background: transparent; +} + +.studio-stage::before { + content: ''; + position: absolute; + inset: 0; + z-index: 0; + background: var(--canvas-background, #ffffff); + pointer-events: none; } .studio-bottombar { @@ -2285,12 +2308,12 @@ body.resizing { padding: 28px; /* 只模糊、不压暗:去掉黑遮罩,保留毛玻璃磨砂。 模糊从 0 缓慢增强到目标值(700ms ease-out),进展不突兀 */ - background: rgba(255, 255, 255, 0.04); - backdrop-filter: blur(10px); - animation: modal-backdrop-in 700ms ease-out; + background: rgba(255, 255, 255, var(--modal-backdrop-opacity)); + backdrop-filter: blur(var(--modal-backdrop-blur)); + animation: modal-backdrop-in var(--modal-backdrop-duration) cubic-bezier(0.22, 1, 0.36, 1); } :root[data-theme="dark"] .template-modal-backdrop { - background: rgba(10, 12, 18, 0.14); + background: rgba(10, 12, 18, calc(var(--modal-backdrop-opacity) * 2.4)); } .template-modal { width: min(960px, 100%); @@ -2302,11 +2325,139 @@ body.resizing { border-radius: var(--radius-lg); background: var(--bg-panel); box-shadow: var(--shadow-lg); - animation: template-pop 160ms ease-out; + animation: template-pop var(--modal-open-duration) var(--modal-open-easing) var(--modal-open-delay) backwards; } -.modal-meta { - margin-top: 10px; +.template-modal-backdrop.template-modal-exiting { + animation: modal-backdrop-out var(--modal-exit-duration) var(--modal-exit-easing) forwards; +} + +.template-modal.template-modal-exiting { + animation: template-exit var(--modal-exit-duration) var(--modal-exit-easing); +} + +.modal-layer { + animation: modal-layer-in var(--modal-content-duration) var(--modal-open-easing) + calc(var(--modal-content-delay) + var(--modal-content-stagger) * var(--modal-layer, 0)) backwards; +} + +.template-modal-exiting .modal-layer { + animation: modal-layer-out var(--modal-exit-duration) var(--modal-exit-easing); +} + +.template-detail-panel { + min-width: 0; + min-height: 0; + display: flex; + flex-direction: column; + gap: 16px; + padding: 22px; + overflow-y: auto; +} + +.template-detail-summary { + display: grid; + gap: 5px; +} + +.template-detail-eyebrow, +.template-detail-section-label { + color: var(--lf-text-faint, var(--text-muted)); + font-size: 11px; + font-weight: 700; + letter-spacing: 0.08em; + line-height: 1.2; + text-transform: uppercase; +} + +.template-detail-title { + margin: 0; + color: var(--lf-text, var(--text-primary)); + font-size: 22px; + line-height: 1.25; + overflow-wrap: anywhere; +} + +.template-detail-description { + display: grid; + gap: 8px; + padding: 13px 14px; + border: 1px solid var(--lf-glass-soft, var(--border)); + border-radius: var(--radius-md); + background: var(--lf-input-bg, var(--bg-panel-alt)); +} + +.template-detail-description p { + display: -webkit-box; + margin: 0; + overflow: hidden; + color: var(--lf-text-dim, var(--text-secondary)); + font-size: 13px; + line-height: 1.55; + -webkit-box-orient: vertical; + -webkit-line-clamp: 4; +} + +.template-detail-specs { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; + margin: 0; +} + +.template-detail-spec { + min-width: 0; + padding: 11px 10px; + border: 1px solid var(--lf-glass-soft, var(--border)); + border-radius: var(--radius-sm); + background: var(--lf-glass-bg, var(--bg-panel-alt)); +} + +.template-detail-spec dt { + margin-bottom: 6px; + color: var(--lf-text-faint, var(--text-muted)); + font-size: 11px; +} + +.template-detail-spec dd { + margin: 0; + overflow: hidden; + color: var(--lf-text, var(--text-primary)); + font-family: var(--font-mono); + font-size: 12px; + line-height: 1.3; + text-overflow: ellipsis; + white-space: nowrap; +} + +.template-detail-spec:first-child { + grid-column: 1 / -1; +} + +.template-detail-references { + display: grid; + gap: 8px; +} + +.template-detail-references .reference-strip { + margin: 0; +} + +.template-detail-actions { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; + margin-top: auto; + padding-top: 16px; + border-top: 1px solid var(--lf-glass-soft, var(--border)); +} + +.template-detail-actions .btn { + min-width: 0; +} + +.template-detail-delete { + grid-column: 1 / -1; } .reference-strip { @@ -2324,11 +2475,12 @@ body.resizing { border-radius: var(--radius-md); background: var(--bg-panel-alt); flex-shrink: 0; - transition: outline 0.1s; + cursor: pointer; + transition: border-color 0.1s, outline 0.1s; } .reference-strip img.active { - outline: 2px solid var(--color-primary, #6c63ff); + outline: 2px solid var(--accent); outline-offset: 1px; } @@ -2367,11 +2519,57 @@ body.resizing { @keyframes template-pop { from { opacity: 0; - transform: scale(0.96); + transform: translateY(0) scale(0.98); } to { opacity: 1; - transform: scale(1); + transform: translateY(0) scale(1); + } +} + +@keyframes modal-layer-in { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes template-exit { + from { + opacity: 1; + transform: translateY(0) scale(1); + } + to { + opacity: 0; + transform: translateY(0) scale(0.98); + } +} + +@keyframes modal-layer-out { + from { + opacity: 1; + transform: translateY(0); + } + to { + opacity: 0; + transform: translateY(5px); + } +} + +@keyframes modal-backdrop-out { + from { + opacity: 1; + -webkit-backdrop-filter: blur(var(--modal-backdrop-blur)); + backdrop-filter: blur(var(--modal-backdrop-blur)); + } + to { + opacity: 0; + -webkit-backdrop-filter: blur(0px); + backdrop-filter: blur(0px); } } /* 遮罩只模糊不压暗 → 让模糊从 0 慢慢增强到目标值,而非瞬间全糊(进展缓一点) */ @@ -2383,8 +2581,8 @@ body.resizing { } to { opacity: 1; - -webkit-backdrop-filter: blur(10px); - backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(var(--modal-backdrop-blur)); + backdrop-filter: blur(var(--modal-backdrop-blur)); } } @@ -2471,6 +2669,27 @@ body.resizing { .template-masonry { column-count: 1; } + + .template-modal-backdrop { + padding: 14px; + } + + .template-detail-panel { + gap: 14px; + padding: 18px; + } + + .template-detail-specs { + grid-template-columns: 1fr; + } + + .template-detail-actions { + grid-template-columns: 1fr; + } + + .template-detail-delete { + grid-column: auto; + } } /* ===== RESIZABLE PANEL HANDLE ===== */ @@ -2574,6 +2793,24 @@ body.resizing .studio-panel { box-shadow: inset 0 0 0 1px var(--lf-glass-soft), 0 12px 34px var(--lf-shadow-strong); } +/* 横向 SVG 使用满高白色画布承托,完整图形居中显示且不裁切。 */ +.template-home .template-modal .template-preview.large { + align-self: stretch; + aspect-ratio: 4 / 3; + min-height: 300px; + background: #fff; +} + +.template-home .template-modal .template-preview.large img { + height: 100%; + max-height: none; +} + +/* SVG 常带透明背景,缩略图统一以白底承托,保证预览可见。 */ +.template-home .template-detail-references .reference-strip img { + background: #fff; +} + /* 空态与模态玻璃化 */ .template-home .template-empty { background: var(--lf-glass-bg); diff --git a/frontend/tests/canvas-layer-order.test.mjs b/frontend/tests/canvas-layer-order.test.mjs new file mode 100644 index 0000000..e37ceef --- /dev/null +++ b/frontend/tests/canvas-layer-order.test.mjs @@ -0,0 +1,60 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import test from 'node:test'; +import ts from 'typescript'; + +async function loadCanvasDocumentModule() { + const source = await readFile(new URL('../src/lib/canvasDocument.ts', import.meta.url), 'utf8'); + const compiled = ts.transpileModule(source, { + compilerOptions: { + module: ts.ModuleKind.CommonJS, + target: ts.ScriptTarget.ES2020, + }, + }).outputText; + const module = { exports: {} }; + new Function('exports', 'module', compiled)(module.exports, module); + return module.exports; +} + +const documentWithTwoLayers = { + width: 400, + height: 240, + background: '#ffffff', + layers: [ + { id: 'layer-bottom', name: '底层', visible: true, locked: false }, + { id: 'layer-top', name: '顶层', visible: true, locked: false }, + ], + layerFolders: [], + elements: [ + { id: 'top-first', type: 'rect', layerId: 'layer-top', x: 0, y: 0, width: 10, height: 10, rotation: 0, opacity: 1, fill: '#f00', stroke: '#f00', strokeWidth: 0 }, + { id: 'bottom-first', type: 'rect', layerId: 'layer-bottom', x: 0, y: 0, width: 10, height: 10, rotation: 0, opacity: 1, fill: '#0f0', stroke: '#0f0', strokeWidth: 0 }, + { id: 'top-second', type: 'rect', layerId: 'layer-top', x: 0, y: 0, width: 10, height: 10, rotation: 0, opacity: 1, fill: '#00f', stroke: '#00f', strokeWidth: 0 }, + ], +}; + +test('canvas paint order keeps every element above the background and honors layer order', async () => { + const { orderCanvasElementsByLayer } = await loadCanvasDocumentModule(); + + assert.deepEqual( + orderCanvasElementsByLayer(documentWithTwoLayers).map(element => element.id), + ['bottom-first', 'top-first', 'top-second'], + ); +}); + +test('moving an element only changes its order inside its own canvas layer', async () => { + const { moveCanvasElementWithinLayer } = await loadCanvasDocumentModule(); + + const moved = moveCanvasElementWithinLayer(documentWithTwoLayers, 'top-first', 1); + + assert.deepEqual( + moved.elements.map(element => element.id), + ['top-second', 'bottom-first', 'top-first'], + ); +}); + +test('canvas elements remain visible when they extend beyond the document surface', async () => { + const styles = await readFile(new URL('../src/styles.css', import.meta.url), 'utf8'); + const stageRule = /\.studio-stage\s*\{([\s\S]*?)\n\}/.exec(styles)?.[1] ?? ''; + + assert.match(stageRule, /overflow:\s*visible/); +}); diff --git a/frontend/tests/docking-preview.test.mjs b/frontend/tests/docking-preview.test.mjs new file mode 100644 index 0000000..c5ab272 --- /dev/null +++ b/frontend/tests/docking-preview.test.mjs @@ -0,0 +1,10 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import test from 'node:test'; + +test('dock preview is mounted in the workspace rather than inside the moving panel', async () => { + const source = await readFile(new URL('../src/components/FloatingPanel.tsx', import.meta.url), 'utf8'); + + assert.match(source, /createPortal\(/); + assert.match(source, /workspaceNodeRef\.current,\s*\n\s*\)/); +}); diff --git a/frontend/tests/template-details-layout.test.mjs b/frontend/tests/template-details-layout.test.mjs new file mode 100644 index 0000000..1ef0842 --- /dev/null +++ b/frontend/tests/template-details-layout.test.mjs @@ -0,0 +1,47 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import test from 'node:test'; + +const sourcePath = new URL('../src/pages/TemplateHome.tsx', import.meta.url); +const stylesPath = new URL('../src/styles.css', import.meta.url); + +test('template details are rendered through one reusable right-panel component', async () => { + const source = await readFile(sourcePath, 'utf8'); + + assert.match(source, /function TemplateDetailPanel\(/); + assert.match(source, / { + const styles = await readFile(stylesPath, 'utf8'); + + assert.match(styles, /\.template-detail-summary/); + assert.match(styles, /\.template-detail-specs/); + assert.match(styles, /\.template-detail-actions/); +}); + +test('template detail specifications and actions stack on narrow screens', async () => { + const styles = await readFile(stylesPath, 'utf8'); + + assert.match(styles, /@media \(max-width: 560px\) \{[\s\S]*?\.template-detail-specs\s*\{\s*grid-template-columns: 1fr;/); + assert.match(styles, /@media \(max-width: 560px\) \{[\s\S]*?\.template-detail-actions\s*\{\s*grid-template-columns: 1fr;/); +}); + +test('narrow-screen modal keeps the action row reachable under the viewport cap', async () => { + const styles = await readFile(stylesPath, 'utf8'); + + // The modal caps height with overflow:hidden. On single-column mobile the + // detail panel must keep its own scroll so actions cannot be clipped away. + assert.match(styles, /\.template-modal\s*\{[^}]*max-height:\s*calc\(100vh\s*-\s*56px\);/); + assert.match(styles, /\.template-modal\s*\{[^}]*overflow:\s*hidden;/); + assert.match(styles, /\.template-detail-panel\s*\{[^}]*overflow-y:\s*auto;/); +}); + +test('modal previews fill their column with a white SVG backdrop and thumbnails stay opaque', async () => { + const styles = await readFile(stylesPath, 'utf8'); + + assert.match(styles, /\.template-home \.template-modal \.template-preview\.large\s*\{[^}]*align-self: stretch;[^}]*aspect-ratio: 4 \/ 3;[^}]*background: #fff;/); + assert.match(styles, /\.template-home \.template-modal \.template-preview\.large img\s*\{[^}]*height: 100%;[^}]*max-height: none;/); + assert.match(styles, /\.template-home \.template-detail-references \.reference-strip img\s*\{[^}]*background: #fff;/); +});