Add Pan Icon and Pan Filled Icon Components for Template Preview

- Introduced new PanIcon and PanFilledIcon components to enhance the TemplatePreview functionality.
- Implemented panning feature in TemplatePreview, allowing users to navigate through content more intuitively.
- Updated TemplatePreview to toggle panning mode with visual feedback using the new icons.
- Enhanced user experience by managing pointer events and cursor styles during panning.
This commit is contained in:
Tom Butcher 2026-08-21 23:15:41 +01:00
parent fede8af6aa
commit 53d488011b
5 changed files with 129 additions and 3 deletions

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.935415,0,0,0.935415,2.292349,1.999999)">
<path d="M23.559,56.897C33.746,67.068 47.027,66.068 56.324,56.787C62.652,50.459 64.606,43.662 62.965,35.615C62.027,29.131 57.996,21.49 55.684,17.1C54.012,13.865 51.918,9.693 50.762,8.178C49.465,6.443 47.402,6.068 45.73,7.318C43.809,8.709 43.543,10.803 44.496,13.521L48.309,23.537C48.621,24.365 48.574,24.818 48.262,25.131C47.934,25.443 47.48,25.521 46.84,24.881L23.199,1.209C21.559,-0.4 19.137,-0.4 17.527,1.193C15.934,2.803 15.934,5.256 17.543,6.865L35.215,24.537C34.512,24.896 33.809,25.271 33.09,25.74L12.824,5.475C11.199,3.834 8.777,3.834 7.168,5.475C5.559,7.084 5.559,9.521 7.168,11.131L27.199,31.162C26.684,31.787 26.152,32.443 25.684,33.084L7.246,14.631C5.637,13.037 3.199,13.021 1.574,14.615C-0.004,16.225 -0.004,18.678 1.59,20.287L21.574,40.256C21.23,41.131 20.996,41.975 20.762,42.803L6.84,28.881C5.246,27.24 2.809,27.24 1.184,28.834C-0.395,30.475 -0.395,32.912 1.184,34.537L23.559,56.897Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

