mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
fix(web): preserve share export layout fidelity (#1277)
* fix(web): preserve share export layout fidelity * fix(web): keep mobile and multi-image exports responsive * fix(web): distinguish coarse pointers from touch desktops
This commit is contained in:
+81
-1
@@ -60,7 +60,7 @@ for (const viewport of [
|
||||
}
|
||||
}
|
||||
const mediaGrid = page.getByRole('dialog').locator('.hapi-share-media-grid')
|
||||
await expect(mediaGrid).toHaveCSS('display', 'grid')
|
||||
await expect(mediaGrid).toHaveCSS('display', 'flex')
|
||||
const imageTops = await mediaGrid.locator('img').evaluateAll((images) => images.map((image) => image.getBoundingClientRect().top))
|
||||
expect(imageTops).toHaveLength(2)
|
||||
expect(Math.abs(imageTops[0] - imageTops[1])).toBeLessThan(1)
|
||||
@@ -192,6 +192,86 @@ test('keeps code and image controls interactive in preview', async ({ page }, te
|
||||
await expect(dialog).toBeVisible()
|
||||
})
|
||||
|
||||
test('preserves the desktop source width but keeps mobile export width stable', async ({ page }, testInfo) => {
|
||||
await page.addInitScript(() => {
|
||||
Object.defineProperty(navigator, 'maxTouchPoints', { configurable: true, value: 5 })
|
||||
})
|
||||
await page.setViewportSize({ width: 1440, height: 1600 })
|
||||
await page.goto('/e2e-fixtures/share-turn-fixture.html?wide=1')
|
||||
const sourceWidth = await page.getByTestId('source-turn').evaluate((element) => element.getBoundingClientRect().width)
|
||||
expect(sourceWidth).toBe(1080)
|
||||
await page.getByRole('button', { name: 'Open share preview' }).click()
|
||||
|
||||
const preview = page.getByRole('dialog').locator('.hapi-share-preview-root')
|
||||
const previewWidth = await preview.evaluate((element) => element.getBoundingClientRect().width)
|
||||
expect(previewWidth).toBeGreaterThan(650)
|
||||
expect(previewWidth).toBeLessThanOrEqual(720)
|
||||
const inlineCode = preview.locator('.aui-md-code').last()
|
||||
await expect(inlineCode).toHaveCSS('display', 'inline')
|
||||
|
||||
const desktopDownloadPromise = page.waitForEvent('download')
|
||||
await page.getByRole('button', { name: 'Download' }).click()
|
||||
const desktopDownload = await desktopDownloadPromise
|
||||
const desktopPath = testInfo.outputPath('source-width-desktop.png')
|
||||
await desktopDownload.saveAs(desktopPath)
|
||||
expect(pngSize(await readFile(desktopPath)).width).toBe(2240)
|
||||
|
||||
await page.setViewportSize({ width: 390, height: 844 })
|
||||
await page.reload()
|
||||
await page.getByRole('button', { name: 'Open share preview' }).click()
|
||||
const mobileDownloadPromise = page.waitForEvent('download')
|
||||
await page.getByRole('button', { name: 'Download' }).click()
|
||||
const mobileDownload = await mobileDownloadPromise
|
||||
const mobilePath = testInfo.outputPath('source-width-mobile.png')
|
||||
await mobileDownload.saveAs(mobilePath)
|
||||
expect(pngSize(await readFile(mobilePath)).width).toBe(1920)
|
||||
})
|
||||
|
||||
test('keeps a landscape touch device on the fixed mobile export path', async ({ page }, testInfo) => {
|
||||
await page.addInitScript(() => {
|
||||
const nativeMatchMedia = window.matchMedia.bind(window)
|
||||
window.matchMedia = (query: string) => {
|
||||
if (query !== '(pointer: coarse)') return nativeMatchMedia(query)
|
||||
return {
|
||||
matches: true,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: () => undefined,
|
||||
removeListener: () => undefined,
|
||||
addEventListener: () => undefined,
|
||||
removeEventListener: () => undefined,
|
||||
dispatchEvent: () => false,
|
||||
}
|
||||
}
|
||||
})
|
||||
await page.setViewportSize({ width: 844, height: 390 })
|
||||
await page.goto('/e2e-fixtures/share-turn-fixture.html?wide=1')
|
||||
await page.getByRole('button', { name: 'Open share preview' }).click()
|
||||
|
||||
const downloadPromise = page.waitForEvent('download')
|
||||
await page.getByRole('button', { name: 'Download' }).click()
|
||||
const download = await downloadPromise
|
||||
const path = testInfo.outputPath('landscape-touch-mobile.png')
|
||||
await download.saveAs(path)
|
||||
expect(pngSize(await readFile(path)).width).toBe(1920)
|
||||
})
|
||||
|
||||
test('allows three or more attachments to wrap instead of shrinking into one row', async ({ page }) => {
|
||||
await page.goto('/e2e-fixtures/share-turn-fixture.html')
|
||||
await page.getByTestId('source-turn').evaluate((source) => {
|
||||
const grid = source.querySelector<HTMLElement>('.hapi-share-media-grid')
|
||||
const firstAttachment = grid?.querySelector<HTMLButtonElement>('button')
|
||||
if (!grid || !firstAttachment) throw new Error('Missing attachment fixture')
|
||||
grid.appendChild(firstAttachment.cloneNode(true))
|
||||
grid.dataset.hapiImageCount = '3'
|
||||
})
|
||||
await page.getByRole('button', { name: 'Open share preview' }).click()
|
||||
|
||||
const mediaGrid = page.getByRole('dialog').locator('.hapi-share-media-grid')
|
||||
await expect(mediaGrid).toHaveCSS('flex-wrap', 'wrap')
|
||||
await expect(mediaGrid.locator(':scope > button')).toHaveCount(3)
|
||||
})
|
||||
|
||||
test('uses a prepared PNG while native share still has click activation', async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
const state = { calls: 0, active: false, fileType: '', fileName: '' }
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useRef, useState } from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import '../src/index.css'
|
||||
import { ShareTurnDialog } from '../src/components/AssistantChat/ShareTurnDialog'
|
||||
import { getUserBubbleClassName } from '../src/components/AssistantChat/messages/user-bubble'
|
||||
import { getUserBubbleClassName, UserBubbleContent } from '../src/components/AssistantChat/messages/user-bubble'
|
||||
import { MarkdownRenderer } from '../src/components/MarkdownRenderer'
|
||||
import { I18nProvider } from '../src/lib/i18n-context'
|
||||
|
||||
@@ -26,6 +26,8 @@ const portraitFixtureImage = 'data:image/svg+xml;charset=utf-8,' + encodeURIComp
|
||||
|
||||
const markdown = `## Complex response fixture
|
||||
|
||||
## ✅ AList 的 frp 和 Caddy 配置已彻底移除
|
||||
|
||||
This paragraph contains **bold text**, *emphasis*, ~~strikethrough~~, inline \`code\`, a [safe link](https://example.com), 中文内容,以及一段足够长的文字,用于验证换行、行高和宽屏导出效果是否与原始 HAPI 页面保持一致。
|
||||
|
||||
> A multi-line blockquote used to verify borders, indentation, colors, and wrapping. \n> 第二行引用包含中文。
|
||||
@@ -54,6 +56,10 @@ console.log(result)
|
||||
---
|
||||
|
||||
Final paragraph after the divider.
|
||||
|
||||
\`alist.techotaku39.top\` 已失效,5244 端口也不再监听。
|
||||
|
||||
请将 \`machine-status-widget/hapi-machine-status.user.js\` 全文覆盖到油猴脚本中,然后强制刷新页面。
|
||||
`
|
||||
|
||||
if (new URLSearchParams(window.location.search).get('theme') === 'dark') {
|
||||
@@ -66,6 +72,7 @@ function App() {
|
||||
const sourceRef = useRef<HTMLDivElement>(null)
|
||||
const [snapshots, setSnapshots] = useState<Snapshot[]>([])
|
||||
const [open, setOpen] = useState(false)
|
||||
const wideSource = new URLSearchParams(window.location.search).get('wide') === '1'
|
||||
|
||||
const openShare = () => {
|
||||
const searchParams = new URLSearchParams(window.location.search)
|
||||
@@ -91,14 +98,11 @@ function App() {
|
||||
|
||||
return (
|
||||
<I18nProvider>
|
||||
<main className="mx-auto w-[960px] max-w-full bg-[var(--app-bg)] p-5 text-[var(--app-fg)]">
|
||||
<main className={`mx-auto max-w-full bg-[var(--app-bg)] p-5 text-[var(--app-fg)] ${wideSource ? 'w-[1120px]' : 'w-[960px]'}`}>
|
||||
<div ref={sourceRef} data-testid="source-turn" className="flex flex-col gap-3">
|
||||
<div data-hapi-message-role="user" className="happy-message flex flex-col items-end">
|
||||
<div className={getUserBubbleClassName()}>
|
||||
<div className="happy-chat-text whitespace-pre-wrap">
|
||||
<span className="mr-1 inline-flex rounded-full bg-[var(--app-chat-user-chip-bg)] px-2 py-px text-[var(--app-chat-user-chip-fg)]">plan</span>
|
||||
{'请导出这一轮复杂对话,并确保代码、表格、图片附件和长文本的样式全部保留。\n第二行用于验证换行。'}
|
||||
</div>
|
||||
<UserBubbleContent text={'这个失败不用说吧:`bun run test` was also attempted. The Web suite passes, but the complete repository run is currently blocked on Windows by unrelated tests.\n第二行用于验证换行。'} />
|
||||
<div className="hapi-share-media-grid mt-3 flex flex-wrap gap-2" data-hapi-image-count="2">
|
||||
<button type="button" title="Click to zoom" data-image-preview-trigger="" data-image-preview-label="HAPI landscape export fixture" className="overflow-hidden rounded-xl">
|
||||
<img className="max-h-60" src={fixtureImage} alt="HAPI landscape export fixture" />
|
||||
@@ -132,6 +136,7 @@ function App() {
|
||||
showFastBadge={false}
|
||||
worktreeBranch="feat/share-turn-polish"
|
||||
sourceSnapshots={snapshots}
|
||||
sourceContentWidth={sourceRef.current?.getBoundingClientRect().width ?? null}
|
||||
onClose={() => setOpen(false)}
|
||||
/>
|
||||
</I18nProvider>
|
||||
|
||||
@@ -52,6 +52,7 @@ type ShareTurnState = {
|
||||
id: number
|
||||
snapshots: ShareTurnSnapshot[]
|
||||
title: string
|
||||
sourceContentWidth: number | null
|
||||
} | null
|
||||
|
||||
type ShareTurnSnapshot = {
|
||||
@@ -1333,6 +1334,9 @@ export function HappyThread(props: {
|
||||
) => {
|
||||
const content = contentRef.current
|
||||
if (!content) return
|
||||
const messageContainer = content.querySelector<HTMLElement>('.happy-thread-messages')
|
||||
const sourceContentWidth = messageContainer?.getBoundingClientRect().width
|
||||
?? content.getBoundingClientRect().width
|
||||
|
||||
let target: HTMLElement | null = typeof messageTarget === 'string'
|
||||
? document.getElementById(messageTarget)
|
||||
@@ -1345,6 +1349,7 @@ export function HappyThread(props: {
|
||||
id: ++shareTurnIdRef.current,
|
||||
snapshots: fallbackSnapshot ? [fallbackSnapshot] : [],
|
||||
title: props.metadata?.summary?.text ?? props.metadata?.name ?? props.metadata?.path ?? props.sessionId.slice(0, 8),
|
||||
sourceContentWidth: sourceContentWidth > 0 ? sourceContentWidth : null,
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -1388,6 +1393,7 @@ export function HappyThread(props: {
|
||||
id: ++shareTurnIdRef.current,
|
||||
snapshots: completeSnapshots,
|
||||
title: props.metadata?.summary?.text ?? props.metadata?.name ?? props.metadata?.path ?? props.sessionId.slice(0, 8),
|
||||
sourceContentWidth: sourceContentWidth > 0 ? sourceContentWidth : null,
|
||||
})
|
||||
}, [props.metadata, props.sessionId])
|
||||
|
||||
@@ -1500,6 +1506,7 @@ export function HappyThread(props: {
|
||||
showFastBadge={props.session.metadata?.flavor === 'codex' && isFastServiceTier(props.session.serviceTier)}
|
||||
worktreeBranch={props.session.metadata?.worktree?.branch ?? null}
|
||||
sourceSnapshots={shareTurn?.snapshots ?? []}
|
||||
sourceContentWidth={shareTurn?.sourceContentWidth ?? null}
|
||||
onClose={() => setShareTurn(null)}
|
||||
/>
|
||||
</ThreadPrimitive.Root>
|
||||
|
||||
@@ -18,12 +18,14 @@ type ShareTurnDialogProps = {
|
||||
text: string
|
||||
role?: 'user' | 'assistant'
|
||||
}>
|
||||
sourceContentWidth?: number | null
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
type ShareTurnSnapshot = ShareTurnDialogProps['sourceSnapshots'][number]
|
||||
|
||||
const SHARE_EXPORT_WIDTH = 960
|
||||
const SHARE_EXPORT_HORIZONTAL_PADDING = 40
|
||||
const SHARE_EXPORT_SCALE = 2
|
||||
const MAX_EXPORT_PIXELS = 24_000_000
|
||||
const SHARE_HIDDEN_CONTENT_SELECTOR = '[data-hapi-share-exclude="true"], .aui-reasoning-group'
|
||||
@@ -143,12 +145,20 @@ function getShareFileName(title: string): string {
|
||||
return `HAPI-${sanitizeShareFileNamePart(title)}-${formatShareTimestamp()}.png`
|
||||
}
|
||||
|
||||
function prepareExportElement(element: HTMLElement): HTMLElement {
|
||||
function prepareExportElement(element: HTMLElement, exportWidth: number, preserveSourceLayout: boolean): HTMLElement {
|
||||
const captureElement = element.cloneNode(true)
|
||||
if (!(captureElement instanceof HTMLElement)) {
|
||||
throw new Error('Failed to prepare shared image')
|
||||
}
|
||||
|
||||
for (const code of Array.from(captureElement.querySelectorAll<HTMLElement>('code'))) {
|
||||
if (code.closest('pre')) continue
|
||||
const textWrapper = document.createElement('span')
|
||||
textWrapper.dataset.hapiInlineCodeText = 'true'
|
||||
while (code.firstChild) textWrapper.appendChild(code.firstChild)
|
||||
code.appendChild(textWrapper)
|
||||
}
|
||||
|
||||
const elementStyle = getComputedStyle(element)
|
||||
const backgroundColor = elementStyle.backgroundColor && elementStyle.backgroundColor !== 'rgba(0, 0, 0, 0)'
|
||||
? elementStyle.backgroundColor
|
||||
@@ -162,16 +172,16 @@ function prepareExportElement(element: HTMLElement): HTMLElement {
|
||||
'left:0',
|
||||
'top:0',
|
||||
'z-index:-1',
|
||||
`width:${SHARE_EXPORT_WIDTH}px`,
|
||||
`max-width:${SHARE_EXPORT_WIDTH}px`,
|
||||
`width:${exportWidth}px`,
|
||||
`max-width:${exportWidth}px`,
|
||||
'box-sizing:border-box',
|
||||
'transform:none',
|
||||
'zoom:1',
|
||||
'pointer-events:none',
|
||||
'overflow:visible',
|
||||
'-webkit-text-size-adjust:100%',
|
||||
'text-size-adjust:100%',
|
||||
'font-size:14px',
|
||||
'line-height:1.6',
|
||||
...(preserveSourceLayout ? [] : ['font-size:14px', 'line-height:1.6']),
|
||||
`background:${backgroundColor}`,
|
||||
`color:${color}`
|
||||
].join(';')
|
||||
@@ -184,8 +194,10 @@ function prepareExportElement(element: HTMLElement): HTMLElement {
|
||||
box-sizing: border-box !important;
|
||||
-webkit-text-size-adjust: 100% !important;
|
||||
text-size-adjust: 100% !important;
|
||||
font-size: 14px !important;
|
||||
line-height: 1.6 !important;
|
||||
${preserveSourceLayout ? '' : `
|
||||
font-size: 14px !important;
|
||||
line-height: 1.6 !important;
|
||||
`}
|
||||
}
|
||||
.hapi-share-export-root [data-hapi-share-exclude="true"],
|
||||
.hapi-share-export-root .aui-reasoning-group,
|
||||
@@ -193,6 +205,29 @@ function prepareExportElement(element: HTMLElement): HTMLElement {
|
||||
.hapi-share-export-root [data-hapi-share-export-exclude="true"] {
|
||||
display: none !important;
|
||||
}
|
||||
.hapi-share-export-root,
|
||||
.hapi-share-export-root * {
|
||||
/* html2canvas-pro renders non-zero letter spacing one grapheme at
|
||||
a time and switches CJK glyphs to an ideographic baseline.
|
||||
Mixed CJK/Latin text then no longer shares the browser's single
|
||||
shaped baseline. Zero spacing keeps each text run intact. */
|
||||
letter-spacing: 0 !important;
|
||||
}
|
||||
.hapi-share-export-root :not(pre) > code {
|
||||
position: relative !important;
|
||||
/* html2canvas-pro paints an inline element's background using its
|
||||
full inherited line box. Collapse the code element's own line
|
||||
box to its em square so the existing block padding produces the
|
||||
same pill height as the browser for both message roles. */
|
||||
line-height: 1em !important;
|
||||
}
|
||||
.hapi-share-export-root [data-hapi-inline-code-text="true"] {
|
||||
/* Move only the monospace glyphs. Keep the inline-code border and
|
||||
background in the same position as the browser layout. */
|
||||
position: relative !important;
|
||||
top: -1px !important;
|
||||
}
|
||||
${preserveSourceLayout ? '' : `
|
||||
.hapi-share-export-root img,
|
||||
.hapi-share-export-root video,
|
||||
.hapi-share-export-root canvas,
|
||||
@@ -229,6 +264,7 @@ function prepareExportElement(element: HTMLElement): HTMLElement {
|
||||
.hapi-share-export-root button:has(img) > :not(img) {
|
||||
display: none !important;
|
||||
}
|
||||
`}
|
||||
.hapi-share-export-root .sr-only {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -236,18 +272,11 @@ function prepareExportElement(element: HTMLElement): HTMLElement {
|
||||
display: block !important;
|
||||
height: 0.75rem !important;
|
||||
}
|
||||
.hapi-share-export-root .aui-md-code:not(.aui-md-codeblockcode) {
|
||||
display: inline-block !important;
|
||||
width: auto !important;
|
||||
max-width: 100% !important;
|
||||
white-space: normal !important;
|
||||
overflow-wrap: anywhere !important;
|
||||
direction: ltr !important;
|
||||
unicode-bidi: isolate !important;
|
||||
text-align: left !important;
|
||||
vertical-align: baseline !important;
|
||||
padding-left: 0.2em !important;
|
||||
padding-right: 0.2em !important;
|
||||
.hapi-share-export-root [data-hapi-code-body="true"] {
|
||||
scrollbar-width: none !important;
|
||||
}
|
||||
.hapi-share-export-root [data-hapi-code-body="true"]::-webkit-scrollbar {
|
||||
display: none !important;
|
||||
}
|
||||
`
|
||||
captureElement.prepend(style)
|
||||
@@ -368,15 +397,18 @@ async function waitForExportReady(root: HTMLElement): Promise<void> {
|
||||
await nextFrame()
|
||||
}
|
||||
|
||||
async function elementToPngBlob(element: HTMLElement): Promise<Blob> {
|
||||
const { default: html2canvas } = await import('html2canvas-pro')
|
||||
async function elementToPngBlob(
|
||||
element: HTMLElement,
|
||||
exportWidth: number,
|
||||
preserveSourceLayout: boolean
|
||||
): Promise<Blob> {
|
||||
const frame = document.createElement('iframe')
|
||||
frame.setAttribute('aria-hidden', 'true')
|
||||
frame.style.cssText = [
|
||||
'position:fixed',
|
||||
'left:-10000px',
|
||||
'top:0',
|
||||
`width:${SHARE_EXPORT_WIDTH}px`,
|
||||
`width:${exportWidth}px`,
|
||||
'height:1000px',
|
||||
'border:0',
|
||||
'opacity:0',
|
||||
@@ -403,7 +435,7 @@ async function elementToPngBlob(element: HTMLElement): Promise<Blob> {
|
||||
frameDocument.body.setAttribute('style', [
|
||||
document.body.getAttribute('style') ?? '',
|
||||
'margin:0',
|
||||
`width:${SHARE_EXPORT_WIDTH}px`,
|
||||
`width:${exportWidth}px`,
|
||||
'min-height:1000px',
|
||||
'overflow:visible',
|
||||
'background:transparent'
|
||||
@@ -415,7 +447,7 @@ async function elementToPngBlob(element: HTMLElement): Promise<Blob> {
|
||||
|
||||
copyLoadedStyleSheets(document, frameDocument)
|
||||
|
||||
const captureElement = prepareExportElement(element)
|
||||
const captureElement = prepareExportElement(element, exportWidth, preserveSourceLayout)
|
||||
captureElement.style.position = 'static'
|
||||
captureElement.style.left = 'auto'
|
||||
captureElement.style.top = 'auto'
|
||||
@@ -428,8 +460,10 @@ async function elementToPngBlob(element: HTMLElement): Promise<Blob> {
|
||||
const captureHeight = captureElement.scrollHeight
|
||||
const maxScale = Math.sqrt(MAX_EXPORT_PIXELS / Math.max(1, captureWidth * captureHeight))
|
||||
const scale = Math.min(SHARE_EXPORT_SCALE, maxScale)
|
||||
const backgroundColor = getComputedStyle(captureElement).backgroundColor || '#ffffff'
|
||||
const { default: html2canvas } = await import('html2canvas-pro')
|
||||
canvas = await html2canvas(captureElement, {
|
||||
backgroundColor: getComputedStyle(captureElement).backgroundColor || '#ffffff',
|
||||
backgroundColor,
|
||||
foreignObjectRendering: false,
|
||||
imageTimeout: 15000,
|
||||
logging: false,
|
||||
@@ -438,7 +472,7 @@ async function elementToPngBlob(element: HTMLElement): Promise<Blob> {
|
||||
useCORS: true,
|
||||
width: captureWidth,
|
||||
height: captureHeight,
|
||||
windowWidth: Math.max(SHARE_EXPORT_WIDTH, captureWidth),
|
||||
windowWidth: Math.max(exportWidth, captureWidth),
|
||||
windowHeight: Math.max(document.documentElement.clientHeight, captureHeight),
|
||||
})
|
||||
} finally {
|
||||
@@ -501,6 +535,16 @@ export function ShareTurnDialog(props: ShareTurnDialogProps) {
|
||||
naturalHeight: number
|
||||
} | null>(null)
|
||||
const showNativeShareButton = true
|
||||
const usesCoarsePrimaryPointer = window.matchMedia('(pointer: coarse)').matches
|
||||
const preserveSourceLayout = Boolean(
|
||||
props.sourceContentWidth
|
||||
&& props.sourceContentWidth > 0
|
||||
&& window.matchMedia('(min-width: 640px)').matches
|
||||
&& !usesCoarsePrimaryPointer
|
||||
)
|
||||
const exportWidth = preserveSourceLayout
|
||||
? Math.min(1240, Math.max(480, Math.ceil((props.sourceContentWidth ?? 0) + SHARE_EXPORT_HORIZONTAL_PADDING)))
|
||||
: SHARE_EXPORT_WIDTH
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setReady(false)
|
||||
@@ -558,7 +602,7 @@ export function ShareTurnDialog(props: ShareTurnDialogProps) {
|
||||
|
||||
let cancelled = false
|
||||
setPreparedBlob(null)
|
||||
void elementToPngBlob(capture).then((blob) => {
|
||||
void elementToPngBlob(capture, exportWidth, preserveSourceLayout).then((blob) => {
|
||||
if (!cancelled) setPreparedBlob(blob)
|
||||
}).catch((err) => {
|
||||
if (!cancelled) setError(err instanceof Error ? err.message : 'Failed to create image')
|
||||
@@ -566,7 +610,7 @@ export function ShareTurnDialog(props: ShareTurnDialogProps) {
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [props.isOpen, props.sourceSnapshots, ready, restoreTick, previewRevision])
|
||||
}, [props.isOpen, props.sourceSnapshots, ready, restoreTick, previewRevision, exportWidth, preserveSourceLayout])
|
||||
|
||||
const handlePreviewClick = (event: ReactMouseEvent<HTMLElement>) => {
|
||||
const target = event.target
|
||||
@@ -662,7 +706,7 @@ export function ShareTurnDialog(props: ShareTurnDialogProps) {
|
||||
setBusy(mode)
|
||||
setError(null)
|
||||
try {
|
||||
const blob = await elementToPngBlob(capture)
|
||||
const blob = await elementToPngBlob(capture, exportWidth, preserveSourceLayout)
|
||||
await action(blob)
|
||||
if (mode === 'copy') setCopied(true)
|
||||
} catch (err) {
|
||||
@@ -693,23 +737,22 @@ export function ShareTurnDialog(props: ShareTurnDialogProps) {
|
||||
display: block !important;
|
||||
height: 0.75rem !important;
|
||||
}
|
||||
.hapi-share-preview-root .hapi-share-media-grid:not([data-hapi-image-count="1"]) {
|
||||
display: grid !important;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
|
||||
align-items: start !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
.hapi-share-preview-root .hapi-share-media-grid:not([data-hapi-image-count="1"]) > button,
|
||||
.hapi-share-preview-root .hapi-share-media-grid:not([data-hapi-image-count="1"]) > button > img {
|
||||
width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
.hapi-share-preview-root .hapi-share-media-grid[data-hapi-image-count="2"] {
|
||||
flex-wrap: nowrap !important;
|
||||
}
|
||||
.hapi-share-preview-root .hapi-share-media-grid > button {
|
||||
height: auto !important;
|
||||
align-self: start !important;
|
||||
flex-shrink: 1 !important;
|
||||
min-width: 0 !important;
|
||||
cursor: zoom-in !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
.hapi-share-preview-root .hapi-share-media-grid > button > img {
|
||||
width: auto !important;
|
||||
max-width: 100% !important;
|
||||
height: auto !important;
|
||||
}
|
||||
`}</style>
|
||||
<div className="mb-4 border-b border-[var(--app-divider)] pb-3">
|
||||
<div className="min-w-0">
|
||||
|
||||
Reference in New Issue
Block a user