diff --git a/e2e/inline-edit/rename-thumb-page.spec.ts b/e2e/inline-edit/rename-thumb-page.spec.ts new file mode 100644 index 00000000..6f4742dc --- /dev/null +++ b/e2e/inline-edit/rename-thumb-page.spec.ts @@ -0,0 +1,34 @@ +import { test, expect } from '@playwright/test'; + +test('rename-thumb-page-through-direct-edit-and-context-menu', async ({ + page, +}) => { + await page.goto(''); + + await page.getByText('Pages').click(); + const thumb = page.getByText('Page 1', { exact: true }); + await expect(thumb).toBeVisible(); + await expect(thumb).toHaveText('Page 1'); + + await thumb.dblclick(); + const input = page.getByRole('textbox'); + const newTextContent = 'Change the name'; + await input.fill(newTextContent); + await page.keyboard.press('Enter'); + const renamedPage = page.getByText(newTextContent, { exact: true }); + await expect(renamedPage).toBeVisible(); + + const siblingElement = page.getByText('Change the name', { exact: true }); + const divThumb = siblingElement.locator('..'); + await divThumb.click({ button: 'right' }); + await page.getByText('Rename').click(); + + const secondNewTextContent = 'Other name'; + await expect(input).toBeVisible(); + await input.fill(secondNewTextContent); + await page.keyboard.press('Enter'); + const secondRenamedPage = page.getByText(secondNewTextContent, { + exact: true, + }); + await expect(secondRenamedPage).toBeVisible(); +}); diff --git a/e2e/inline-edit/visible-thumb-page.spec.ts b/e2e/inline-edit/visible-thumb-page.spec.ts new file mode 100644 index 00000000..7d59d605 --- /dev/null +++ b/e2e/inline-edit/visible-thumb-page.spec.ts @@ -0,0 +1,19 @@ +import { test, expect } from '@playwright/test'; + +test('verifies visibility of thumb page, chevron and add new page button', async ({ + page, +}) => { + await page.goto(''); + + await page.getByText('Pages').click(); + + const siblingElement = page.getByText('Page 1', { exact: true }); + const thumb = siblingElement.locator('..'); + await expect(thumb).toBeVisible(); + + const svgElement = thumb.locator('span > svg'); + await expect(svgElement).toBeVisible(); + + const addButton = page.getByRole('button', { name: 'add new page' }); + await expect(addButton).toBeVisible(); +}); diff --git a/index.html b/index.html index 7d823a70..cb8aa063 100644 --- a/index.html +++ b/index.html @@ -1,342 +1,341 @@ - - - - - - - Quickmock - - - - - - - - - - - + - @media screen and (min-width: 1345px) { - .back { - max-height: 1100px; - padding: 0; - } - .github { - position: absolute; - top: min(7vw, 150px); - right: 1vw; - width: 90px; - padding: 24px; - } - .video { - width: 100%; - } - .right { - align-self: end; - padding: 10px; - } - .left { - padding: 10px; - padding-top: 3vw; - align-self: start; - } - .title { - top: 2vw; - width: 60%; - max-width: 900px; - margin-right: auto; - padding-left: 1.5vw; - } - - .content { - grid-template-columns: 1fr 2fr 1fr; - text-align: left; - line-height: 1.3; - padding: 0 56px; - gap: 24px; - max-width: inherit; - margin-top: -3vw; - } - } - @media screen and (max-device-width: 1090px) { - .hide-mobile { - display: none; - } - .mobile-only { - display: block; - } - } - - - -
-
- - - - -
-

- Quickmock, free open source low fidelity wireframes -

+ +
+
+ + + + +
+

+ Quickmock, free open source low fidelity wireframes +

+
+
+
+

+ Share your ideas as low-fidelity mocks in minutes, perfect for + getting started. +

+

Community preview

-
-
-

- Share your ideas as low-fidelity mocks in minutes, perfect for - getting started. -

-

Community preview

-
-
-
- -
-
-
-

- This free, open-source wireframing tool helps you quickly create - basic wireframes for your projects. It's simple to use, making it - easy to sketch out ideas and organize your thoughts without the - need for complex software. -

+
+
+
- START DESIGN -

Now, only available on desktop.

+
+

+ This free, open-source wireframing tool helps you quickly create + basic wireframes for your projects. It's simple to use, making it + easy to sketch out ideas and organize your thoughts without the + need for complex software. +

+
-
- - + START DESIGN +

Now, only available on desktop.

