Skip to content

Commit 4cc52cb

Browse files
committed
feat: add download flow image
1 parent df60b13 commit 4cc52cb

File tree

9 files changed

+95
-10
lines changed

9 files changed

+95
-10
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { toPng } from 'html-to-image'
2+
import { Download } from 'lucide-react'
3+
import {
4+
Panel,
5+
useReactFlow
6+
} from 'reactflow'
7+
8+
interface DownloadButtonProps {
9+
title?: string
10+
}
11+
12+
const DownloadButton = ({ title = 'Decision Flow - UX Patterns for Devs' }: DownloadButtonProps) => {
13+
const reactFlowInstance = useReactFlow()
14+
15+
const onClick = () => {
16+
// Store the current viewport state
17+
const currentViewport = reactFlowInstance.getViewport()
18+
19+
// Get the viewport element
20+
const viewportElement = document.querySelector('.react-flow__viewport') as HTMLElement
21+
if (!viewportElement) return
22+
23+
// Get the flow wrapper element for dimensions
24+
const flowWrapper = document.querySelector('.react-flow') as HTMLElement
25+
if (!flowWrapper) return
26+
27+
// Check if dark mode is active
28+
const isDarkMode = document.documentElement.classList.contains('dark')
29+
30+
const rect = flowWrapper.getBoundingClientRect()
31+
32+
toPng(viewportElement, {
33+
backgroundColor: isDarkMode ? 'rgb(17 24 39)' : 'rgb(255 255 255)',
34+
width: rect.width,
35+
height: rect.height,
36+
style: {
37+
width: rect.width + 'px',
38+
height: rect.height + 'px',
39+
transform: `translate(${currentViewport.x}px, ${currentViewport.y}px) scale(${currentViewport.zoom})`
40+
},
41+
filter: (node) => {
42+
// Skip download button and controls in the image
43+
const className = node?.className?.toString() || ''
44+
return !className.includes('download-btn') && !className.includes('react-flow__controls')
45+
},
46+
}).then((dataUrl) => {
47+
const a = document.createElement('a')
48+
a.setAttribute('download', `${title}${` - UX Patterns for Devs`}.png`)
49+
a.setAttribute('href', dataUrl)
50+
a.click()
51+
})
52+
}
53+
54+
return (
55+
<Panel position="top-right" className="download-btn">
56+
<button
57+
type="button"
58+
onClick={onClick}
59+
className="flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700 rounded-md shadow-sm border border-gray-200 dark:border-gray-700 transition-colors"
60+
>
61+
<Download className="w-4 h-4" />
62+
Download as PNG
63+
</button>
64+
</Panel>
65+
)
66+
}
67+
68+
export default DownloadButton

app/_components/decision-flow/index.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import ReactFlow, {
1313
} from 'reactflow'
1414
import 'reactflow/dist/style.css'
1515

16+
import DownloadButton from './download-button'
1617
import { ConsiderationNode } from './nodes/ConsiderationNode'
1718
import { PatternNode } from './nodes/PatternNode'
1819
import { QuestionNode } from './nodes/QuestionNode'
@@ -99,7 +100,12 @@ const getLayoutedElements = (nodes: Node[], edges: Edge[]) => {
99100
return { nodes: layoutedNodes, edges: styledEdges }
100101
}
101102

