From c47d037fcc87548608af6fd30f37ff5970b70aca Mon Sep 17 00:00:00 2001 From: Ananovo <78636812+techotaku39@users.noreply.github.com> Date: Wed, 26 Aug 2026 15:43:40 +0800 Subject: [PATCH 1/2] feat(web): add fullscreen preview for shared turns --- e2e/share-turn.spec.ts | 302 +++++++++++++++++- .../AssistantChat/ShareTurnDialog.tsx | 90 +++++- web/src/components/icons.tsx | 30 ++ web/src/index.css | 5 + web/src/lib/locales/en.ts | 2 + web/src/lib/locales/zh-CN.ts | 2 + 6 files changed, 423 insertions(+), 8 deletions(-) diff --git a/e2e/share-turn.spec.ts b/e2e/share-turn.spec.ts index 25fc702d50..4b1c85126f 100644 --- a/e2e/share-turn.spec.ts +++ b/e2e/share-turn.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from '@playwright/test' +import { devices, expect, test } from '@playwright/test' import { readFile } from 'node:fs/promises' function pngSize(bytes: Buffer): { width: number; height: number } { @@ -131,6 +131,7 @@ test('localizes the share dialog actions in Chinese', async ({ page }) => { const dialog = page.getByRole('dialog') await expect(dialog.getByRole('heading', { name: '将本轮对话分享为图片' })).toBeVisible() + await expect(dialog.locator('[data-hapi-share-control="fullscreen"]')).toHaveAttribute('aria-label', '打开全屏预览') await expect(dialog.getByText('分享会话', { exact: true })).toHaveCount(0) await expect(dialog.getByText('Generated by HAPI', { exact: true })).toBeVisible() await expect(dialog.getByRole('button', { name: '取消' })).toHaveCount(0) @@ -179,6 +180,168 @@ test('keeps code and image controls interactive in preview', async ({ page }, te await page.getByRole('button', { name: 'Open share preview' }).click() const dialog = page.getByRole('dialog', { name: 'Share turn as image' }) + const fullscreenButton = dialog.locator('[data-hapi-share-control="fullscreen"]') + await expect(fullscreenButton).toBeEnabled() + const closeButton = dialog.locator('button[aria-label="Close"]') + const fullscreenBox = await fullscreenButton.boundingBox() + const closeBox = await closeButton.boundingBox() + const dialogBox = await dialog.boundingBox() + expect(fullscreenBox).not.toBeNull() + expect(closeBox).not.toBeNull() + expect(dialogBox).not.toBeNull() + expect(fullscreenBox?.x ?? 0).toBeCloseTo((dialogBox?.x ?? 0) + 12, 0) + expect(fullscreenBox?.y ?? 0).toBeCloseTo((dialogBox?.y ?? 0) + 8, 0) + expect((closeBox?.x ?? 0) + (closeBox?.width ?? 0)).toBeCloseTo((dialogBox?.x ?? 0) + (dialogBox?.width ?? 0) - 12, 0) + expect(closeBox?.y ?? 0).toBeCloseTo((dialogBox?.y ?? 0) + 8, 0) + const fullscreenIconBox = await fullscreenButton.locator('svg').boundingBox() + const closeIconBox = await closeButton.locator('svg').boundingBox() + expect(fullscreenIconBox?.width ?? 0).toBeCloseTo(closeIconBox?.width ?? 0, 0) + expect(fullscreenIconBox?.height ?? 0).toBeCloseTo(closeIconBox?.height ?? 0, 0) + + const shareAction = dialog.locator('[data-hapi-share-control="share"]') + const copyAction = dialog.locator('[data-hapi-share-control="copy"]') + const downloadAction = dialog.locator('[data-hapi-share-control="download"]') + const shareBox = await shareAction.boundingBox() + const copyBox = await copyAction.boundingBox() + const downloadBox = await downloadAction.boundingBox() + expect(copyBox?.x ?? 0).toBeLessThan(shareBox?.x ?? 0) + expect(shareBox?.x ?? 0).toBeLessThan(downloadBox?.x ?? 0) + + const initialDialogBox = await dialog.boundingBox() + const previewSurface = dialog.locator('.hapi-share-preview-root') + const previewScroll = dialog.locator('.hapi-share-preview-scroll') + await expect(previewSurface).toHaveClass(/sm:px-5/) + await expect(previewSurface).toHaveCSS('padding-left', '14px') + const measureHeaderSpacing = async () => await dialog.evaluate((element) => { + const dialogBox = element.getBoundingClientRect() + const title = element.querySelector('h2') + const preview = element.querySelector('.hapi-share-preview-scroll') + const actions = element.querySelector('[data-hapi-share-control="download"]')?.parentElement + const root = element.querySelector('.hapi-share-preview-root') + const footer = element.querySelector('[data-hapi-share-footer="true"]') + if (!title || !preview || !actions || !root || !footer) throw new Error('Missing share dialog spacing elements') + const titleBox = title.getBoundingClientRect() + const previewBox = preview.getBoundingClientRect() + const actionsBox = actions.getBoundingClientRect() + const rootStyle = getComputedStyle(root) + const footerStyle = getComputedStyle(footer) + return { + topToTitle: titleBox.top - dialogBox.top, + titleToPreview: previewBox.top - titleBox.bottom, + previewToActions: actionsBox.top - previewBox.bottom, + actionsToBottom: dialogBox.bottom - actionsBox.bottom, + rootPaddingTop: parseFloat(rootStyle.paddingTop), + rootPaddingBottom: parseFloat(rootStyle.paddingBottom), + footerPaddingTop: parseFloat(footerStyle.paddingTop) + } + }) + const measureFooterSpacing = async () => await previewScroll.evaluate((element) => { + const root = element.querySelector('.hapi-share-preview-root') + const footer = root?.querySelector('[data-hapi-share-footer="true"]') + const watermark = root?.querySelector('[data-hapi-share-watermark="true"]') + if (!root || !footer || !watermark) throw new Error('Missing share footer elements') + const previousScrollTop = element.scrollTop + element.scrollTop = element.scrollHeight + const footerBox = footer.getBoundingClientRect() + const watermarkBox = watermark.getBoundingClientRect() + const scrollBox = element.getBoundingClientRect() + const result = { + dividerToWatermark: watermarkBox.top - footerBox.top, + watermarkToScrollBottom: scrollBox.bottom - watermarkBox.bottom, + scrollPaddingBottom: parseFloat(getComputedStyle(element).paddingBottom) + } + element.scrollTop = previousScrollTop + return result + }) + const measurePreviewGaps = async () => await previewScroll.evaluate((element) => { + const root = element.querySelector('.hapi-share-preview-root') + if (!root) throw new Error('Missing share preview root') + const title = root.querySelector('.text-lg') + if (!title) throw new Error('Missing share preview title') + const scrollBox = element.getBoundingClientRect() + const titleBox = title.getBoundingClientRect() + const styles = getComputedStyle(element) + return { + left: titleBox.left - scrollBox.left, + right: scrollBox.right - titleBox.right, + top: titleBox.top - scrollBox.top, + scrollbarGutter: styles.scrollbarGutter + } + }) + const initialPreviewWidth = await previewSurface.boundingBox() + expect(initialDialogBox).not.toBeNull() + expect(initialPreviewWidth).not.toBeNull() + const initialPreviewGaps = await measurePreviewGaps() + expect(initialPreviewGaps.scrollbarGutter).toContain('stable both-edges') + expect(Math.abs(initialPreviewGaps.left - initialPreviewGaps.right)).toBeLessThan(1) + // Desktop title glyphs sit about 4px below their 21px line-box inset; + // the visible ink therefore aligns with the 25px horizontal inset. + expect(initialPreviewGaps.left).toBeCloseTo(25, 0) + expect(initialPreviewGaps.right).toBeCloseTo(25, 0) + expect(initialPreviewGaps.top).toBeCloseTo(14, 0) + const initialHeaderSpacing = await measureHeaderSpacing() + expect(initialHeaderSpacing.topToTitle).toBeCloseTo(16, 0) + expect(initialHeaderSpacing.titleToPreview).toBeCloseTo(16, 0) + expect(initialHeaderSpacing.previewToActions).toBeCloseTo(16, 0) + expect(initialHeaderSpacing.actionsToBottom).toBeCloseTo(16, 0) + expect(initialHeaderSpacing.rootPaddingTop).toBeCloseTo(1, 0) + expect(initialHeaderSpacing.rootPaddingBottom).toBeCloseTo(12, 0) + expect(initialHeaderSpacing.footerPaddingTop).toBeCloseTo(12, 0) + const initialFooterSpacing = await measureFooterSpacing() + expect(initialFooterSpacing.watermarkToScrollBottom).toBeCloseTo(initialFooterSpacing.dividerToWatermark, 0) + expect(initialFooterSpacing.scrollPaddingBottom).toBeCloseTo(0, 0) + + await fullscreenButton.click() + await expect(fullscreenButton).toHaveAttribute('aria-label', 'Exit full-screen preview') + const fullScreenDialogBox = await dialog.boundingBox() + const fullScreenPreviewWidth = await previewSurface.boundingBox() + expect(fullScreenDialogBox).not.toBeNull() + expect(fullScreenPreviewWidth).not.toBeNull() + expect(fullScreenDialogBox?.x ?? 0).toBe(0) + expect(fullScreenDialogBox?.y ?? 0).toBe(0) + expect(fullScreenDialogBox?.width ?? 0).toBeGreaterThan(initialDialogBox?.width ?? 0) + expect(fullScreenDialogBox?.height ?? 0).toBeGreaterThan(initialDialogBox?.height ?? 0) + expect(fullScreenPreviewWidth?.width ?? 0).toBeGreaterThan(initialPreviewWidth?.width ?? 0) + const fullScreenPreviewGaps = await measurePreviewGaps() + expect(fullScreenPreviewGaps.scrollbarGutter).toContain('stable both-edges') + expect(Math.abs(fullScreenPreviewGaps.left - fullScreenPreviewGaps.right)).toBeLessThan(1) + expect(fullScreenPreviewGaps.left).toBeCloseTo(25, 0) + expect(fullScreenPreviewGaps.right).toBeCloseTo(25, 0) + expect(fullScreenPreviewGaps.top).toBeCloseTo(14, 0) + const fullScreenHeaderSpacing = await measureHeaderSpacing() + expect(fullScreenHeaderSpacing.topToTitle).toBeCloseTo(16, 0) + expect(fullScreenHeaderSpacing.titleToPreview).toBeCloseTo(16, 0) + expect(fullScreenHeaderSpacing.previewToActions).toBeCloseTo(16, 0) + expect(fullScreenHeaderSpacing.actionsToBottom).toBeCloseTo(16, 0) + expect(fullScreenHeaderSpacing.rootPaddingTop).toBeCloseTo(1, 0) + expect(fullScreenHeaderSpacing.rootPaddingBottom).toBeCloseTo(12, 0) + expect(fullScreenHeaderSpacing.footerPaddingTop).toBeCloseTo(12, 0) + const fullScreenFooterSpacing = await measureFooterSpacing() + expect(fullScreenFooterSpacing.watermarkToScrollBottom).toBeCloseTo(fullScreenFooterSpacing.dividerToWatermark, 0) + expect(fullScreenFooterSpacing.scrollPaddingBottom).toBeCloseTo(0, 0) + await expect(dialog.getByText(/type ExportResult/)).toBeVisible() + + const selectedText = await dialog.locator('[data-hapi-share-body="true"]').evaluate((element) => { + const selection = window.getSelection() + const range = document.createRange() + range.selectNodeContents(element) + selection?.removeAllRanges() + selection?.addRange(range) + const text = selection?.toString() ?? '' + selection?.removeAllRanges() + return text + }) + expect(selectedText).toContain('type ExportResult') + + const fullScreenCopyButton = dialog.locator('[data-hapi-code-copy="true"]').first() + await fullScreenCopyButton.click() + await expect(fullScreenCopyButton).toHaveAttribute('title', 'Copied') + await expect.poll(() => page.evaluate(() => navigator.clipboard.readText())).toContain('type ExportResult') + + await fullscreenButton.click() + await expect(fullscreenButton).toHaveAttribute('aria-label', 'Open full-screen preview') + await expect(dialog).toBeVisible() + const wrapButton = dialog.locator('[data-hapi-code-wrap-toggle="true"]').first() await expect(wrapButton).toHaveAttribute('aria-pressed', 'false') @@ -188,6 +351,7 @@ test('keeps code and image controls interactive in preview', async ({ page }, te const unwrappedPath = testInfo.outputPath('interactive-unwrapped.png') await unwrappedDownload.saveAs(unwrappedPath) const unwrappedSize = pngSize(await readFile(unwrappedPath)) + expect(unwrappedSize.width).toBe(1920) await wrapButton.click() await expect(wrapButton).toHaveAttribute('aria-pressed', 'true') @@ -226,6 +390,140 @@ test('keeps code and image controls interactive in preview', async ({ page }, te await expect(dialog).toBeVisible() }) +test('remembers the fullscreen preference across share dialog openings', async ({ page }) => { + await page.addInitScript(() => localStorage.removeItem('hapi-share-preview-fullscreen')) + await page.goto('/e2e-fixtures/share-turn-fixture.html') + + const openPreview = page.getByRole('button', { name: 'Open share preview' }) + await openPreview.click() + let dialog = page.getByRole('dialog', { name: 'Share turn as image' }) + const fullscreenButton = dialog.locator('[data-hapi-share-control="fullscreen"]') + await expect(fullscreenButton).toHaveAttribute('aria-label', 'Open full-screen preview') + + await fullscreenButton.click() + await expect(fullscreenButton).toHaveAttribute('aria-label', 'Exit full-screen preview') + await expect.poll(() => page.evaluate(() => localStorage.getItem('hapi-share-preview-fullscreen'))).toBe('true') + + await dialog.locator('button[aria-label="Close"]').click() + await expect(dialog).toHaveCount(0) + + await openPreview.click() + dialog = page.getByRole('dialog', { name: 'Share turn as image' }) + await expect(dialog.locator('[data-hapi-share-control="fullscreen"]')).toHaveAttribute('aria-label', 'Exit full-screen preview') + + await dialog.locator('[data-hapi-share-control="fullscreen"]').click() + await expect(dialog.locator('[data-hapi-share-control="fullscreen"]')).toHaveAttribute('aria-label', 'Open full-screen preview') + await expect.poll(() => page.evaluate(() => localStorage.getItem('hapi-share-preview-fullscreen'))).toBe('false') + + await dialog.locator('button[aria-label="Close"]').click() + await expect(dialog).toHaveCount(0) + await openPreview.click() + await expect(page.getByRole('dialog', { name: 'Share turn as image' }).locator('[data-hapi-share-control="fullscreen"]')) + .toHaveAttribute('aria-label', 'Open full-screen preview') +}) + +test('keeps the mobile preview canvas margins symmetric before and after fullscreen', async ({ browser }) => { + const context = await browser.newContext({ ...devices['iPhone 13'] }) + const page = await context.newPage() + try { + await page.goto('/e2e-fixtures/share-turn-fixture.html') + await page.getByRole('button', { name: 'Open share preview' }).click() + + const dialog = page.getByRole('dialog', { name: 'Share turn as image' }) + const fullscreenButton = dialog.locator('[data-hapi-share-control="fullscreen"]') + const scroll = dialog.locator('.hapi-share-preview-scroll') + const dialogHeading = dialog.getByRole('heading', { name: 'Share turn as image' }) + const measureHeaderSpacing = async () => await dialog.evaluate((element) => { + const dialogBox = element.getBoundingClientRect() + const title = element.querySelector('h2') + const preview = element.querySelector('.hapi-share-preview-scroll') + const actions = element.querySelector('[data-hapi-share-control="download"]')?.parentElement + if (!title || !preview || !actions) throw new Error('Missing mobile share dialog spacing elements') + const titleBox = title.getBoundingClientRect() + const previewBox = preview.getBoundingClientRect() + const actionsBox = actions.getBoundingClientRect() + return { + topToTitle: titleBox.top - dialogBox.top, + titleToPreview: previewBox.top - titleBox.bottom, + previewToActions: actionsBox.top - previewBox.bottom, + actionsToBottom: dialogBox.bottom - actionsBox.bottom + } + }) + const measureControlCenters = async () => { + const headingBox = await dialogHeading.boundingBox() + const fullscreenBox = await fullscreenButton.boundingBox() + const closeBox = await dialog.locator('button[aria-label="Close"]').boundingBox() + if (!headingBox || !fullscreenBox || !closeBox) throw new Error('Missing mobile share control geometry') + return { + headingCenterY: headingBox.y + headingBox.height / 2, + fullscreenCenterY: fullscreenBox.y + fullscreenBox.height / 2, + closeCenterY: closeBox.y + closeBox.height / 2 + } + } + const measureGaps = async () => await scroll.evaluate((element) => { + const root = element.querySelector('.hapi-share-preview-root') + if (!root) throw new Error('Missing share preview root') + const scrollBox = element.getBoundingClientRect() + const rootBox = root.getBoundingClientRect() + const styles = getComputedStyle(element) + const dialogElement = element.closest('[role="dialog"]') + if (!dialogElement) throw new Error('Missing share preview dialog') + const dialogStyles = getComputedStyle(dialogElement) + const rootStyles = getComputedStyle(root) + return { + left: rootBox.left - scrollBox.left, + right: scrollBox.right - rootBox.right, + scrollbarGutter: styles.scrollbarGutter, + paddingInline: parseFloat(styles.paddingLeft), + dialogPaddingInline: parseFloat(dialogStyles.paddingLeft), + rootPaddingInline: parseFloat(rootStyles.paddingLeft), + rootPaddingTop: parseFloat(rootStyles.paddingTop), + rootPaddingBottom: parseFloat(rootStyles.paddingBottom) + } + }) + + const initialGaps = await measureGaps() + expect(initialGaps.scrollbarGutter).toContain('stable both-edges') + expect(initialGaps.paddingInline).toBeCloseTo(4, 1) + expect(initialGaps.dialogPaddingInline).toBeCloseTo(16, 1) + expect(initialGaps.rootPaddingInline).toBeCloseTo(8, 1) + expect(initialGaps.rootPaddingTop).toBeCloseTo(12, 1) + expect(initialGaps.rootPaddingBottom).toBeCloseTo(8, 1) + expect(initialGaps.paddingInline).toBeCloseTo(4, 1) + expect(Math.abs(initialGaps.left - initialGaps.right)).toBeLessThan(1) + const initialControlCenters = await measureControlCenters() + expect(initialControlCenters.fullscreenCenterY).toBeCloseTo(initialControlCenters.headingCenterY, 0) + expect(initialControlCenters.closeCenterY).toBeCloseTo(initialControlCenters.headingCenterY, 0) + const initialHeaderSpacing = await measureHeaderSpacing() + expect(initialHeaderSpacing.topToTitle).toBeCloseTo(16, 0) + expect(initialHeaderSpacing.titleToPreview).toBeCloseTo(16, 0) + expect(initialHeaderSpacing.previewToActions).toBeCloseTo(16, 0) + expect(initialHeaderSpacing.actionsToBottom).toBeCloseTo(16, 0) + + await fullscreenButton.click() + await expect(fullscreenButton).toHaveAttribute('aria-label', 'Exit full-screen preview') + const fullscreenGaps = await measureGaps() + expect(fullscreenGaps.scrollbarGutter).toContain('stable both-edges') + expect(fullscreenGaps.paddingInline).toBeCloseTo(4, 1) + expect(fullscreenGaps.dialogPaddingInline).toBeCloseTo(16, 1) + expect(fullscreenGaps.rootPaddingInline).toBeCloseTo(8, 1) + expect(fullscreenGaps.rootPaddingTop).toBeCloseTo(12, 1) + expect(fullscreenGaps.rootPaddingBottom).toBeCloseTo(8, 1) + expect(fullscreenGaps.paddingInline).toBeCloseTo(4, 1) + expect(Math.abs(fullscreenGaps.left - fullscreenGaps.right)).toBeLessThan(1) + const fullscreenControlCenters = await measureControlCenters() + expect(fullscreenControlCenters.fullscreenCenterY).toBeCloseTo(fullscreenControlCenters.headingCenterY, 0) + expect(fullscreenControlCenters.closeCenterY).toBeCloseTo(fullscreenControlCenters.headingCenterY, 0) + const fullscreenHeaderSpacing = await measureHeaderSpacing() + expect(fullscreenHeaderSpacing.topToTitle).toBeCloseTo(16, 0) + expect(fullscreenHeaderSpacing.titleToPreview).toBeCloseTo(16, 0) + expect(fullscreenHeaderSpacing.previewToActions).toBeCloseTo(16, 0) + expect(fullscreenHeaderSpacing.actionsToBottom).toBeCloseTo(16, 0) + } finally { + await context.close() + } +}) + 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 }) @@ -239,7 +537,7 @@ test('preserves the desktop source width but keeps mobile export width stable', 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) + expect(previewWidth).toBeLessThanOrEqual(736) const inlineCode = preview.locator('.aui-md-code').last() await expect(inlineCode).toHaveCSS('display', 'inline') diff --git a/web/src/components/AssistantChat/ShareTurnDialog.tsx b/web/src/components/AssistantChat/ShareTurnDialog.tsx index 019f328228..0f50e7ec3e 100644 --- a/web/src/components/AssistantChat/ShareTurnDialog.tsx +++ b/web/src/components/AssistantChat/ShareTurnDialog.tsx @@ -2,9 +2,11 @@ import { useEffect, useLayoutEffect, useRef, useState, type MouseEvent as ReactM import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { useTranslation } from '@/lib/use-translation' import { AgentFlavorIcon } from '@/components/AgentFlavorIcon' +import { FullscreenExitIcon, FullscreenIcon } from '@/components/icons' import { ZoomableLightbox } from '@/components/ZoomableLightbox' import { safeCopyToClipboard } from '@/lib/clipboard' import type { ShareTurnMetadataItem } from '@/lib/shareTurnMetadata' +import { cn } from '@/lib/utils' type ShareTurnDialogProps = { isOpen: boolean @@ -26,6 +28,23 @@ 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' +const SHARE_FULLSCREEN_STORAGE_KEY = 'hapi-share-preview-fullscreen' + +function readShareFullscreenPreference(): boolean { + try { + return window.localStorage.getItem(SHARE_FULLSCREEN_STORAGE_KEY) === 'true' + } catch { + return false + } +} + +function writeShareFullscreenPreference(value: boolean): void { + try { + window.localStorage.setItem(SHARE_FULLSCREEN_STORAGE_KEY, String(value)) + } catch { + // Fullscreen still works when browser storage is unavailable. + } +} function nextFrame(): Promise { return new Promise((resolve) => { @@ -164,6 +183,7 @@ function prepareExportElement(element: HTMLElement, exportWidth: number, preserv captureElement.classList.add('hapi-share-export-root') stripExportControls(captureElement) + captureElement.style.paddingBottom = '14px' captureElement.style.cssText += [ 'position:absolute', 'left:0', @@ -525,6 +545,7 @@ export function ShareTurnDialog(props: ShareTurnDialogProps) { const [ready, setReady] = useState(false) const [preparedBlob, setPreparedBlob] = useState(null) const [previewRevision, setPreviewRevision] = useState(0) + const [isFullScreen, setIsFullScreen] = useState(readShareFullscreenPreference) const [previewImage, setPreviewImage] = useState<{ src: string label: string @@ -713,23 +734,77 @@ export function ShareTurnDialog(props: ShareTurnDialogProps) { } } + const toggleFullScreen = () => { + const nextFullScreen = !isFullScreen + setIsFullScreen(nextFullScreen) + writeShareFullscreenPreference(nextFullScreen) + } + return ( { if (!open) props.onClose() }}> button:last-child]:top-2', + isFullScreen + ? 'flex flex-col rounded-none' + : 'max-h-[calc(100vh-24px)] max-w-3xl' + )} + style={isFullScreen ? { + left: 0, + top: 0, + width: '100vw', + maxWidth: 'none', + height: '100dvh', + maxHeight: '100dvh', + transform: 'none', + translate: '0 0' + } : undefined} aria-describedby={undefined} > - + + {t('shareTurn.title')} -
+