+
+
+ + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index a431b73f..d5cee5bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@fontsource-variable/montserrat": "^5.0.20", "@fontsource/balsamiq-sans": "^5.0.21", "@types/lodash.clonedeep": "^4.5.9", + "@uiw/react-color-chrome": "^2.3.2", "immer": "^10.1.1", "konva": "^9.3.12", "lodash.clonedeep": "^4.5.0", @@ -1997,6 +1998,195 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@uiw/color-convert": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@uiw/color-convert/-/color-convert-2.3.2.tgz", + "integrity": "sha512-Txs0oAcOGhvM15yi7NqDJSws6htpuGx75EblFlZmh4h4AyUYXaeN2HNcOAUt835M3SN1j7rqMC+XERIE4r776Q==", + "license": "MIT", + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0" + } + }, + "node_modules/@uiw/react-color-alpha": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@uiw/react-color-alpha/-/react-color-alpha-2.3.2.tgz", + "integrity": "sha512-+yh+KEpNKjxNFFODQrB3Lki2hu6kznoSCngHgptlWBUtAC3e/e7tIiTTedSpCGr7fwIpC0CWrKwxENA3tyY/2Q==", + "license": "MIT", + "dependencies": { + "@uiw/color-convert": "2.3.2", + "@uiw/react-drag-event-interactive": "2.3.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@uiw/react-color-chrome": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@uiw/react-color-chrome/-/react-color-chrome-2.3.2.tgz", + "integrity": "sha512-WvA8dg2y+vgoyy8GFBM3B1+SeJ29ov5OVEei1kACMKxThADPdI4w3RRmhYIMnSeFGVW3bGuBMq6JimjIKZirCQ==", + "license": "MIT", + "dependencies": { + "@uiw/color-convert": "2.3.2", + "@uiw/react-color-alpha": "2.3.2", + "@uiw/react-color-editable-input": "2.3.2", + "@uiw/react-color-editable-input-hsla": "2.3.2", + "@uiw/react-color-editable-input-rgba": "2.3.2", + "@uiw/react-color-github": "2.3.2", + "@uiw/react-color-hue": "2.3.2", + "@uiw/react-color-saturation": "2.3.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@uiw/react-color-editable-input": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@uiw/react-color-editable-input/-/react-color-editable-input-2.3.2.tgz", + "integrity": "sha512-DDl9pCN7hH5Q+OB6LiFGFmkqIQw81EDIEvDi6rr6G9U+k+oqZ9JCBKSZ9qNKSa4MqnuISOkFv0vL3Jh8fSyqcg==", + "license": "MIT", + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@uiw/react-color-editable-input-hsla": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@uiw/react-color-editable-input-hsla/-/react-color-editable-input-hsla-2.3.2.tgz", + "integrity": "sha512-lLO8K+Zv5L9HKBgM3zYSqeLKisBkpXCxlQmF8iCKYcIgeRilM26qqylskA4n6pVixfSooL0hyL/HwfNmbCIrrg==", + "license": "MIT", + "dependencies": { + "@uiw/color-convert": "2.3.2", + "@uiw/react-color-editable-input-rgba": "2.3.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@uiw/react-color-editable-input-rgba": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@uiw/react-color-editable-input-rgba/-/react-color-editable-input-rgba-2.3.2.tgz", + "integrity": "sha512-HV0+5zzpaNG5v6EyVgmPfInd9UzYknQI7gdsVmmXKzB13L3RFhiv77r6o+q3IZMEnoDZ3U92uX4FeRaM1NrwYw==", + "license": "MIT", + "dependencies": { + "@uiw/color-convert": "2.3.2", + "@uiw/react-color-editable-input": "2.3.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@uiw/react-color-github": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@uiw/react-color-github/-/react-color-github-2.3.2.tgz", + "integrity": "sha512-3QxpEOKYXbbV/L1cYsf7nhoOnl19/zWTpRRgda8LOe3SuRhFrFM3ZLa+UJUEXgO1cg9h64gxSKINh2st/FawpQ==", + "license": "MIT", + "dependencies": { + "@uiw/color-convert": "2.3.2", + "@uiw/react-color-swatch": "2.3.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@uiw/react-color-hue": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@uiw/react-color-hue/-/react-color-hue-2.3.2.tgz", + "integrity": "sha512-aAveo++GAghw09Ngc8Zzwxhj9mGaJfw8q40fDGFrVNxdrwrAjySIKHzlOSg5kw6WnEp4tUjhkMXDfCZWUhqmPA==", + "license": "MIT", + "dependencies": { + "@uiw/color-convert": "2.3.2", + "@uiw/react-color-alpha": "2.3.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@uiw/react-color-saturation": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@uiw/react-color-saturation/-/react-color-saturation-2.3.2.tgz", + "integrity": "sha512-aDKMhjylBUb4dH4oTQYz+U4mhpOebbQ2J0Y8y5aX1tfZ3fZuBqnXtWzu7Ffj3ksdKwkQHPddxT5IDTbAChF/og==", + "license": "MIT", + "dependencies": { + "@uiw/color-convert": "2.3.2", + "@uiw/react-drag-event-interactive": "2.3.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@uiw/react-color-swatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@uiw/react-color-swatch/-/react-color-swatch-2.3.2.tgz", + "integrity": "sha512-AjkEcSdlpxiFm9yull4WDujuHr0tD9/+XkLtcus/MH768zSQbb+rj6cY1nZ8L8FI6LRDGRaVJqFaXm4ZOAaIZw==", + "license": "MIT", + "dependencies": { + "@uiw/color-convert": "2.3.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@uiw/react-drag-event-interactive": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@uiw/react-drag-event-interactive/-/react-drag-event-interactive-2.3.2.tgz", + "integrity": "sha512-lG5pJCtqbYBv7Dj0r12PE9q9yg7P2CzlQodw5ZHPY9GCSZVXHJc0g4lGvCbe/4Y8HYqM8aU4CYS8LplpX+mIQw==", + "license": "MIT", + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.19.0", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -2495,10 +2685,11 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", diff --git a/package.json b/package.json index bf53e95f..74a96792 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@fontsource-variable/montserrat": "^5.0.20", "@fontsource/balsamiq-sans": "^5.0.21", "@types/lodash.clonedeep": "^4.5.9", + "@uiw/react-color-chrome": "^2.3.2", "immer": "^10.1.1", "konva": "^9.3.12", "lodash.clonedeep": "^4.5.0", diff --git a/src/common/components/mock-components/front-basic-shapes/horizontal-line-basic-shape.tsx b/src/common/components/mock-components/front-basic-shapes/horizontal-line-basic-shape.tsx index 35b3e729..60cc2e38 100644 --- a/src/common/components/mock-components/front-basic-shapes/horizontal-line-basic-shape.tsx +++ b/src/common/components/mock-components/front-basic-shapes/horizontal-line-basic-shape.tsx @@ -8,7 +8,7 @@ import { BASIC_SHAPE } from '../front-components/shape.const'; import { useGroupShapeProps } from '../mock-components.utils'; const horizontalLineShapeRestrictions: ShapeSizeRestrictions = { - minWidth: 50, + minWidth: 30, minHeight: 10, maxWidth: -1, maxHeight: 10, diff --git a/src/common/components/mock-components/front-basic-shapes/large-arrow-shape.tsx b/src/common/components/mock-components/front-basic-shapes/large-arrow-shape.tsx index 0cf8123f..3a062e6f 100644 --- a/src/common/components/mock-components/front-basic-shapes/large-arrow-shape.tsx +++ b/src/common/components/mock-components/front-basic-shapes/large-arrow-shape.tsx @@ -8,7 +8,7 @@ import { BASIC_SHAPE } from '../front-components/shape.const'; import { useGroupShapeProps } from '../mock-components.utils'; const LargeArrowShapeSizeRestrictions: ShapeSizeRestrictions = { - minWidth: 50, + minWidth: 30, minHeight: 50, maxWidth: -1, maxHeight: -1, diff --git a/src/common/components/mock-components/front-basic-shapes/vertical-line-basic-shape.tsx b/src/common/components/mock-components/front-basic-shapes/vertical-line-basic-shape.tsx index 5cdf01e0..a6e1d619 100644 --- a/src/common/components/mock-components/front-basic-shapes/vertical-line-basic-shape.tsx +++ b/src/common/components/mock-components/front-basic-shapes/vertical-line-basic-shape.tsx @@ -9,7 +9,7 @@ import { useGroupShapeProps } from '../mock-components.utils'; const verticalLineShapeRestrictions: ShapeSizeRestrictions = { minWidth: 10, - minHeight: 50, + minHeight: 30, maxWidth: 10, maxHeight: -1, defaultWidth: 10, diff --git a/src/common/components/mock-components/front-text-components/front-text-hooks/resize-fontsize-change.hook.ts b/src/common/components/mock-components/front-text-components/front-text-hooks/resize-fontsize-change.hook.ts index a629b2f3..6a99c601 100644 --- a/src/common/components/mock-components/front-text-components/front-text-hooks/resize-fontsize-change.hook.ts +++ b/src/common/components/mock-components/front-text-components/front-text-hooks/resize-fontsize-change.hook.ts @@ -32,7 +32,7 @@ export const useResizeOnFontSizeChange = ( id, coords, { - width: width * 1.15, + width: width * 1.25, height: multiline ? _calcParagraphTotalHeight(height, 0.8, paragraphLines.length) : height, diff --git a/src/common/components/mock-components/front-text-components/paragraph-text-shape.tsx b/src/common/components/mock-components/front-text-components/paragraph-text-shape.tsx index a987e4f4..48ee655b 100644 --- a/src/common/components/mock-components/front-text-components/paragraph-text-shape.tsx +++ b/src/common/components/mock-components/front-text-components/paragraph-text-shape.tsx @@ -10,7 +10,7 @@ import { useResizeOnFontSizeChange } from './front-text-hooks/resize-fontsize-ch const paragraphSizeRestrictions: ShapeSizeRestrictions = { minWidth: 200, - minHeight: 70, + minHeight: 20, maxWidth: -1, maxHeight: -1, defaultWidth: 420, diff --git a/src/pods/properties/components/color-picker/color-picker.component.module.css b/src/pods/properties/components/color-picker/color-picker.component.module.css index 8e51ebc3..91d34b7f 100644 --- a/src/pods/properties/components/color-picker/color-picker.component.module.css +++ b/src/pods/properties/components/color-picker/color-picker.component.module.css @@ -10,8 +10,9 @@ flex: 1; } -.button { - border: none; +.button, +.colorBox { + border: 1px solid var(--primary-100); color: var(--text-color); background-color: inherit; width: var(--space-lg); @@ -23,10 +24,69 @@ align-items: center; justify-content: center; gap: var(--space-s); - transition: all 0.3s ease-in-out; + transition: all 0.2s ease-in-out; cursor: pointer; + padding: 0; } -.button:hover { +.button:hover, +.colorBox:hover { background-color: var(--primary-100); + border-color: var(--primary-600); +} + +.popover { + position: absolute; + z-index: 2; + right: 9px; +} +.cover { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; +} + +.chromeContainer { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + background-color: white; + border: 1px solid #ccc; + border-radius: var(--border-radius-unit); + box-shadow: 0px 8px 15px rgba(0, 0, 0, 0.1); + padding-bottom: 5px; + width: 232px; +} + +.colorPalette { + border-top: 1px solid var(--primary-100); + padding-top: 5px; + display: flex; + flex-wrap: wrap; + justify-content: center; + width: 229px; + gap: var(--space-xs); +} + +.colorBox:first-child { + background: linear-gradient( + 135deg, + transparent 47%, + red 10%, + transparent 53% + ); +} +.button[data-color='noColor']::after { + content: ''; + width: 100%; + height: 100%; + background: linear-gradient( + 135deg, + transparent 47%, + red 10%, + transparent 53% + ); } diff --git a/src/pods/properties/components/color-picker/color-picker.component.tsx b/src/pods/properties/components/color-picker/color-picker.component.tsx index 2a4bb591..92af5d54 100644 --- a/src/pods/properties/components/color-picker/color-picker.component.tsx +++ b/src/pods/properties/components/color-picker/color-picker.component.tsx @@ -1,3 +1,8 @@ +import { useState } from 'react'; +import Chrome from '@uiw/react-color-chrome'; +import { hexToHsva, HsvaColor } from '@uiw/color-convert'; +import { GithubPlacement } from '@uiw/react-color-github'; +import { PRESET_COLORS } from './color-picker.const'; import classes from './color-picker.component.module.css'; interface Props { @@ -6,18 +11,66 @@ interface Props { onChange: (color: string) => void; } +interface ColorProps { + hsva: HsvaColor; + hexa: string; +} + export const ColorPicker: React.FC = props => { const { label, color, onChange } = props; + const [picker, setPicker] = useState(false); + const [hsva, setHsva] = useState(() => hexToHsva(color)); + + const togglePicker = () => { + setPicker(!picker); + setHsva(hexToHsva(color)); + }; + + const handleChange = (color: ColorProps) => { + setHsva(color.hsva); + onChange(color.hexa); + }; + + const handlePresetColors = (newColor: string) => { + const hsvaColor = hexToHsva(newColor); + setHsva(hsvaColor); + onChange(newColor); + }; return ( -
-

{label}

- onChange(e.target.value)} - className={classes.button} - /> -
+ <> +
+

{label}

+ +
+ {picker && ( +
+
+
+ +
+ {PRESET_COLORS.map(presetColor => ( +
handlePresetColors(presetColor)} + >
+ ))} +
+
+
+ )} + ); }; diff --git a/src/pods/properties/components/color-picker/color-picker.const.ts b/src/pods/properties/components/color-picker/color-picker.const.ts new file mode 100644 index 00000000..5c3d0d65 --- /dev/null +++ b/src/pods/properties/components/color-picker/color-picker.const.ts @@ -0,0 +1,18 @@ +export const PRESET_COLORS = [ + '#00000000', // need to be always first. + '#DD0000', + '#0022BB', + '#0088FF', + '#55EEFF', + '#006400', + '#00BB00', + '#99FF66', + '#FFFF00', + '#FF9900', + '#CC55FF', + '#FF88AA', + '#994400', + '#000000', + '#999999', + '#FFFFFF', +]; diff --git a/src/pods/toolbar/components/export-button/export-button.tsx b/src/pods/toolbar/components/export-button/export-button.tsx index 698ffcc5..adf00839 100644 --- a/src/pods/toolbar/components/export-button/export-button.tsx +++ b/src/pods/toolbar/components/export-button/export-button.tsx @@ -1,40 +1,16 @@ import { ExportIcon } from '@/common/components/icons/export-icon.component'; import { useCanvasContext } from '@/core/providers'; import classes from '@/pods/toolbar/toolbar.pod.module.css'; -import { Stage } from 'konva/lib/Stage'; -import { calculateCanvasBounds } from './export-button.utils'; +import { + calculateCanvasBounds, + buildExportFileName, + createDownloadLink, +} from './export-button.utils'; import { ToolbarButton } from '../toolbar-button'; import Konva from 'konva'; export const ExportButton = () => { - const { stageRef, shapes } = useCanvasContext(); - - const createDownloadLink = (dataURL: string) => { - const link = document.createElement('a'); - link.href = dataURL; - link.download = 'canvas.png'; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - }; - - const resetScale = (stage: Stage) => { - stage.scale({ x: 1, y: 1 }); - }; - - const applyFiltersToImages = (stage: Stage) => { - stage.find('Image').forEach(node => { - if (node.filters()?.includes(Konva.Filters.Grayscale)) { - node.cache({ - x: 0, - y: 0, - width: node.width(), - height: node.height(), - pixelRatio: 2, - }); - } - }); - }; + const { stageRef, shapes, fileName, getActivePageName } = useCanvasContext(); const handleExport = () => { if (stageRef.current) { @@ -46,7 +22,7 @@ export const ExportButton = () => { const bounds = calculateCanvasBounds(shapes); const dataURL = clonedStage.toDataURL({ - mimeType: 'image/png', // Swap to 'image/jpeg' if necessary + mimeType: 'image/png', x: bounds.x, y: bounds.y, width: bounds.width, @@ -54,10 +30,29 @@ export const ExportButton = () => { pixelRatio: 2, }); - createDownloadLink(dataURL); + const exportFileName = buildExportFileName(fileName, getActivePageName()); + createDownloadLink(dataURL, exportFileName); } }; + const resetScale = (stage: Konva.Stage) => { + stage.scale({ x: 1, y: 1 }); + }; + + const applyFiltersToImages = (stage: Konva.Stage) => { + stage.find('Image').forEach(node => { + if (node.filters()?.includes(Konva.Filters.Grayscale)) { + node.cache({ + x: 0, + y: 0, + width: node.width(), + height: node.height(), + pixelRatio: 2, + }); + } + }); + }; + return ( { return canvasBounds; }; + +export const buildExportFileName = ( + fileName: string, + activePageName: string +): string => { + // Remove extension and replace spaces with dashes from file name + let baseFileName = + fileName === '' + ? 'new-document' + : fileName.substring(0, fileName.lastIndexOf('.')) || fileName; + baseFileName = baseFileName.trim().replace(/\s+/g, '-'); + + // Get the active page name and replace spaces with dashes + const pageName = activePageName + .toLocaleLowerCase() + .trim() + .replace(/\s+/g, '-'); + + return `${baseFileName}-${pageName}.png`; // Add extension jpg also available +}; + +export const createDownloadLink = (dataURL: string, fileName: string) => { + const link = document.createElement('a'); + link.href = dataURL; + link.download = fileName; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); +};