102-
export function DecisionFlow({ nodes: initialNodes, edges: initialEdges, className = '' }: DecisionFlowProps) {
103+
export function DecisionFlow({
104+
nodes: initialNodes,
105+
edges: initialEdges,
106+
className = '',
107+
title
108+
}: DecisionFlowProps) {
103109
// Layout the nodes on first render
104110
const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(
105111
initialNodes,
@@ -126,11 +132,12 @@ export function DecisionFlow({ nodes: initialNodes, edges: initialEdges, classNa
126132
fitView
127133
minZoom={0.5}
128134
maxZoom={1.5}
129-
defaultViewport={{ x: 0, y: 0, zoom: 0.7 }} // Reduced default zoom
135+
defaultViewport={{ x: 0, y: 0, zoom: 0.7 }}
130136
attributionPosition="bottom-left"
131137
>
132138
<Background color="var(--flow-background-dots)" gap={16} />
133139
<Controls className="bg-white dark:bg-gray-800" />
140+
<DownloadButton title={title} />
134141
</ReactFlow>
135142
</div>
136143
)

app/_components/decision-flow/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ export interface DecisionFlowProps {
2525
nodes: DecisionNode[];
2626
edges: DecisionEdge[];
2727
className?: string;
28+
title?: string; // Optional title for the download filename
2829
}

app/_components/pattern-comparison/decision-flow-section.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ import { DecisionFlow } from '@app/_components/decision-flow'
44
import type { DecisionEdge, DecisionNode } from '@app/_components/decision-flow/types'
55

66
interface DecisionFlowSectionProps {
7+
title: string
78
nodes: DecisionNode[]
89
edges: DecisionEdge[]
910
}
1011

11-
export function DecisionFlowSection({ nodes, edges }: DecisionFlowSectionProps) {
12+
export function DecisionFlowSection({ title, nodes, edges }: DecisionFlowSectionProps) {
1213
return (
1314
<div className="mb-16">
1415
<h2 className="text-2xl font-bold mb-6">Interactive Decision Flow</h2>
@@ -17,6 +18,7 @@ export function DecisionFlowSection({ nodes, edges }: DecisionFlowSectionProps)
1718
nodes={nodes}
1819
edges={edges}
1920
className="rounded-xl"
21+
title={title}
2022
/>
2123
</div>
2224
</div>

content/en/pattern-assistant/choosing-input-types.mdx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import { PatternNextSteps } from "@app/_components/pattern-comparison/pattern-ne
3030

3131
</div>
3232

33-
<DecisionFlowSection nodes={inputTypeNodes} edges={inputTypeEdges} />
33+
<DecisionFlowSection nodes={inputTypeNodes} edges={inputTypeEdges} title="Input Types Decision Flow" />
3434
<ComparisonGrid patterns={comparisonData.patterns} />
3535
<ImplementationMetrics patterns={implementationData.patterns} />
3636
<ExamplesGrid examples={examplesData.examples} />
@@ -42,7 +42,7 @@ export const comparisonData = {
4242
{
4343
title: "Text Input",
4444
subtitle: "Best for single-line text entry and basic data collection",
45-
href: "/patterns/forms/text-input",
45+
href: "/patterns/forms/text-field",
4646
icon: "Input",
4747
theme: "pattern",
4848
criteria: [
@@ -70,7 +70,7 @@ export const comparisonData = {
7070
{
7171
title: "Select",
7272
subtitle: "Perfect for choosing from predefined options",
73-
href: "/patterns/forms/select",
73+
href: "/patterns/forms/selection-input",
7474
icon: "ListBox",
7575
theme: "pattern",
7676
criteria: [
@@ -413,5 +413,3 @@ export const nextStepsData = {
413413
},
414414
],
415415
};
416-
417-
;

content/en/pattern-assistant/pagination-vs-infinite-scroll.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import { PerformanceComparison } from "@app/_components/pattern-comparison/perfo
3434

3535
</div>
3636

37-
<DecisionFlowSection nodes={nodes} edges={edges} />
37+
<DecisionFlowSection nodes={nodes} edges={edges} title="Pagination vs Infinite Scroll Decision Flow" />
3838
<ComparisonGrid patterns={comparisonData.patterns} />
3939
<PerformanceComparison patterns={performanceData.patterns} />
4040
<ExamplesGrid examples={examplesData.examples} />
@@ -144,7 +144,7 @@ export const examplesData = {
144144
{
145145
title: "Pinterest Board",
146146
description: "Visual content discovery",
147-
imageUrl: "/examples/pinterest-infinite.webp",
147+
imageUrl: "/examples/pinterest-infinite.gif",
148148
},
149149
{
150150
title: "Facebook News Feed",

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"clsx": "^2.1.1",
3939
"dagre": "^0.8.5",
4040
"gray-matter": "^4.0.3",
41+
"html-to-image": "^1.11.11",
4142
"lucide-react": "^0.474.0",
4243
"markdown-to-jsx": "^7.7.3",
4344
"next": "^15.1.6",

pnpm-lock.yaml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
12.4 MB
Loading

0 commit comments

Comments
 (0)