dify
This commit is contained in:
51
dify/web/app/components/base/svg-gallery/index.stories.tsx
Normal file
51
dify/web/app/components/base/svg-gallery/index.stories.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs'
|
||||
import SVGRenderer from '.'
|
||||
|
||||
const SAMPLE_SVG = `
|
||||
<svg width="400" height="280" viewBox="0 0 400 280" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#D1E9FF"/>
|
||||
<stop offset="100%" stop-color="#FBE8FF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="400" height="280" rx="24" fill="url(#bg)"/>
|
||||
<g font-family="sans-serif" fill="#1F2937" text-anchor="middle">
|
||||
<text x="200" y="120" font-size="32" font-weight="600">SVG Preview</text>
|
||||
<text x="200" y="160" font-size="16">Click to open high-resolution preview</text>
|
||||
</g>
|
||||
<circle cx="320" cy="70" r="28" fill="#E0F2FE" stroke="#2563EB" stroke-width="4"/>
|
||||
<circle cx="80" cy="200" r="18" fill="#FDE68A" stroke="#CA8A04" stroke-width="4"/>
|
||||
<rect x="120" y="190" width="160" height="48" rx="12" fill="#FFF" opacity="0.85"/>
|
||||
<text x="200" y="220" font-size="16" font-weight="500">Inline SVG asset</text>
|
||||
</svg>
|
||||
`.trim()
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Data Display/SVGRenderer',
|
||||
component: SVGRenderer,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Renders sanitized SVG markup with zoom-to-preview capability.',
|
||||
},
|
||||
source: {
|
||||
language: 'tsx',
|
||||
code: `
|
||||
<SVGRenderer content={\`
|
||||
<svg width="400" height="280" ...>...</svg>
|
||||
\`} />
|
||||
`.trim(),
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
args: {
|
||||
content: SAMPLE_SVG,
|
||||
},
|
||||
} satisfies Meta<typeof SVGRenderer>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {}
|
||||
78
dify/web/app/components/base/svg-gallery/index.tsx
Normal file
78
dify/web/app/components/base/svg-gallery/index.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { SVG } from '@svgdotjs/svg.js'
|
||||
import DOMPurify from 'dompurify'
|
||||
import ImagePreview from '@/app/components/base/image-uploader/image-preview'
|
||||
|
||||
const SVGRenderer = ({ content }: { content: string }) => {
|
||||
const svgRef = useRef<HTMLDivElement>(null)
|
||||
const [imagePreview, setImagePreview] = useState('')
|
||||
const [windowSize, setWindowSize] = useState({
|
||||
width: typeof window !== 'undefined' ? window.innerWidth : 0,
|
||||
height: typeof window !== 'undefined' ? window.innerHeight : 0,
|
||||
})
|
||||
|
||||
const svgToDataURL = (svgElement: Element): string => {
|
||||
const svgString = new XMLSerializer().serializeToString(svgElement)
|
||||
const base64String = Buffer.from(svgString).toString('base64')
|
||||
return `data:image/svg+xml;base64,${base64String}`
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setWindowSize({ width: window.innerWidth, height: window.innerHeight })
|
||||
}
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
return () => window.removeEventListener('resize', handleResize)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (svgRef.current) {
|
||||
try {
|
||||
svgRef.current.innerHTML = ''
|
||||
const draw = SVG().addTo(svgRef.current)
|
||||
|
||||
const parser = new DOMParser()
|
||||
const svgDoc = parser.parseFromString(content, 'image/svg+xml')
|
||||
const svgElement = svgDoc.documentElement
|
||||
|
||||
if (!(svgElement instanceof SVGElement))
|
||||
throw new Error('Invalid SVG content')
|
||||
|
||||
const originalWidth = Number.parseInt(svgElement.getAttribute('width') || '400', 10)
|
||||
const originalHeight = Number.parseInt(svgElement.getAttribute('height') || '600', 10)
|
||||
draw.viewbox(0, 0, originalWidth, originalHeight)
|
||||
|
||||
svgRef.current.style.width = `${Math.min(originalWidth, 298)}px`
|
||||
|
||||
const rootElement = draw.svg(DOMPurify.sanitize(content))
|
||||
|
||||
rootElement.click(() => {
|
||||
setImagePreview(svgToDataURL(svgElement as Element))
|
||||
})
|
||||
}
|
||||
catch {
|
||||
if (svgRef.current)
|
||||
svgRef.current.innerHTML = '<span style="padding: 1rem;">Error rendering SVG. Wait for the image content to complete.</span>'
|
||||
}
|
||||
}
|
||||
}, [content, windowSize])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={svgRef} style={{
|
||||
maxHeight: '80vh',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'normal',
|
||||
margin: '0 auto',
|
||||
}} />
|
||||
{imagePreview && (<ImagePreview url={imagePreview} title='Preview' onCancel={() => setImagePreview('')} />)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SVGRenderer
|
||||
Reference in New Issue
Block a user