diff --git a/src/common/components/mock-components/front-rich-components/tabsbar/business/tabsbar.business.ts b/src/common/components/mock-components/front-rich-components/tabsbar/business/tabsbar.business.ts index ce08f8bb..ed119ee4 100644 --- a/src/common/components/mock-components/front-rich-components/tabsbar/business/tabsbar.business.ts +++ b/src/common/components/mock-components/front-rich-components/tabsbar/business/tabsbar.business.ts @@ -1,6 +1,6 @@ import { Layer } from 'konva/lib/Layer'; import { balanceSpacePerItem } from './balance-space'; -import { calcTextWidth } from './calc-text-width'; +import { calcTextDimensions } from '@/common/utils/calc-text-dimensions'; export const adjustTabWidths = (args: { tabs: string[]; @@ -35,9 +35,14 @@ export const adjustTabWidths = (args: { } const arrangeTabsInfo = tabs.reduce( (acc: OriginalTabInfo[], tab, index): OriginalTabInfo[] => { - const tabFullTextWidth = - calcTextWidth(tab, font.fontSize, font.fontFamily, konvaLayer) + - totalInnerXPadding; + const { width: textWidth } = calcTextDimensions( + tab, + font.fontSize, + font.fontFamily, + konvaLayer + ); + + const tabFullTextWidth = textWidth + totalInnerXPadding; const desiredWidth = Math.max(totalMinTabSpace, tabFullTextWidth); return [ ...acc, 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 new file mode 100644 index 00000000..a629b2f3 --- /dev/null +++ b/src/common/components/mock-components/front-text-components/front-text-hooks/resize-fontsize-change.hook.ts @@ -0,0 +1,63 @@ +import { calcTextDimensions } from '@/common/utils/calc-text-dimensions'; +import { useCanvasContext } from '@/core/providers'; +import { useEffect, useRef } from 'react'; + +export const useResizeOnFontSizeChange = ( + id: string, + coords: { x: number; y: number }, + text: string, + fontSize: number, + fontVariant: string, + multiline: boolean = false +) => { + const previousFontSize = useRef(fontSize); + const { updateShapeSizeAndPosition, stageRef } = useCanvasContext(); + const konvaLayer = stageRef.current?.getLayers()[0]; + + useEffect(() => { + if (previousFontSize.current !== fontSize) { + previousFontSize.current = fontSize; + + const paragraphLines = _extractParagraphLines(text); + const longestLine = _findLongestString(paragraphLines); + + const { width, height } = calcTextDimensions( + multiline ? longestLine : text, + fontSize, + fontVariant, + konvaLayer + ); + + updateShapeSizeAndPosition( + id, + coords, + { + width: width * 1.15, + height: multiline + ? _calcParagraphTotalHeight(height, 0.8, paragraphLines.length) + : height, + }, + true + ); + } + }, [fontSize]); +}; + +/* Hook Helper functions */ +function _extractParagraphLines(multilineText: string) { + return multilineText.split(/\r?\n/).filter(line => line.trim() !== ''); +} + +function _findLongestString(stringsArray: string[]): string { + return stringsArray.reduce((longest, current) => + current.length > longest.length ? current : longest + ); +} + +function _calcParagraphTotalHeight( + heightPerLine: number, + lineHeight: number = 1.3, + linesQty: number +) { + return heightPerLine * lineHeight * linesQty; +} diff --git a/src/common/components/mock-components/front-text-components/heading1-text-shape.tsx b/src/common/components/mock-components/front-text-components/heading1-text-shape.tsx index ed5ef8c5..1e76ba40 100644 --- a/src/common/components/mock-components/front-text-components/heading1-text-shape.tsx +++ b/src/common/components/mock-components/front-text-components/heading1-text-shape.tsx @@ -6,6 +6,7 @@ import { fitSizeToShapeSizeRestrictions } from '@/common/utils/shapes/shape-rest import { useShapeProps } from '../../shapes/use-shape-props.hook'; import { BASIC_SHAPE } from '../front-components/shape.const'; import { useGroupShapeProps } from '../mock-components.utils'; +import { useResizeOnFontSizeChange } from './front-text-hooks/resize-fontsize-change.hook'; const heading1SizeRestrictions: ShapeSizeRestrictions = { minWidth: 40, @@ -56,6 +57,8 @@ export const Heading1Shape = forwardRef((props, ref) => { ref ); + useResizeOnFontSizeChange(id, { x, y }, text, fontSize, fontVariant); + return ( ((props, ref) => { ref ); + useResizeOnFontSizeChange(id, { x, y }, text, fontSize, fontVariant); + return ( ((props, ref) => { ref ); + useResizeOnFontSizeChange(id, { x, y }, text, fontSize, fontVariant); + return ( ((props, ref) => { const { width: restrictedWidth, height: restrictedHeight } = restrictedSize; - const { textColor, textDecoration, fontSize, textAlignment } = useShapeProps( - otherProps, - BASIC_SHAPE - ); + const { textColor, textDecoration, fontSize, textAlignment, fontVariant } = + useShapeProps(otherProps, BASIC_SHAPE); const commonGroupProps = useGroupShapeProps( props, @@ -53,6 +52,8 @@ export const LinkShape = forwardRef((props, ref) => { ref ); + useResizeOnFontSizeChange(id, { x, y }, text, fontSize, fontVariant); + return ( ((props, ref) => { ref ); + useResizeOnFontSizeChange(id, { x, y }, text, fontSize, fontVariant); + return ( ((props, ref) => { ); const { width: restrictedWidth, height: restrictedHeight } = restrictedSize; - const { textColor, fontSize, textAlignment } = useShapeProps( + const { textColor, fontSize, textAlignment, fontVariant } = useShapeProps( otherProps, BASIC_SHAPE ); @@ -52,6 +53,8 @@ export const ParagraphShape = forwardRef((props, ref) => { ref ); + useResizeOnFontSizeChange(id, { x, y }, text, fontSize, fontVariant, true); + return ( ((props, ref) => { ref ); + useResizeOnFontSizeChange(id, { x, y }, text, fontSize, fontVariant); + return ( { const context = konvaLayer.getContext(); context.font = `${fontSize}px ${fontfamily}`; - return context.measureText(text).width; + const { width, fontBoundingBoxAscent, fontBoundingBoxDescent } = + context.measureText(text); + const totalHeight = fontBoundingBoxAscent + fontBoundingBoxDescent; + return { width, height: totalHeight }; }; const _getTextCreatingNewCanvas = ( @@ -43,8 +46,13 @@ const _getTextCreatingNewCanvas = ( const context = canvas.getContext('2d'); if (context) { context.font = `${fontSize}px ${fontfamily}`; - return context.measureText(text).width; + const { width, fontBoundingBoxAscent, fontBoundingBoxDescent } = + context.measureText(text); + const height = fontBoundingBoxAscent + fontBoundingBoxDescent; + return { width, height }; } const charAverageWidth = fontSize * 0.7; - return text.length * charAverageWidth + charAverageWidth * 0.8; + const width = text.length * charAverageWidth + charAverageWidth * 0.8; + const height = fontSize * 1.5; + return { width, height }; }; diff --git a/src/core/local-disk/use-local-disk.hook.ts b/src/core/local-disk/use-local-disk.hook.ts index fbcb3f18..b5acf272 100644 --- a/src/core/local-disk/use-local-disk.hook.ts +++ b/src/core/local-disk/use-local-disk.hook.ts @@ -58,6 +58,7 @@ export const useLocalDisk = () => { reader.onload = () => { const content = reader.result as string; const parseData: QuickMockFileContract = JSON.parse(content); + setFileName(file.name); if (parseData.version === '0.1') { // Handle version 0.1 parsing const appDocument = diff --git a/src/core/providers/canvas/canvas.model.ts b/src/core/providers/canvas/canvas.model.ts index e0e2e503..45d6274e 100644 --- a/src/core/providers/canvas/canvas.model.ts +++ b/src/core/providers/canvas/canvas.model.ts @@ -98,6 +98,7 @@ export interface CanvasContextModel { addNewPage: () => void; duplicatePage: (pageIndex: number) => void; getActivePage: () => Page; + getActivePageName: () => string; setActivePage: (pageId: string) => void; deletePage: (pageIndex: number) => void; editPageTitle: (pageIndex: number, newName: string) => void; diff --git a/src/core/providers/canvas/canvas.provider.tsx b/src/core/providers/canvas/canvas.provider.tsx index dea96574..e6a0ba56 100644 --- a/src/core/providers/canvas/canvas.provider.tsx +++ b/src/core/providers/canvas/canvas.provider.tsx @@ -101,6 +101,10 @@ export const CanvasProvider: React.FC = props => { return document.pages[document.activePageIndex]; }; + const getActivePageName = () => { + return document.pages[document.activePageIndex].name; + }; + const setActivePage = (pageId: string) => { selectionInfo.clearSelection(); selectionInfo.shapeRefs.current = {}; @@ -179,6 +183,7 @@ export const CanvasProvider: React.FC = props => { const createNewFullDocument = () => { setDocument(createDefaultDocumentModel()); + setFileName(''); }; const deleteSelectedShapes = () => { @@ -318,6 +323,7 @@ export const CanvasProvider: React.FC = props => { addNewPage, duplicatePage, getActivePage, + getActivePageName, setActivePage, deletePage, editPageTitle, diff --git a/src/pods/footer/footer.pod.module.css b/src/pods/footer/footer.pod.module.css index 2754dafe..7edbac1a 100644 --- a/src/pods/footer/footer.pod.module.css +++ b/src/pods/footer/footer.pod.module.css @@ -1,14 +1,29 @@ .container { display: flex; - justify-content: center; + justify-content: space-between; align-items: center; background-color: var(--primary-50); border-top: 1px solid var(--primary-100); padding: var(--space-xs) var(--space-md); } -.title { - flex-grow: 1; +.left, +.center, +.right { + flex: 1; +} + +.left { + text-align: left; +} + +.center { + text-align: center; +} + +.right { + display: flex; + justify-content: flex-end; } .zoomContainer { diff --git a/src/pods/footer/footer.pod.tsx b/src/pods/footer/footer.pod.tsx index 124f2921..b4aa0b96 100644 --- a/src/pods/footer/footer.pod.tsx +++ b/src/pods/footer/footer.pod.tsx @@ -3,23 +3,33 @@ import classes from './footer.pod.module.css'; import { ZoomInButton, ZoomOutButton } from './components'; export const FooterPod = () => { - const { scale, setScale } = useCanvasContext(); + const { scale, setScale, getActivePageName, fileName } = useCanvasContext(); return (
-

Quickmock - © Lemoncode

-
- -

{(scale * 100).toFixed(0)} %

- +
+

+ 📄 {fileName == '' ? 'New' : fileName} -{' '} + {getActivePageName()} +

+
+
+

Quickmock - © Lemoncode

+
+
+
+ +

{(scale * 100).toFixed(0)} %

+ +
); diff --git a/src/pods/properties/components/image-black-and-white/image-black-and-white-selector.component.tsx b/src/pods/properties/components/image-black-and-white/image-black-and-white-selector.component.tsx index dc3e1ba1..1bb4c8d4 100644 --- a/src/pods/properties/components/image-black-and-white/image-black-and-white-selector.component.tsx +++ b/src/pods/properties/components/image-black-and-white/image-black-and-white-selector.component.tsx @@ -14,6 +14,7 @@ export const ImageBlackAndWhite: React.FC = props => {

{label}

onChange(!imageBlackAndWhite)} className={classes.checkbox} /> diff --git a/src/pods/toolbar/components/export-button/export-button.tsx b/src/pods/toolbar/components/export-button/export-button.tsx index 668f9f34..698ffcc5 100644 --- a/src/pods/toolbar/components/export-button/export-button.tsx +++ b/src/pods/toolbar/components/export-button/export-button.tsx @@ -4,6 +4,7 @@ import classes from '@/pods/toolbar/toolbar.pod.module.css'; import { Stage } from 'konva/lib/Stage'; import { calculateCanvasBounds } from './export-button.utils'; import { ToolbarButton } from '../toolbar-button'; +import Konva from 'konva'; export const ExportButton = () => { const { stageRef, shapes } = useCanvasContext(); @@ -21,20 +22,38 @@ export const ExportButton = () => { 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 handleExport = () => { if (stageRef.current) { const originalStage = stageRef.current; const clonedStage = originalStage.clone(); + + applyFiltersToImages(clonedStage); resetScale(clonedStage); + const bounds = calculateCanvasBounds(shapes); const dataURL = clonedStage.toDataURL({ - mimeType: 'image/png', // Change to jpeg to download as jpeg + mimeType: 'image/png', // Swap to 'image/jpeg' if necessary x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height, pixelRatio: 2, }); + createDownloadLink(dataURL); } };