7
assets/icons/panicon.svg Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.89584,0,0,0.89584,2.323959,1.951688)">
<path d="M58.333,59.078C67.13,50.281 68.599,39.468 62.739,27.046L55.505,11.796C54.911,10.546 54.208,9.531 53.38,8.734C50.583,5.921 46.38,5.843 43.489,8.718C41.864,10.343 41.536,12.781 42.083,15.265L43.192,17.953C43.224,18.062 43.224,18.156 43.177,18.234C43.067,18.343 42.942,18.281 42.864,18.203L27.052,2.39C23.927,-0.766 19.692,-0.704 16.552,2.453C15.802,3.171 15.333,4.078 15.099,5.046C12.271,3.484 9.239,3.953 6.817,6.359C4.521,8.64 4.083,11.765 5.521,14.546C4.63,14.718 3.817,15.171 3.161,15.828C0.239,18.734 0.224,22.812 3.192,25.765L4.536,27.125C3.692,27.265 2.833,27.687 2.146,28.359C-0.776,31.312 -0.714,35.593 2.333,38.593L22.224,58.515C33.458,69.765 47.614,69.781 58.333,59.078ZM53.302,54.328C45.224,62.406 35.177,62.281 26.411,53.484L7.505,34.562C6.833,33.89 6.802,32.984 7.458,32.343C8.099,31.703 9.036,31.687 9.724,32.359L21.239,43.874C22.395,45.031 24.192,44.968 25.302,43.859C26.427,42.734 26.505,40.937 25.349,39.765L8.005,22.406C7.317,21.734 7.286,20.843 7.958,20.218C8.567,19.546 9.505,19.562 10.192,20.218L26.349,36.39C27.505,37.546 29.317,37.468 30.442,36.359C31.536,35.234 31.614,33.406 30.474,32.265L11.411,13.218C10.724,12.546 10.724,11.624 11.364,10.968C11.989,10.343 12.927,10.328 13.599,10.999L31.974,29.39C33.114,30.5 34.911,30.468 36.036,29.343C37.177,28.25 37.224,26.39 36.083,25.265L20.599,9.781C19.942,9.109 19.927,8.234 20.567,7.578C21.208,6.921 22.13,6.921 22.786,7.593L45.27,30.031C46.802,31.593 48.755,31.39 49.974,30.109C50.989,29.078 51.474,27.562 50.77,25.75L46.802,15.374C46.38,14.343 46.817,13.531 47.567,13.187C48.411,12.828 49.208,13.187 49.677,14.187L56.864,29.843C61.536,40.093 60.067,47.546 53.302,54.328Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -11,6 +11,8 @@ import { Flex, Button, Input, Select } from 'antd'
import PlusIcon from '../../Icons/PlusIcon.jsx'
import MinusIcon from '../../Icons/MinusIcon.jsx'
import InfoCircleIcon from '../../Icons/InfoCircleIcon.jsx'
import PanIcon from '../../Icons/PanIcon.jsx'
import PanFilledIcon from '../../Icons/PanFilledIcon.jsx'
import ObjectProperty from '../common/ObjectProperty.jsx'
import ScrollBox from './ScrollBox.jsx'
import { ApiServerContext } from '../context/ApiServerContext.jsx'
@ -39,6 +41,9 @@ const TemplatePreview = ({
const [reloadLoading, setReloadLoading] = useState(false)
const [previewScale, setPreviewScale] = useState(1)
const [previewType, setPreviewType] = useState('HTML')
const [panMode, setPanMode] = useState(false)
const [isPanning, setIsPanning] = useState(false)
const panStartRef = useRef({ x: 0, y: 0, scrollLeft: 0, scrollTop: 0 })
const [iframeSize, setIframeSize] = useState({
width: '100%',
height: 'auto'
@ -236,6 +241,74 @@ const TemplatePreview = ({
}
}, [])
useEffect(() => {
if (!panMode) {
setIsPanning(false)
}
}, [panMode])
useEffect(() => {
const scrollEl = scrollElementRef.current
if (!panMode || !scrollEl || previewType != 'HTML') {
return
}
const handlePointerDown = (event) => {
if (event.pointerType === 'mouse' && event.button !== 0) {
return
}
event.preventDefault()
panStartRef.current = {
x: event.clientX,
y: event.clientY,
scrollLeft: scrollEl.scrollLeft,
scrollTop: scrollEl.scrollTop
}
setIsPanning(true)
}
scrollEl.addEventListener('pointerdown', handlePointerDown)
return () => {
scrollEl.removeEventListener('pointerdown', handlePointerDown)
}
}, [panMode, previewType])
useEffect(() => {
if (!isPanning) {
return
}
const previousCursor = document.body.style.cursor
const previousUserSelect = document.body.style.userSelect
document.body.style.cursor = 'grabbing'
document.body.style.userSelect = 'none'
const handlePointerMove = (event) => {
const scrollEl = scrollElementRef.current
if (!scrollEl) {
return
}
const { x, y, scrollLeft, scrollTop } = panStartRef.current
scrollEl.scrollLeft = scrollLeft - (event.clientX - x)
scrollEl.scrollTop = scrollTop - (event.clientY - y)
}
const handlePointerUp = () => {
setIsPanning(false)
}
window.addEventListener('pointermove', handlePointerMove)
window.addEventListener('pointerup', handlePointerUp)
window.addEventListener('pointercancel', handlePointerUp)
return () => {
document.body.style.cursor = previousCursor
document.body.style.userSelect = previousUserSelect
window.removeEventListener('pointermove', handlePointerMove)
window.removeEventListener('pointerup', handlePointerUp)
window.removeEventListener('pointercancel', handlePointerUp)
}
}, [isPanning])
return (
<Flex vertical gap={'middle'} style={{ height: '100%' }}>
<Flex gap={'small'}>
@ -266,6 +339,19 @@ const TemplatePreview = ({
</>
) : null}
<Button
icon={
panMode ? (
<PanFilledIcon style={{ color: 'var(--color-primary)' }} />
) : (
<PanIcon />
)
}
disabled={loading || reloadLoading || previewType == 'PDF'}
onClick={() => {
setPanMode((prev) => !prev)
}}
/>
<Button
icon={<PlusIcon />}
onClick={() => {
@ -314,11 +400,22 @@ const TemplatePreview = ({
}}
>
{previewType == 'HTML' ? (
<ScrollBox scrollableNodeProps={{ ref: scrollElementRef }}>
<ScrollBox
scrollableNodeProps={{ ref: scrollElementRef }}
style={{
cursor: panMode ? (isPanning ? 'grabbing' : 'grab') : undefined,
touchAction: panMode ? 'none' : undefined
}}
>
<div
style={{
width: iframeSize.width,
height: iframeSize.height
height: iframeSize.height,
cursor: panMode
? isPanning
? 'grabbing'
: 'grab'
: undefined
}}
>
<iframe
@ -332,7 +429,8 @@ const TemplatePreview = ({
height: iframeSize.height === 'auto' ? 150 : '100%',
display: 'block',
border: 0,
overflow: 'hidden'
overflow: 'hidden',
pointerEvents: panMode ? 'none' : undefined
}}
/>
</div>

View File

@ -0,0 +1,8 @@
import Icon from '@ant-design/icons'
import CustomIconSvg from '../../../assets/icons/panfilledicon.svg?react'
const PanFilledIcon = (props) => (
<Icon component={CustomIconSvg} {...props} />
)
export default PanFilledIcon

View File

@ -0,0 +1,6 @@
import Icon from '@ant-design/icons'
import CustomIconSvg from '../../../assets/icons/panicon.svg?react'
const PanIcon = (props) => <Icon component={CustomIconSvg} {...props} />
export default PanIcon