diff --git a/README.md b/README.md index 606d08ca..9e695474 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,17 @@ Team members participating in this project Pablo Reinaldo + + + Alberto Escribano + + + Jorge Miranda de la quintana + + + Josemi Toribio + + Gabi Birsan diff --git a/e2e/helpers/konva-testing.helpers.ts b/e2e/helpers/konva-testing.helpers.ts index 519df69e..fc747d50 100644 --- a/e2e/helpers/konva-testing.helpers.ts +++ b/e2e/helpers/konva-testing.helpers.ts @@ -3,6 +3,7 @@ import { Layer } from 'konva/lib/Layer'; import { Shape } from 'konva/lib/Shape'; import { Group } from 'konva/lib/Group'; import { E2E_CanvasItemKeyAttrs } from './types/e2e-types'; +import { getCanvasBoundingBox } from './position.helpers'; const getLayer = async (page: Page): Promise => await page.evaluate(() => { @@ -77,7 +78,8 @@ export const clickOnCanvasItem = async ( item: E2E_CanvasItemKeyAttrs ) => { const { x, y } = item; - const canvasWindowPos = await page.locator('canvas').boundingBox(); + const stageCanvas = await page.locator('#konva-stage canvas').first(); + const canvasWindowPos = await stageCanvas.boundingBox(); if (!canvasWindowPos) throw new Error('Canvas is not loaded on ui'); await page.mouse.move( canvasWindowPos?.x + x + 20, @@ -90,6 +92,19 @@ export const clickOnCanvasItem = async ( return item; }; +export const dbClickOnCanvasItem = async ( + page: Page, + item: E2E_CanvasItemKeyAttrs +) => { + const { x, y } = item; + const canvasWindowPos = await getCanvasBoundingBox(page); + await page.mouse.dblclick( + canvasWindowPos?.x + x + 20, + canvasWindowPos?.y + y + 20 + ); + return item; +}; + export const ctrlClickOverCanvasItems = async ( page: Page, itemList: E2E_CanvasItemKeyAttrs[] diff --git a/e2e/helpers/position.helpers.ts b/e2e/helpers/position.helpers.ts index c0e85df5..c29f19f1 100644 --- a/e2e/helpers/position.helpers.ts +++ b/e2e/helpers/position.helpers.ts @@ -9,6 +9,7 @@ interface Position { export const getLocatorPosition = async ( locator: Locator ): Promise => { + await locator.scrollIntoViewIfNeeded(); const box = (await locator.boundingBox()) || { x: 0, y: 0, @@ -18,6 +19,17 @@ export const getLocatorPosition = async ( return { x: box.x + box.width / 2, y: box.y + box.height / 2 }; }; +export const getCanvasBoundingBox = async (page: Page) => { + const canvasWindowPos = await page + .locator('#konva-stage canvas') + .boundingBox(); + if (canvasWindowPos) { + return canvasWindowPos; + } else { + throw new Error('Canvas is not loaded on ui'); + } +}; + export const dragAndDrop = async ( page: Page, aPosition: Position, @@ -33,7 +45,8 @@ export const addComponentsToCanvas = async ( page: Page, components: string[] ) => { - const canvasPosition = await page.locator('canvas').boundingBox(); + const stageCanvas = await page.locator('#konva-stage canvas').first(); + const canvasPosition = await stageCanvas.boundingBox(); if (!canvasPosition) throw new Error('No canvas found'); for await (const [index, c] of components.entries()) { diff --git a/e2e/helpers/ui-buttons.helpers.ts b/e2e/helpers/ui-buttons.helpers.ts new file mode 100644 index 00000000..9dad6572 --- /dev/null +++ b/e2e/helpers/ui-buttons.helpers.ts @@ -0,0 +1,13 @@ +import { Page } from '@playwright/test'; + +export const clickUndoUiButton = async (page: Page) => + await page.getByRole('button', { name: 'Undo' }).click(); + +export const clickRedoUiButton = async (page: Page) => + await page.getByRole('button', { name: 'Redo' }).click(); + +export const clickCopyUiButton = async (page: Page) => + await page.getByRole('button', { name: 'Copy' }).click(); + +export const clickPasteUiButton = async (page: Page) => + await page.getByRole('button', { name: 'Paste' }).click(); diff --git a/e2e/inline-edit/multiple-line-inline-edit.spec.ts b/e2e/inline-edit/multiple-line-inline-edit.spec.ts new file mode 100644 index 00000000..ad85802c --- /dev/null +++ b/e2e/inline-edit/multiple-line-inline-edit.spec.ts @@ -0,0 +1,121 @@ +import { test, expect } from '@playwright/test'; +import { Group } from 'konva/lib/Group'; +import { dragAndDrop, getByShapeType, getLocatorPosition } from '../helpers'; + +test('can add textarea to canvas, edit content, and verify shape text', async ({ + page, +}) => { + await page.goto(''); + const component = page.getByAltText('Textarea'); + await component.scrollIntoViewIfNeeded(); + + const position = await getLocatorPosition(component); + const targetPosition = { + x: position.x + 500, + y: position.y - 240, + }; + await dragAndDrop(page, position, targetPosition); + await page.mouse.dblclick(targetPosition.x, targetPosition.y + 40); + const textarea = page.getByRole('textbox').first(); + const textareaContent = await textarea.inputValue(); + expect(textareaContent).toEqual('Your text here...'); + + const textContent = 'Hello'; + await textarea.fill(textContent); + await page.mouse.click(800, 130); + const textareaShape = (await getByShapeType(page, 'textarea')) as Group; + + expect(textareaShape).toBeDefined(); + const textShape = textareaShape.children.find( + child => child.attrs.text === textContent + ); + expect(textShape).toBeDefined(); +}); + +test('cancels textarea edit on Escape and verifies original shape text', async ({ + page, +}) => { + await page.goto(''); + const component = page.getByAltText('Textarea'); + await component.scrollIntoViewIfNeeded(); + + const position = await getLocatorPosition(component); + const targetPosition = { + x: position.x + 500, + y: position.y - 240, + }; + await dragAndDrop(page, position, targetPosition); + await page.mouse.dblclick(targetPosition.x, targetPosition.y + 40); + const textarea = page.getByRole('textbox').first(); + + const textContent = 'Hello'; + await textarea.fill(textContent); + await page.keyboard.press('Escape'); + const originalTextContent = 'Your text here...'; + const textareaShape = (await getByShapeType(page, 'textarea')) as Group; + + expect(textareaShape).toBeDefined(); + const textShape = textareaShape.children.find( + child => child.attrs.text === originalTextContent + ); + expect(textShape).toBeDefined(); +}); + +test('can add and edit input, and delete last letter', async ({ page }) => { + await page.goto(''); + const component = page.getByAltText('Textarea'); + await component.scrollIntoViewIfNeeded(); + + const position = await getLocatorPosition(component); + const targetPosition = { + x: position.x + 500, + y: position.y - 240, + }; + await dragAndDrop(page, position, targetPosition); + await page.mouse.dblclick(targetPosition.x, targetPosition.y + 40); + const textarea = page.getByRole('textbox').first(); + + const textContent = 'World'; + await textarea.fill(textContent); + await page.keyboard.press('Backspace'); + const updatedTextareaContent = await textarea.inputValue(); + expect(updatedTextareaContent).toEqual('Worl'); + + await page.mouse.click(800, 130); + + const textareaShape = (await getByShapeType(page, 'textarea')) as Group; + expect(textareaShape).toBeDefined(); + const textShape = textareaShape.children.find( + child => child.attrs.text === 'Worl' + ); + expect(textShape).toBeDefined(); +}); + +test('adds multi-line text to textarea on canvas and verifies shape text', async ({ + page, +}) => { + await page.goto(''); + const component = page.getByAltText('Textarea'); + await component.scrollIntoViewIfNeeded(); + + const position = await getLocatorPosition(component); + const targetPosition = { + x: position.x + 500, + y: position.y - 240, + }; + await dragAndDrop(page, position, targetPosition); + await page.mouse.dblclick(targetPosition.x, targetPosition.y + 40); + const textarea = page.getByRole('textbox').first(); + + const textContent = 'Line 1\nLine 2'; + await textarea.fill(textContent); + + await page.mouse.click(800, 130); + + const textareaShape = (await getByShapeType(page, 'textarea')) as Group; + expect(textareaShape).toBeDefined(); + const textShape = textareaShape.children.find( + child => child.attrs.text === textContent + ); + expect(textShape).toBeDefined(); +}); diff --git a/e2e/selection/shape-selection.spec.ts b/e2e/selection/shape-selection.spec.ts index ad9a7b93..cc352ccc 100644 --- a/e2e/selection/shape-selection.spec.ts +++ b/e2e/selection/shape-selection.spec.ts @@ -46,7 +46,8 @@ test('drop shape in canvas, click on canvas, drop diselected', async ({ const inputShape = (await getByShapeType(page, 'input')) as Group; expect(inputShape).toBeDefined(); - await page.click('canvas'); + //Click Away + await page.mouse.click(800, 130); const transformer = await getTransformer(page); expect(transformer).toBeDefined(); diff --git a/e2e/ui-functionality/copy-paste.spec.ts b/e2e/ui-functionality/copy-paste.spec.ts new file mode 100644 index 00000000..3b085f34 --- /dev/null +++ b/e2e/ui-functionality/copy-paste.spec.ts @@ -0,0 +1,158 @@ +import { test, expect } from '@playwright/test'; +import { + addComponentsToCanvas, + dragAndDrop, + getWithinCanvasItemList, +} from '../helpers'; +import { E2E_CanvasItemKeyAttrs } from '../helpers/types/e2e-types'; +import { + clickCopyUiButton, + clickPasteUiButton, +} from '../helpers/ui-buttons.helpers'; + +test.describe('Copy/Paste functionality tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto(''); + }); + + test('Should copy and paste a single shape using the ToolBar UI buttons', async ({ + page, + }) => { + //Arrange one Input component + const addInputIntoCanvas = ['Input']; + await addComponentsToCanvas(page, addInputIntoCanvas); + + //Copy and assert there are only one component within the canvas + await clickCopyUiButton(page); + const insideCanvasItemList = (await getWithinCanvasItemList( + page + )) as E2E_CanvasItemKeyAttrs[]; + expect(insideCanvasItemList.length).toEqual(1); + + //Paste and assert there are 2 Input Components and that they have different IDs + await clickPasteUiButton(page); + const updatedInsideCanvasItemList = (await getWithinCanvasItemList( + page + )) as E2E_CanvasItemKeyAttrs[]; + const [originalComponent, copiedComponent] = updatedInsideCanvasItemList; + + expect(updatedInsideCanvasItemList.length).toEqual(2); + expect(originalComponent.shapeType).toEqual(copiedComponent.shapeType); + expect(originalComponent['data-id']).not.toEqual( + copiedComponent['data-id'] + ); + }); + + test('Should copy and paste a single shape using keyboard commands', async ({ + page, + }) => { + // NOTE: This test has the same steps as the previous one, except for the keyboard commands. + //Arrange one Input component + const addInputIntoCanvas = ['Input']; + await addComponentsToCanvas(page, addInputIntoCanvas); + + //Copy and assert there are only one component within the canvas + await page.keyboard.press('ControlOrMeta+c'); + const insideCanvasItemList = (await getWithinCanvasItemList( + page + )) as E2E_CanvasItemKeyAttrs[]; + expect(insideCanvasItemList.length).toEqual(1); + + //Paste and assert there are 2 Input Components and that they have different IDs + await page.keyboard.press('ControlOrMeta+v'); + const updatedInsideCanvasItemList = (await getWithinCanvasItemList( + page + )) as E2E_CanvasItemKeyAttrs[]; + const [originalComponent, copiedComponent] = updatedInsideCanvasItemList; + + expect(updatedInsideCanvasItemList.length).toEqual(2); + expect(originalComponent.shapeType).toEqual(copiedComponent.shapeType); + expect(originalComponent['data-id']).not.toEqual( + copiedComponent['data-id'] + ); + }); + + /* + test('Should copy and paste a multiple shapes using the ToolBar UI buttons', async ({ + page, + }) => { + //Add several components into the canvas + const addInputIntoCanvas = ['Input', 'Combobox', 'Icon']; + await addComponentsToCanvas(page, addInputIntoCanvas); + + //Select items by drag and drop + await dragAndDrop(page, { x: 260, y: 130 }, { x: 1000, y: 550 }); + + //Copy and assert there are 3 components within the canvas + await clickCopyUiButton(page); + const insideCanvasItemList = (await getWithinCanvasItemList( + page + )) as E2E_CanvasItemKeyAttrs[]; + const [originalComp_1, originalComp_2, originalComp_3] = + insideCanvasItemList; + expect(insideCanvasItemList.length).toEqual(3); + + //Paste + await clickPasteUiButton(page); + const updatedInsideCanvasItemList = (await getWithinCanvasItemList( + page + )) as E2E_CanvasItemKeyAttrs[]; + const [, , , copiedComp_1, copiedComp_2, copiedComp_3] = + updatedInsideCanvasItemList; + + //Assert there are 6 Components, + expect(updatedInsideCanvasItemList.length).toEqual(6); + + //Assert they match the same shapes respectively + expect(originalComp_1.shapeType).toEqual(copiedComp_1.shapeType); + expect(originalComp_2.shapeType).toEqual(copiedComp_2.shapeType); + expect(originalComp_3.shapeType).toEqual(copiedComp_3.shapeType); + + //Assert they have different IDs + expect(originalComp_1['data-id']).not.toEqual(copiedComp_1['data-id']); + expect(originalComp_2['data-id']).not.toEqual(copiedComp_2['data-id']); + expect(originalComp_3['data-id']).not.toEqual(copiedComp_3['data-id']); + }); +*/ + test('Should copy and paste a multiple shapes using keyboard commands', async ({ + page, + }) => { + // NOTE: This test has the same steps as the previous one, except for the keyboard commands. + //Add several components into the canvas + const addInputIntoCanvas = ['Input', 'Combobox', 'Icon']; + await addComponentsToCanvas(page, addInputIntoCanvas); + + //Select items by drag and drop + await dragAndDrop(page, { x: 260, y: 130 }, { x: 1000, y: 550 }); + + //Copy and assert there are 3 components within the canvas + await page.keyboard.press('ControlOrMeta+c'); + const insideCanvasItemList = (await getWithinCanvasItemList( + page + )) as E2E_CanvasItemKeyAttrs[]; + const [originalComp_1, originalComp_2, originalComp_3] = + insideCanvasItemList; + expect(insideCanvasItemList.length).toEqual(3); + + //Paste + await page.keyboard.press('ControlOrMeta+v'); + const updatedInsideCanvasItemList = (await getWithinCanvasItemList( + page + )) as E2E_CanvasItemKeyAttrs[]; + const [, , , copiedComp_1, copiedComp_2, copiedComp_3] = + updatedInsideCanvasItemList; + + //Assert there are 6 Components, + expect(updatedInsideCanvasItemList.length).toEqual(6); + + //Assert they match the same shapes respectively + expect(originalComp_1.shapeType).toEqual(copiedComp_1.shapeType); + expect(originalComp_2.shapeType).toEqual(copiedComp_2.shapeType); + expect(originalComp_3.shapeType).toEqual(copiedComp_3.shapeType); + + //Assert they have different IDs + expect(originalComp_1['data-id']).not.toEqual(copiedComp_1['data-id']); + expect(originalComp_2['data-id']).not.toEqual(copiedComp_2['data-id']); + expect(originalComp_3['data-id']).not.toEqual(copiedComp_3['data-id']); + }); +}); diff --git a/e2e/ui-functionality/toolbar_undo-redo.spec.ts b/e2e/ui-functionality/toolbar_undo-redo.spec.ts new file mode 100644 index 00000000..c9ad1c68 --- /dev/null +++ b/e2e/ui-functionality/toolbar_undo-redo.spec.ts @@ -0,0 +1,166 @@ +import { test, expect } from '@playwright/test'; +import { + addComponentsToCanvas, + getWithinCanvasItemList, + getByShapeType, + dbClickOnCanvasItem, + getCanvasBoundingBox, + getShapePosition, +} from '../helpers'; +import { E2E_CanvasItemKeyAttrs } from '../helpers/types/e2e-types'; +import { Group } from 'konva/lib/Group'; +import { + clickRedoUiButton, + clickUndoUiButton, +} from '../helpers/ui-buttons.helpers'; + +test.describe('ToolBar buttons Undo/Redo functionality tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto(''); + }); + + test('Should remove and restore a just dragged into canvas-item, respectively', async ({ + page, + }) => { + //Arrange + const addInputIntoCanvas = ['Input']; + await addComponentsToCanvas(page, addInputIntoCanvas); + + //Undo and check within canvas items + await clickUndoUiButton(page); + const insideCanvasItemList = await getWithinCanvasItemList(page); + + expect(insideCanvasItemList.length).toEqual(0); + + //Redo and check existing item within canvas + await clickRedoUiButton(page); + const updatedInsideCanvasItemList = await getWithinCanvasItemList(page); + + expect(updatedInsideCanvasItemList.length).toEqual(1); + }); + + test('Should remove and restore the last item you just dragged into the canvas', async ({ + page, + }) => { + //Arrange + const addComponentsIntoCanvas = ['Input', 'Combobox']; + await addComponentsToCanvas(page, addComponentsIntoCanvas); + + //Undo and assert there is only one Item within canvas + await clickUndoUiButton(page); + const insideCanvasItemList = await getWithinCanvasItemList(page); + + expect(insideCanvasItemList.length).toEqual(1); + + const firsCanvastItem = await getByShapeType(page, 'input'); + expect(firsCanvastItem).toBeDefined(); + + //Redo and assert both items are contained within the canvas + await clickRedoUiButton(page); + const updatedInsideCanvasItemList = await getWithinCanvasItemList(page); + const secondCanvasItem = await getByShapeType(page, 'combobox'); + + expect(updatedInsideCanvasItemList.length).toEqual(2); + expect(firsCanvastItem).toBeDefined(); + expect(secondCanvasItem).toBeDefined(); + }); + + test('Should reverse and restore an edited text of an Input Component', async ({ + page, + }) => { + //Arrange data and drag an input + const addComponentsIntoCanvas = ['Input']; + const defaultInputPlaceholder = 'Placeholder'; + const updatedText = 'Hello'; + + await addComponentsToCanvas(page, addComponentsIntoCanvas); + const [inputOnCanvas] = (await getWithinCanvasItemList( + page + )) as E2E_CanvasItemKeyAttrs[]; + + //Start Input component inline editing + await dbClickOnCanvasItem(page, inputOnCanvas); + const editableInput = page.locator('input[data-is-inline-edition-on=true]'); + const defaultInputValue = await editableInput.inputValue(); + + await editableInput.fill(updatedText); + const updatedInputValue = await editableInput.inputValue(); + + //Undo edit and assert text is reversed + await clickUndoUiButton(page); + expect(defaultInputValue).toEqual(defaultInputPlaceholder); + + //Redo edit and assert that input contains the restored updated text + await clickRedoUiButton(page); + expect(updatedInputValue).toEqual(updatedText); + }); + + test('Should restore the item position to its previous placement', async ({ + page, + }) => { + //Arrange data and drag an input into canvas + const componentToAddintoCanvas = ['Input']; + await addComponentsToCanvas(page, componentToAddintoCanvas); + + const { x: canvasXStart, y: canvasYStart } = + await getCanvasBoundingBox(page); + + const inputElement = (await getByShapeType(page, 'input')) as Group; + + const inputInitialPosition = await getShapePosition(inputElement); + const inputModifiedPosition = { + x: inputInitialPosition.x + canvasXStart + 200, + y: inputInitialPosition.y + canvasYStart, + }; + + //Displace item within the canvas + await page.mouse.down(); + await page.mouse.move(inputModifiedPosition.x, inputModifiedPosition.y); + await page.mouse.up(); + + //Undo and assert that the item is placed in its initial position + await clickUndoUiButton(page); + const finalInputPosition = await getShapePosition(inputElement); + + expect(finalInputPosition).toEqual(inputInitialPosition); + }); + + test('Should undo and redo, backward and forward severals steps consistently', async ({ + page, + }) => { + //Arrange data and drag an items into canvas + const componentsToAddIntoCanvas = ['Input', 'Combobox']; + await addComponentsToCanvas(page, componentsToAddIntoCanvas); + + await page.getByText('Rich Components').click(); + const richComponentsToAddintoCanvas = ['Accordion']; + await addComponentsToCanvas(page, richComponentsToAddintoCanvas); + + //Assert there are 3 items within the canvas + const itemsQtyWithinCanvas_step1 = (await getWithinCanvasItemList(page)) + .length; + + expect(itemsQtyWithinCanvas_step1).toEqual(3); + + //x3 undo + await clickUndoUiButton(page); + await clickUndoUiButton(page); + await clickUndoUiButton(page); + + //Assert there are no items within the canvas + const itemsQtyWithinCanvas_step2 = (await getWithinCanvasItemList(page)) + .length; + + expect(itemsQtyWithinCanvas_step2).toEqual(0); + + //x3 redo + await clickRedoUiButton(page); + await clickRedoUiButton(page); + await clickRedoUiButton(page); + + //Assert there are again 3 items within the canvas + const itemsQtyWithinCanvas_step3 = (await getWithinCanvasItemList(page)) + .length; + expect(itemsQtyWithinCanvas_step3).toEqual(3); + }); +}); diff --git a/public/assets/alberto-escribano.jpeg b/public/assets/alberto-escribano.jpeg new file mode 100644 index 00000000..ede2fa67 Binary files /dev/null and b/public/assets/alberto-escribano.jpeg differ diff --git a/public/assets/jorge-miranda.jpeg b/public/assets/jorge-miranda.jpeg new file mode 100644 index 00000000..3543acb3 Binary files /dev/null and b/public/assets/jorge-miranda.jpeg differ diff --git a/public/assets/josemi-toribio.jpeg b/public/assets/josemi-toribio.jpeg new file mode 100644 index 00000000..4f5647d0 Binary files /dev/null and b/public/assets/josemi-toribio.jpeg differ diff --git a/public/icons/addlist.svg b/public/icons/addlist.svg new file mode 100644 index 00000000..e8e1e1b4 --- /dev/null +++ b/public/icons/addlist.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/alarm.svg b/public/icons/alarm.svg new file mode 100644 index 00000000..a2f17aec --- /dev/null +++ b/public/icons/alarm.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/alternativemouse.svg b/public/icons/alternativemouse.svg new file mode 100644 index 00000000..37a674b9 --- /dev/null +++ b/public/icons/alternativemouse.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/arrowbendupleft.svg b/public/icons/arrowbendupleft.svg new file mode 100644 index 00000000..8622868d --- /dev/null +++ b/public/icons/arrowbendupleft.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/arrowbendupright.svg b/public/icons/arrowbendupright.svg new file mode 100644 index 00000000..16929199 --- /dev/null +++ b/public/icons/arrowbendupright.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/arrowfatdown.svg b/public/icons/arrowfatdown.svg new file mode 100644 index 00000000..3b68d491 --- /dev/null +++ b/public/icons/arrowfatdown.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/arrowfatleft.svg b/public/icons/arrowfatleft.svg new file mode 100644 index 00000000..bc94d86f --- /dev/null +++ b/public/icons/arrowfatleft.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/arrowfatright.svg b/public/icons/arrowfatright.svg new file mode 100644 index 00000000..0000fd9d --- /dev/null +++ b/public/icons/arrowfatright.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/arrowfatup.svg b/public/icons/arrowfatup.svg new file mode 100644 index 00000000..c95f2b8d --- /dev/null +++ b/public/icons/arrowfatup.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/arrowsclockwise.svg b/public/icons/arrowsclockwise.svg new file mode 100644 index 00000000..2ac71b81 --- /dev/null +++ b/public/icons/arrowsclockwise.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/arrowscounterclockwise.svg b/public/icons/arrowscounterclockwise.svg new file mode 100644 index 00000000..3756d8d0 --- /dev/null +++ b/public/icons/arrowscounterclockwise.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/autoflash.svg b/public/icons/autoflash.svg new file mode 100644 index 00000000..75495c8e --- /dev/null +++ b/public/icons/autoflash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/callphoneincoming.svg b/public/icons/callphoneincoming.svg new file mode 100644 index 00000000..ec93c6e3 --- /dev/null +++ b/public/icons/callphoneincoming.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/camera.svg b/public/icons/camera.svg new file mode 100644 index 00000000..d51fcf35 --- /dev/null +++ b/public/icons/camera.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/caretdown.svg b/public/icons/caretdown.svg new file mode 100644 index 00000000..6bcca3ef --- /dev/null +++ b/public/icons/caretdown.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/caretleft.svg b/public/icons/caretleft.svg new file mode 100644 index 00000000..b4bf3c51 --- /dev/null +++ b/public/icons/caretleft.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/caretright.svg b/public/icons/caretright.svg new file mode 100644 index 00000000..523c9793 --- /dev/null +++ b/public/icons/caretright.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/caretup.svg b/public/icons/caretup.svg new file mode 100644 index 00000000..5f197bec --- /dev/null +++ b/public/icons/caretup.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/check.svg b/public/icons/check.svg new file mode 100644 index 00000000..f3015dcb --- /dev/null +++ b/public/icons/check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/checkfat.svg b/public/icons/checkfat.svg new file mode 100644 index 00000000..3a62fb8a --- /dev/null +++ b/public/icons/checkfat.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/clipboard.svg b/public/icons/clipboard.svg new file mode 100644 index 00000000..9df8092b --- /dev/null +++ b/public/icons/clipboard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/company.svg b/public/icons/company.svg new file mode 100644 index 00000000..a72ba20c --- /dev/null +++ b/public/icons/company.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/controller.svg b/public/icons/controller.svg new file mode 100644 index 00000000..fbc75e00 --- /dev/null +++ b/public/icons/controller.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/copyright.svg b/public/icons/copyright.svg new file mode 100644 index 00000000..b8838440 --- /dev/null +++ b/public/icons/copyright.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/cursor.svg b/public/icons/cursor.svg new file mode 100644 index 00000000..1776f3bf --- /dev/null +++ b/public/icons/cursor.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/cursorclick.svg b/public/icons/cursorclick.svg new file mode 100644 index 00000000..151300e7 --- /dev/null +++ b/public/icons/cursorclick.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/dotssixvertical.svg b/public/icons/dotssixvertical.svg new file mode 100644 index 00000000..f2f38a02 --- /dev/null +++ b/public/icons/dotssixvertical.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/dotssquare.svg b/public/icons/dotssquare.svg new file mode 100644 index 00000000..90e4c2b6 --- /dev/null +++ b/public/icons/dotssquare.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/dotsvertical.svg b/public/icons/dotsvertical.svg new file mode 100644 index 00000000..e78d0c71 --- /dev/null +++ b/public/icons/dotsvertical.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/doublecheck.svg b/public/icons/doublecheck.svg new file mode 100644 index 00000000..0b8babe4 --- /dev/null +++ b/public/icons/doublecheck.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/drive.svg b/public/icons/drive.svg new file mode 100644 index 00000000..adcab6ac --- /dev/null +++ b/public/icons/drive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/emptybattery.svg b/public/icons/emptybattery.svg new file mode 100644 index 00000000..8d4cbada --- /dev/null +++ b/public/icons/emptybattery.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/factory.svg b/public/icons/factory.svg new file mode 100644 index 00000000..c41a3fe1 --- /dev/null +++ b/public/icons/factory.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/filedoc.svg b/public/icons/filedoc.svg new file mode 100644 index 00000000..0e59f4fa --- /dev/null +++ b/public/icons/filedoc.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/fileexcel.svg b/public/icons/fileexcel.svg new file mode 100644 index 00000000..daf4d734 --- /dev/null +++ b/public/icons/fileexcel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/filejpg.svg b/public/icons/filejpg.svg new file mode 100644 index 00000000..63c73cc0 --- /dev/null +++ b/public/icons/filejpg.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/filepdf.svg b/public/icons/filepdf.svg new file mode 100644 index 00000000..db9d41a5 --- /dev/null +++ b/public/icons/filepdf.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/filepng.svg b/public/icons/filepng.svg new file mode 100644 index 00000000..2b12da29 --- /dev/null +++ b/public/icons/filepng.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/filepowerpoint.svg b/public/icons/filepowerpoint.svg new file mode 100644 index 00000000..9444ea8c --- /dev/null +++ b/public/icons/filepowerpoint.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/firstaid.svg b/public/icons/firstaid.svg new file mode 100644 index 00000000..07581b08 --- /dev/null +++ b/public/icons/firstaid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/flag.svg b/public/icons/flag.svg new file mode 100644 index 00000000..36594a99 --- /dev/null +++ b/public/icons/flag.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/flashslash.svg b/public/icons/flashslash.svg new file mode 100644 index 00000000..466efdd1 --- /dev/null +++ b/public/icons/flashslash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/genderfemale.svg b/public/icons/genderfemale.svg new file mode 100644 index 00000000..732bd0a2 --- /dev/null +++ b/public/icons/genderfemale.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/gendermale.svg b/public/icons/gendermale.svg new file mode 100644 index 00000000..8ec35a0d --- /dev/null +++ b/public/icons/gendermale.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/gitlab.svg b/public/icons/gitlab.svg new file mode 100644 index 00000000..bbc69a9c --- /dev/null +++ b/public/icons/gitlab.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/hammer.svg b/public/icons/hammer.svg new file mode 100644 index 00000000..110aa5c6 --- /dev/null +++ b/public/icons/hammer.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/handgrabbing.svg b/public/icons/handgrabbing.svg new file mode 100644 index 00000000..5c23986d --- /dev/null +++ b/public/icons/handgrabbing.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/handswipeleft.svg b/public/icons/handswipeleft.svg new file mode 100644 index 00000000..360faf9b --- /dev/null +++ b/public/icons/handswipeleft.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/handswiperight.svg b/public/icons/handswiperight.svg new file mode 100644 index 00000000..1e8c4416 --- /dev/null +++ b/public/icons/handswiperight.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/handtap.svg b/public/icons/handtap.svg new file mode 100644 index 00000000..3f0dde70 --- /dev/null +++ b/public/icons/handtap.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/help.svg b/public/icons/help.svg new file mode 100644 index 00000000..6097ec58 --- /dev/null +++ b/public/icons/help.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/home.svg b/public/icons/home.svg new file mode 100644 index 00000000..188c3d2d --- /dev/null +++ b/public/icons/home.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/joystick.svg b/public/icons/joystick.svg new file mode 100644 index 00000000..d5ecc1c1 --- /dev/null +++ b/public/icons/joystick.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/key.svg b/public/icons/key.svg new file mode 100644 index 00000000..b6ec42ae --- /dev/null +++ b/public/icons/key.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/keyboard.svg b/public/icons/keyboard.svg new file mode 100644 index 00000000..a7a79892 --- /dev/null +++ b/public/icons/keyboard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/lightning.svg b/public/icons/lightning.svg new file mode 100644 index 00000000..88fd6ae3 --- /dev/null +++ b/public/icons/lightning.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/linux.svg b/public/icons/linux.svg new file mode 100644 index 00000000..65615d6d --- /dev/null +++ b/public/icons/linux.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/listchecks.svg b/public/icons/listchecks.svg new file mode 100644 index 00000000..e447fa2b --- /dev/null +++ b/public/icons/listchecks.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/listdashes.svg b/public/icons/listdashes.svg new file mode 100644 index 00000000..102e7a31 --- /dev/null +++ b/public/icons/listdashes.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/listdots.svg b/public/icons/listdots.svg new file mode 100644 index 00000000..bfb265e6 --- /dev/null +++ b/public/icons/listdots.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/listheart.svg b/public/icons/listheart.svg new file mode 100644 index 00000000..40ee0693 --- /dev/null +++ b/public/icons/listheart.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/listnumbers.svg b/public/icons/listnumbers.svg new file mode 100644 index 00000000..1f72ebab --- /dev/null +++ b/public/icons/listnumbers.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/liststar.svg b/public/icons/liststar.svg new file mode 100644 index 00000000..243c4eaf --- /dev/null +++ b/public/icons/liststar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/lock.svg b/public/icons/lock.svg new file mode 100644 index 00000000..6eaba0b9 --- /dev/null +++ b/public/icons/lock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/lockopen.svg b/public/icons/lockopen.svg new file mode 100644 index 00000000..bc29248f --- /dev/null +++ b/public/icons/lockopen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/microphone.svg b/public/icons/microphone.svg new file mode 100644 index 00000000..be171729 --- /dev/null +++ b/public/icons/microphone.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/microphoneslash.svg b/public/icons/microphoneslash.svg new file mode 100644 index 00000000..262343fa --- /dev/null +++ b/public/icons/microphoneslash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/moon.svg b/public/icons/moon.svg new file mode 100644 index 00000000..ba13967b --- /dev/null +++ b/public/icons/moon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/musicnote.svg b/public/icons/musicnote.svg new file mode 100644 index 00000000..b77539be --- /dev/null +++ b/public/icons/musicnote.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/normalshield.svg b/public/icons/normalshield.svg new file mode 100644 index 00000000..9293db66 --- /dev/null +++ b/public/icons/normalshield.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/paintbucket.svg b/public/icons/paintbucket.svg new file mode 100644 index 00000000..6fa93cbe --- /dev/null +++ b/public/icons/paintbucket.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/phone.svg b/public/icons/phone.svg new file mode 100644 index 00000000..6bf536a0 --- /dev/null +++ b/public/icons/phone.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/phonecall.svg b/public/icons/phonecall.svg new file mode 100644 index 00000000..fbecdd97 --- /dev/null +++ b/public/icons/phonecall.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/phonehang.svg b/public/icons/phonehang.svg new file mode 100644 index 00000000..09677754 --- /dev/null +++ b/public/icons/phonehang.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/phonelist.svg b/public/icons/phonelist.svg new file mode 100644 index 00000000..57dd209d --- /dev/null +++ b/public/icons/phonelist.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/phonepause.svg b/public/icons/phonepause.svg new file mode 100644 index 00000000..dbb6a8db --- /dev/null +++ b/public/icons/phonepause.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/phoneslash.svg b/public/icons/phoneslash.svg new file mode 100644 index 00000000..e7086a36 --- /dev/null +++ b/public/icons/phoneslash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/pisymbol.svg b/public/icons/pisymbol.svg new file mode 100644 index 00000000..f33f668e --- /dev/null +++ b/public/icons/pisymbol.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/plug.svg b/public/icons/plug.svg new file mode 100644 index 00000000..2e07385f --- /dev/null +++ b/public/icons/plug.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/power.svg b/public/icons/power.svg new file mode 100644 index 00000000..c52b50bb --- /dev/null +++ b/public/icons/power.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/printer.svg b/public/icons/printer.svg new file mode 100644 index 00000000..c2df959d --- /dev/null +++ b/public/icons/printer.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/scissors.svg b/public/icons/scissors.svg new file mode 100644 index 00000000..fc8fc248 --- /dev/null +++ b/public/icons/scissors.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/searchlist.svg b/public/icons/searchlist.svg new file mode 100644 index 00000000..d1575f46 --- /dev/null +++ b/public/icons/searchlist.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/shieldcheck.svg b/public/icons/shieldcheck.svg new file mode 100644 index 00000000..ee9d4bfb --- /dev/null +++ b/public/icons/shieldcheck.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/shieldcheckered.svg b/public/icons/shieldcheckered.svg new file mode 100644 index 00000000..e0623fd7 --- /dev/null +++ b/public/icons/shieldcheckered.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/shieldslash.svg b/public/icons/shieldslash.svg new file mode 100644 index 00000000..267243bd --- /dev/null +++ b/public/icons/shieldslash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/shieldwarning.svg b/public/icons/shieldwarning.svg new file mode 100644 index 00000000..8c930220 --- /dev/null +++ b/public/icons/shieldwarning.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/signin.svg b/public/icons/signin.svg new file mode 100644 index 00000000..4290f3e8 --- /dev/null +++ b/public/icons/signin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/signout.svg b/public/icons/signout.svg new file mode 100644 index 00000000..26c94390 --- /dev/null +++ b/public/icons/signout.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/spinner.svg b/public/icons/spinner.svg new file mode 100644 index 00000000..94b6df09 --- /dev/null +++ b/public/icons/spinner.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/spinnergap.svg b/public/icons/spinnergap.svg new file mode 100644 index 00000000..3f13aacf --- /dev/null +++ b/public/icons/spinnergap.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/star.svg b/public/icons/star.svg new file mode 100644 index 00000000..d2659c54 --- /dev/null +++ b/public/icons/star.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/subtitles.svg b/public/icons/subtitles.svg new file mode 100644 index 00000000..075c420d --- /dev/null +++ b/public/icons/subtitles.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/table.svg b/public/icons/table.svg new file mode 100644 index 00000000..ab769a83 --- /dev/null +++ b/public/icons/table.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/tag.svg b/public/icons/tag.svg new file mode 100644 index 00000000..2d346419 --- /dev/null +++ b/public/icons/tag.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/textaligncenter.svg b/public/icons/textaligncenter.svg new file mode 100644 index 00000000..ed31774d --- /dev/null +++ b/public/icons/textaligncenter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/textalignjustify.svg b/public/icons/textalignjustify.svg new file mode 100644 index 00000000..bdddc920 --- /dev/null +++ b/public/icons/textalignjustify.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/textalignleft.svg b/public/icons/textalignleft.svg new file mode 100644 index 00000000..2b5c720e --- /dev/null +++ b/public/icons/textalignleft.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/textalignright.svg b/public/icons/textalignright.svg new file mode 100644 index 00000000..365ec8cc --- /dev/null +++ b/public/icons/textalignright.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/textbolder.svg b/public/icons/textbolder.svg new file mode 100644 index 00000000..9d76c192 --- /dev/null +++ b/public/icons/textbolder.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/textindent.svg b/public/icons/textindent.svg new file mode 100644 index 00000000..6b6d648f --- /dev/null +++ b/public/icons/textindent.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/textitalic.svg b/public/icons/textitalic.svg new file mode 100644 index 00000000..0d87b1f5 --- /dev/null +++ b/public/icons/textitalic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/textparagraph.svg b/public/icons/textparagraph.svg new file mode 100644 index 00000000..63fbc855 --- /dev/null +++ b/public/icons/textparagraph.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/textunderline.svg b/public/icons/textunderline.svg new file mode 100644 index 00000000..0824359d --- /dev/null +++ b/public/icons/textunderline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/tray.svg b/public/icons/tray.svg new file mode 100644 index 00000000..105565b2 --- /dev/null +++ b/public/icons/tray.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/upload.svg b/public/icons/upload.svg new file mode 100644 index 00000000..daf15cbe --- /dev/null +++ b/public/icons/upload.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/uppercaselowercase.svg b/public/icons/uppercaselowercase.svg new file mode 100644 index 00000000..adee2ea2 --- /dev/null +++ b/public/icons/uppercaselowercase.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/warning.svg b/public/icons/warning.svg new file mode 100644 index 00000000..7924d15f --- /dev/null +++ b/public/icons/warning.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/warningcircle.svg b/public/icons/warningcircle.svg new file mode 100644 index 00000000..f645bd1c --- /dev/null +++ b/public/icons/warningcircle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/warningoctagon.svg b/public/icons/warningoctagon.svg new file mode 100644 index 00000000..6cd14c31 --- /dev/null +++ b/public/icons/warningoctagon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/webcam.svg b/public/icons/webcam.svg new file mode 100644 index 00000000..a28cd7ea --- /dev/null +++ b/public/icons/webcam.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/webcamslash.svg b/public/icons/webcamslash.svg new file mode 100644 index 00000000..591edab2 --- /dev/null +++ b/public/icons/webcamslash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/windows.svg b/public/icons/windows.svg new file mode 100644 index 00000000..dbf56516 --- /dev/null +++ b/public/icons/windows.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/shapes/modalCover.svg b/public/shapes/modalCover.svg new file mode 100644 index 00000000..a471d138 --- /dev/null +++ b/public/shapes/modalCover.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/text/link.svg b/public/text/link.svg new file mode 100644 index 00000000..cfdba9f9 --- /dev/null +++ b/public/text/link.svg @@ -0,0 +1,3 @@ + + Link + diff --git a/public/widgets/timepicker.svg b/public/widgets/timepicker.svg index d71771ce..1a262986 100644 --- a/public/widgets/timepicker.svg +++ b/public/widgets/timepicker.svg @@ -1,13 +1,24 @@ - - - - - - - : - - - : - + + + + + Time + + + + hh:mm + + + + + + + + + + + + + + diff --git a/src/common/components/icons/caret-down-icon.component.tsx b/src/common/components/icons/caret-down-icon.component.tsx new file mode 100644 index 00000000..b51c615a --- /dev/null +++ b/src/common/components/icons/caret-down-icon.component.tsx @@ -0,0 +1,15 @@ +export const CaretDown = () => { + return ( + + + + ); +}; diff --git a/src/common/components/icons/index.ts b/src/common/components/icons/index.ts index 2bf628b8..74835f5b 100644 --- a/src/common/components/icons/index.ts +++ b/src/common/components/icons/index.ts @@ -8,3 +8,7 @@ export * from './x-icon.component'; export * from './quickmock-logo.component'; export * from './copy-icon.component'; export * from './paste-icon.component'; +export * from './delete-icon.component'; +export * from './pencil-icon.component'; +export * from './caret-down-icon.component'; +export * from './plus-icon.component'; diff --git a/src/common/components/icons/pencil-icon.component.tsx b/src/common/components/icons/pencil-icon.component.tsx new file mode 100644 index 00000000..232a6868 --- /dev/null +++ b/src/common/components/icons/pencil-icon.component.tsx @@ -0,0 +1,15 @@ +export const PencilIcon = () => { + return ( + + + + ); +}; diff --git a/src/common/components/icons/plus-icon.component.tsx b/src/common/components/icons/plus-icon.component.tsx new file mode 100644 index 00000000..71739b2a --- /dev/null +++ b/src/common/components/icons/plus-icon.component.tsx @@ -0,0 +1,16 @@ +export const PlusIcon = () => { + return ( + + ); +}; diff --git a/src/common/components/mock-components/front-basic-shapes/index.ts b/src/common/components/mock-components/front-basic-shapes/index.ts index 8a8940da..0303d694 100644 --- a/src/common/components/mock-components/front-basic-shapes/index.ts +++ b/src/common/components/mock-components/front-basic-shapes/index.ts @@ -8,3 +8,4 @@ export * from './circle-basic-shape'; export * from './star-shape'; export * from './large-arrow-shape'; export * from './image-shape'; +export * from './modal-cover-shape'; diff --git a/src/common/components/mock-components/front-basic-shapes/modal-cover-shape.tsx b/src/common/components/mock-components/front-basic-shapes/modal-cover-shape.tsx new file mode 100644 index 00000000..d40094f4 --- /dev/null +++ b/src/common/components/mock-components/front-basic-shapes/modal-cover-shape.tsx @@ -0,0 +1,65 @@ +import { fitSizeToShapeSizeRestrictions } from '@/common/utils/shapes'; +import { ShapeSizeRestrictions } from '@/core/model'; +import { Group, Rect } from 'react-konva'; +import { useGroupShapeProps } from '../mock-components.utils'; +import { forwardRef } from 'react'; +import { ShapeProps } from '../shape.model'; + +const modalCoverShapeSizeRestrictions: ShapeSizeRestrictions = { + minWidth: 50, + minHeight: 50, + maxWidth: -1, + maxHeight: -1, + defaultWidth: 200, + defaultHeight: 200, +}; + +export const getModalCoverShapeSizeRestrictions = (): ShapeSizeRestrictions => + modalCoverShapeSizeRestrictions; + +const shapeType = 'modalCover'; + +export const ModalCoverShape = forwardRef((props, ref) => { + const { + x, + y, + width, + height, + id, + onSelected, + text, + otherProps, + ...shapeProps + } = props; + + const restrictedSize = fitSizeToShapeSizeRestrictions( + modalCoverShapeSizeRestrictions, + width, + height + ); + + const { width: restrictedWidth, height: restrictedHeight } = restrictedSize; + + const commonGroupProps = useGroupShapeProps( + props, + restrictedSize, + shapeType, + ref + ); + + return ( + + + + ); +}); 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 198d588d..5cdf01e0 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 @@ -60,9 +60,9 @@ export const VerticalLineShape = forwardRef((props, ref) => { /> ((props, ref) => { /> ( ellipsis={true} wrap="none" /> + + {/* Calendar Icon */} { + const response = await fetch(url); + const svgText = await response.text(); + + const modifiedSvg = svgText.replace(/fill="[^"]*"/g, `fill="${fillColor}"`); + + const svgBlob = new Blob([modifiedSvg], { type: 'image/svg+xml' }); + const objectURL = URL.createObjectURL(svgBlob); + + const img = new window.Image(); + img.src = objectURL; + + return img; +}; + +export const returnIconSize = (iconSize: IconSize): number[] => { + switch (iconSize) { + case 'XS': + return [25, 25]; + case 'S': + return [50, 50]; + case 'M': + return [100, 100]; + case 'L': + return [125, 125]; + case 'XL': + return [150, 150]; + default: + return [50, 50]; + } +}; diff --git a/src/common/components/mock-components/front-components/icon-shape.tsx b/src/common/components/mock-components/front-components/icon/icon-shape.tsx similarity index 66% rename from src/common/components/mock-components/front-components/icon-shape.tsx rename to src/common/components/mock-components/front-components/icon/icon-shape.tsx index 6e3936e2..f268a9ae 100644 --- a/src/common/components/mock-components/front-components/icon-shape.tsx +++ b/src/common/components/mock-components/front-components/icon/icon-shape.tsx @@ -1,18 +1,13 @@ import { fitSizeToShapeSizeRestrictions } from '@/common/utils/shapes/shape-restrictions'; -import { - BASE_ICONS_URL, - IconSize, - ShapeSizeRestrictions, - ShapeType, -} from '@/core/model'; -import { forwardRef } from 'react'; +import { BASE_ICONS_URL, ShapeSizeRestrictions, ShapeType } from '@/core/model'; +import { forwardRef, useRef, useState, useEffect } from 'react'; import { Group, Image } from 'react-konva'; -import useImage from 'use-image'; -import { ShapeProps } from '../shape.model'; +import { ShapeProps } from '../../shape.model'; import { useModalDialogContext } from '@/core/providers/model-dialog-providers/model-dialog.provider'; import { IconModal } from '@/pods/properties/components/icon-selector/modal'; import { useCanvasContext } from '@/core/providers'; -import { useGroupShapeProps } from '../mock-components.utils'; +import { useGroupShapeProps } from '../../mock-components.utils'; +import { loadSvgWithFill, returnIconSize } from './icon-shape.business'; const iconShapeRestrictions: ShapeSizeRestrictions = { minWidth: 25, @@ -38,11 +33,13 @@ export const SvgIcon = forwardRef((props, ref) => { onSelected, iconInfo, iconSize, + stroke, ...shapeProps } = props; const { openModal } = useModalDialogContext(); const { selectionInfo } = useCanvasContext(); const { updateOtherPropsOnSelected } = selectionInfo; + const handleDoubleClick = () => { openModal( ((props, ref) => { 'Choose Icon' ); }; - const [image] = useImage(`${BASE_ICONS_URL}${iconInfo.filename}`); - - const returnIconSize = (iconSize: IconSize): number[] => { - switch (iconSize) { - case 'XS': - return [25, 25]; - case 'S': - return [50, 50]; - case 'M': - return [100, 100]; - case 'L': - return [125, 125]; - case 'XL': - return [150, 150]; - default: - return [50, 50]; - } - }; const [iconWidth, iconHeight] = returnIconSize(iconSize); @@ -88,15 +67,32 @@ export const SvgIcon = forwardRef((props, ref) => { ref ); + const [image, setImage] = useState(null); + const imageRef = useRef(null); + + useEffect(() => { + if (iconInfo?.filename) { + loadSvgWithFill( + `${BASE_ICONS_URL}${iconInfo.filename}`, + `${stroke}` + ).then(img => { + setImage(img); + }); + } + }, [iconInfo?.filename, stroke]); + return ( - + {image && ( + + )} ); }); diff --git a/src/common/components/mock-components/front-components/icon/index.ts b/src/common/components/mock-components/front-components/icon/index.ts new file mode 100644 index 00000000..076f313a --- /dev/null +++ b/src/common/components/mock-components/front-components/icon/index.ts @@ -0,0 +1 @@ +export * from './icon-shape'; diff --git a/src/common/components/mock-components/front-components/index.ts b/src/common/components/mock-components/front-components/index.ts index ae303ff1..f79d76ca 100644 --- a/src/common/components/mock-components/front-components/index.ts +++ b/src/common/components/mock-components/front-components/index.ts @@ -7,9 +7,9 @@ export * from './progressbar-shape'; export * from './listbox'; export * from './datepickerinput-shape'; export * from './button-shape'; -export * from './timepickerinput-shape'; +export * from './timepickerinput/timepickerinput-shape'; export * from './radiobutton-shape'; -export * from './icon-shape'; +export * from './icon'; export * from './verticalscrollbar-shape'; export * from './horizontalscrollbar-shape'; export * from './label-shape'; diff --git a/src/common/components/mock-components/front-components/input-shape.tsx b/src/common/components/mock-components/front-components/input-shape.tsx index 2c415044..959c66e8 100644 --- a/src/common/components/mock-components/front-components/input-shape.tsx +++ b/src/common/components/mock-components/front-components/input-shape.tsx @@ -82,3 +82,33 @@ export const InputShape = forwardRef((props, ref) => { ); }); + +/* + + + + + +*/ diff --git a/src/common/components/mock-components/front-components/shape.const.ts b/src/common/components/mock-components/front-components/shape.const.ts index f35a775e..458d4178 100644 --- a/src/common/components/mock-components/front-components/shape.const.ts +++ b/src/common/components/mock-components/front-components/shape.const.ts @@ -88,3 +88,28 @@ export const POSTIT_SHAPE: DefaultStyleShape = { DEFAULT_FONT_STYLE, DEFAULT_TEXT_DECORATION, }; + +interface FontValues { + HEADING1: number; + HEADING2: number; + HEADING3: number; + NORMALTEXT: number; + SMALLTEXT: number; + PARAGRAPH: number; + LINK: number; +} + +export const FONT_SIZE_VALUES: FontValues = { + HEADING1: 28, + HEADING2: 24, + HEADING3: 18, + NORMALTEXT: 18, + SMALLTEXT: 14, + PARAGRAPH: 14, + LINK: 20, +}; + +export const LINK_SHAPE: DefaultStyleShape = { + ...BASIC_SHAPE, + DEFAULT_FILL_TEXT: '#0000FF', +}; diff --git a/src/common/components/mock-components/front-components/timepickerinput-shape.tsx b/src/common/components/mock-components/front-components/timepickerinput-shape.tsx deleted file mode 100644 index a4b64adc..00000000 --- a/src/common/components/mock-components/front-components/timepickerinput-shape.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { ShapeSizeRestrictions, ShapeType } from '@/core/model'; -import { forwardRef } from 'react'; -import { ShapeProps } from '../shape.model'; -import { fitSizeToShapeSizeRestrictions } from '@/common/utils/shapes/shape-restrictions'; -import { Group, Rect, Text } from 'react-konva'; -import { BASIC_SHAPE } from './shape.const'; -import { useShapeProps } from '../../shapes/use-shape-props.hook'; -import { useGroupShapeProps } from '../mock-components.utils'; - -const timepickerInputShapeRestrictions: ShapeSizeRestrictions = { - minWidth: 100, - minHeight: 50, - maxWidth: -1, - maxHeight: 50, - defaultWidth: 220, - defaultHeight: 50, -}; - -const shapeType: ShapeType = 'timepickerinput'; - -export const getTimepickerInputShapeSizeRestrictions = - (): ShapeSizeRestrictions => timepickerInputShapeRestrictions; - -export const TimepickerInputShape = forwardRef( - (props, ref) => { - const { x, y, width, height, id, onSelected, otherProps, ...shapeProps } = - props; - const restrictedSize = fitSizeToShapeSizeRestrictions( - timepickerInputShapeRestrictions, - width, - height - ); - const { width: restrictedWidth, height: restrictedHeight } = restrictedSize; - - const separatorPadding = 3; // Extra padding for spacers - const separator1X = restrictedWidth / 3; - const separator2X = (2 * restrictedWidth) / 3; - - const { stroke, strokeStyle, fill, borderRadius } = useShapeProps( - otherProps, - BASIC_SHAPE - ); - - const commonGroupProps = useGroupShapeProps( - props, - restrictedSize, - shapeType, - ref - ); - - return ( - - {/* input frame */} - - - {/* Separators : */} - - - - ); - } -); diff --git a/src/common/components/mock-components/front-components/timepickerinput/index.ts b/src/common/components/mock-components/front-components/timepickerinput/index.ts new file mode 100644 index 00000000..b2663481 --- /dev/null +++ b/src/common/components/mock-components/front-components/timepickerinput/index.ts @@ -0,0 +1 @@ +export * from './timepickerinput-shape.tsx'; diff --git a/src/common/components/mock-components/front-components/timepickerinput/timepickerinput-shape.business.ts b/src/common/components/mock-components/front-components/timepickerinput/timepickerinput-shape.business.ts new file mode 100644 index 00000000..a257c096 --- /dev/null +++ b/src/common/components/mock-components/front-components/timepickerinput/timepickerinput-shape.business.ts @@ -0,0 +1,33 @@ +const MAX_DIGITS = 2; +const MAX_HOURS = '23'; +const MAX_MINUTES = '59'; +const HOUR_MASK = 'hh'; +const MINUTES_MASK = 'mm'; + +export const splitCSVContent = (csvContent: string): string[] => { + const splitedCsvContent = csvContent + .trim() + .split(/[:|,]/) + .map(el => el.trim()); + return splitedCsvContent; +}; + +export const setTime = (csvData: string[]) => { + let [hour, minutes] = csvData; + if (csvData.length < 2) { + return true; + } + if (csvData[0] !== HOUR_MASK || csvData[1] !== MINUTES_MASK) { + if ( + csvData.length > MAX_DIGITS || + hour.length !== MAX_DIGITS || + hour === '' || + hour > MAX_HOURS || + minutes.length !== MAX_DIGITS || + minutes === '' || + minutes > MAX_MINUTES + ) { + return true; + } + } +}; diff --git a/src/common/components/mock-components/front-components/timepickerinput/timepickerinput-shape.tsx b/src/common/components/mock-components/front-components/timepickerinput/timepickerinput-shape.tsx new file mode 100644 index 00000000..a0ec43c8 --- /dev/null +++ b/src/common/components/mock-components/front-components/timepickerinput/timepickerinput-shape.tsx @@ -0,0 +1,148 @@ +import { ShapeSizeRestrictions, ShapeType } from '@/core/model'; +import { forwardRef } from 'react'; +import { ShapeProps } from '../../shape.model'; +import { fitSizeToShapeSizeRestrictions } from '@/common/utils/shapes/shape-restrictions'; +import { Group, Rect, Text, Image } from 'react-konva'; +import { BASIC_SHAPE } from '../shape.const'; +import { useShapeProps } from '../../../shapes/use-shape-props.hook'; +import { useGroupShapeProps } from '../../mock-components.utils'; +import { splitCSVContent, setTime } from './timepickerinput-shape.business'; + +import clockIconSrc from '/icons/clock.svg'; + +const timepickerInputShapeRestrictions: ShapeSizeRestrictions = { + minWidth: 100, + minHeight: 50, + maxWidth: -1, + maxHeight: 50, + defaultWidth: 220, + defaultHeight: 50, +}; + +const shapeType: ShapeType = 'timepickerinput'; + +export const getTimepickerInputShapeSizeRestrictions = + (): ShapeSizeRestrictions => timepickerInputShapeRestrictions; + +export const TimepickerInputShape = forwardRef( + (props, ref) => { + const { + x, + y, + width, + height, + id, + text, + onSelected, + otherProps, + ...shapeProps + } = props; + + const restrictedSize = fitSizeToShapeSizeRestrictions( + timepickerInputShapeRestrictions, + width, + height + ); + const { width: restrictedWidth, height: restrictedHeight } = restrictedSize; + + const { stroke, strokeStyle, fill, borderRadius } = useShapeProps( + otherProps, + BASIC_SHAPE + ); + + const commonGroupProps = useGroupShapeProps( + props, + restrictedSize, + shapeType, + ref + ); + + const csvData = splitCSVContent(text); + let isError = setTime(csvData); + + const iconWidth = 25; + const availableWidth = restrictedWidth - iconWidth - 20; + const fontSize = Math.min( + availableWidth * 0.2, + restrictedHeight * 0.35, + 20 + ); + const labelFontSize = Math.min(restrictedHeight * 0.3, 12); + + const clockIcon = new window.Image(); + clockIcon.src = clockIconSrc; + + return ( + + {/* External Rectangle */} + + {/* Background of Time Label */} + + {/* Label "Time" */} + + {/* Main Text */} + + {/* Error Text */} + {isError && ( + + )} + + {/* Clock Icon */} + + + ); + } +); diff --git a/src/common/components/mock-components/front-containers/browserwindow-shape.tsx b/src/common/components/mock-components/front-containers/browserwindow-shape.tsx index 524d8937..a40eca77 100644 --- a/src/common/components/mock-components/front-containers/browserwindow-shape.tsx +++ b/src/common/components/mock-components/front-containers/browserwindow-shape.tsx @@ -21,7 +21,7 @@ export const getBrowserWindowShapeSizeRestrictions = const shapeType: ShapeType = 'browser'; export const BrowserWindowShape = forwardRef((props, ref) => { - const { x, y, width, height, id, onSelected, ...shapeProps } = props; + const { x, y, width, height, id, onSelected, text, ...shapeProps } = props; const restrictedSize = fitSizeToShapeSizeRestrictions( browserWindowShapeSizeRestrictions, width, @@ -114,9 +114,9 @@ export const BrowserWindowShape = forwardRef((props, ref) => { ((props, ref) => { const buttonWidth = (restrictedWidth - (buttons.length + 1) * buttonSpacing) / buttons.length; - const { stroke, strokeStyle, fill, textColor } = useShapeProps( + const { stroke, strokeStyle, fill, textColor, fontSize } = useShapeProps( otherProps, BASIC_SHAPE ); @@ -99,7 +99,7 @@ export const Modal = forwardRef((props, ref) => { width={restrictedWidth - 60} text={modalTitle} fontFamily="Arial" - fontSize={18} + fontSize={fontSize} fill="white" wrap="none" ellipsis={true} diff --git a/src/common/components/mock-components/front-rich-components/tabsbar/business/balance-space.spec.ts b/src/common/components/mock-components/front-rich-components/tabsbar/business/balance-space.spec.ts new file mode 100644 index 00000000..a228db36 --- /dev/null +++ b/src/common/components/mock-components/front-rich-components/tabsbar/business/balance-space.spec.ts @@ -0,0 +1,77 @@ +import { describe, it, expect } from 'vitest'; +import { balanceSpacePerItem } from './balance-space'; + +const _sum = (resultado: number[]) => + resultado.reduce((acc, current) => acc + current, 0); + +describe('balanceSpacePerItem tests', () => { + it('should return an array which sums 150 when apply [10, 20, 30, 40, 50]', () => { + // Arrange + const theArray = [10, 20, 30, 40, 50]; + const availableWidth = 150; + + // Act + const result = balanceSpacePerItem(theArray, availableWidth); + const totalSum = _sum(result); + + // Assert + expect(totalSum).toBeGreaterThan(0); + expect(totalSum).toBeLessThanOrEqual(availableWidth); + }); + + it('should return an array which sums equal or less than 100 when apply [10, 20, 30, 40, 50]', () => { + // Arrange + const theArray = [10, 20, 30, 40, 50]; + const availableWidth = 100; + + // Act + const result = balanceSpacePerItem(theArray, availableWidth); + const totalSum = _sum(result); + + // Assert + expect(totalSum).toBeGreaterThan(0); + expect(totalSum).toBeLessThanOrEqual(availableWidth); + }); + + it('should return an array which sums less or equal than 150 when apply [10, 20, 31, 41, 50]', () => { + // Arrange + const theArray = [10, 20, 31, 41, 50]; + const availableWidth = 150; + + // Act + const result = balanceSpacePerItem(theArray, availableWidth); + const totalSum = _sum(result); + + // Assert + expect(totalSum).toBeGreaterThan(0); + expect(totalSum).toBeLessThanOrEqual(availableWidth); + }); + + it('should return an array which sums 10 when apply [10]', () => { + // Arrange + const theArray = [100]; + const availableWidth = 10; + + // Act + const result = balanceSpacePerItem(theArray, availableWidth); + const totalSum = _sum(result); + + // Assert + expect(totalSum).toBeGreaterThan(0); + expect(totalSum).toBeLessThanOrEqual(availableWidth); + }); + + it('should return an array which sums 18 when apply [10, 10]', () => { + // Arrange + const theArray = [10, 10]; + const availableWidth = 18; + + // Act + const result = balanceSpacePerItem(theArray, availableWidth); + const totalSum = _sum(result); + + // Assert + expect(totalSum).toBeGreaterThan(0); + expect(totalSum).toBeLessThanOrEqual(availableWidth); + }); +}); diff --git a/src/common/components/mock-components/front-rich-components/tabsbar/business/balance-space.ts b/src/common/components/mock-components/front-rich-components/tabsbar/business/balance-space.ts new file mode 100644 index 00000000..8e683166 --- /dev/null +++ b/src/common/components/mock-components/front-rich-components/tabsbar/business/balance-space.ts @@ -0,0 +1,75 @@ +/** + * This calc is made "layer by layer", distributing a larger chunk of width in each iteration + * @param {Array} itemList - List of spaces to balance (Must be provided in ascendent order to work) + * @param {Number} availableSpace - The amount of space to be distributed + */ +export const balanceSpacePerItem = ( + itemList: number[], + availableSpace: number +) => { + const totalSpaceUsed = _spacesFactory(); + const maxItemSize = _spacesFactory(); + + return itemList.reduce((newList: number[], current, index, arr) => { + // Check if the array provided is properly ordered + if (index > 0) _checkListOrder(arr[index - 1], current); + + const lastItemSize: number = index > 0 ? newList[index - 1] : 0; + + // A) Once the maximum possible size of the item is reached, apply this size directly. + if (maxItemSize.value) { + totalSpaceUsed.add(maxItemSize.value); + return [...newList, lastItemSize]; + } + + /** Precalculate "existingSum + spaceSum" taking into account + * all next items supposing all they use current size */ + const timesToApply = arr.length - index; + const virtualTotalsSum = totalSpaceUsed.value + current * timesToApply; + + /** B) First "Bigger" tab behaviour: If the virtual-sum of next items using this size + * doesn't fit within available space, calc maxItemSize */ + if (virtualTotalsSum >= availableSpace) { + const remainder = + availableSpace - (totalSpaceUsed.value + lastItemSize * timesToApply); + const remainderPortionPerItem = Math.floor(remainder / timesToApply); + maxItemSize.set(lastItemSize + remainderPortionPerItem); + + totalSpaceUsed.add(maxItemSize.value); + + return [...newList, maxItemSize.value]; + } + + // C) "Normal" behaviour: Apply proposed new size to current + totalSpaceUsed.add(current); + return [...newList, current]; + }, []); +}; + +/* Balance helper functions: */ + +function _checkListOrder(prev: number, current: number) { + if (prev > current) { + throw new Error( + 'Disordered list. Please provide an ascendent ordered list as param *itemlist*' + ); + } +} + +function _spacesFactory() { + let _size = 0; + //Assure we are setting natural num w/o decimals + const _adjustNum = (num: number) => { + if (typeof num !== 'number') throw new Error('Number must be provided'); + return Math.max(0, Math.floor(num)); + }; + const add = (qty: number) => (_size += _adjustNum(qty)); + const set = (qty: number) => (_size = _adjustNum(qty)); + return { + get value() { + return _size; + }, + add, + set, + }; +} diff --git a/src/common/components/mock-components/front-rich-components/tabsbar/business/calc-text-width.ts b/src/common/components/mock-components/front-rich-components/tabsbar/business/calc-text-width.ts new file mode 100644 index 00000000..843bd15e --- /dev/null +++ b/src/common/components/mock-components/front-rich-components/tabsbar/business/calc-text-width.ts @@ -0,0 +1,50 @@ +import { Layer } from 'konva/lib/Layer'; + +/** + * Virtually calculates the width that a text will occupy, by using a canvas. + * If a Konva Layer is provided, it will reuse the already existing canvas. + * Otherwise, it will create a canvas within the document, on the fly, to perform the measurement. + * Finaly, as a safety net, a very generic calculation is provided in case the other options are not available. + */ +export const calcTextWidth = ( + inputText: string, + fontSize: number, + fontfamily: string, + konvaLayer?: Layer +) => { + if (konvaLayer) + return _getTextWidthByKonvaMethod( + konvaLayer, + inputText, + fontSize, + fontfamily + ); + + return _getTextCreatingNewCanvas(inputText, fontSize, fontfamily); +}; + +const _getTextWidthByKonvaMethod = ( + konvaLayer: Layer, + text: string, + fontSize: number, + fontfamily: string +) => { + const context = konvaLayer.getContext(); + context.font = `${fontSize}px ${fontfamily}`; + return context.measureText(text).width; +}; + +const _getTextCreatingNewCanvas = ( + text: string, + fontSize: number, + fontfamily: string +) => { + let canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + if (context) { + context.font = `${fontSize}px ${fontfamily}`; + return context.measureText(text).width; + } + const charAverageWidth = fontSize * 0.7; + return text.length * charAverageWidth + charAverageWidth * 0.8; +}; diff --git a/src/common/components/mock-components/front-rich-components/tabsbar/business/tabsbar.business.spec.ts b/src/common/components/mock-components/front-rich-components/tabsbar/business/tabsbar.business.spec.ts new file mode 100644 index 00000000..5e6b414b --- /dev/null +++ b/src/common/components/mock-components/front-rich-components/tabsbar/business/tabsbar.business.spec.ts @@ -0,0 +1,42 @@ +import { describe, it, expect } from 'vitest'; +import { adjustTabWidths } from './tabsbar.business'; + +const _sum = (resultado: number[]) => + resultado.reduce((acc, current) => acc + current, 0); + +describe('tabsbar.business tests', () => { + it('should return a new array of numbers, which sum is less than or equal to totalWidth', () => { + // Arrange + const tabs = [ + 'Text', + 'Normal text for tab', + 'Extra large text for a tab', + 'Really really large text for a tab', + 'xs', + ]; + const containerWidth = 1000; + const minTabWidth = 100; + const tabsGap = 10; + + // Act + const result = adjustTabWidths({ + tabs, + containerWidth, + minTabWidth, + tabXPadding: 20, + tabsGap, + font: { fontSize: 14, fontFamily: 'Arial' }, + }); + + console.log({ tabs }, { containerWidth }, { minTabWidth }); + console.log({ result }); + + const totalSum = _sum(result.widthList) + (tabs.length - 1) * tabsGap; + console.log('totalSum: ', totalSum); + + // Assert + expect(result.widthList[0]).not.toBe(0); + expect(result.widthList.length).toBe(tabs.length); + expect(totalSum).toBeLessThanOrEqual(containerWidth); + }); +}); 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 new file mode 100644 index 00000000..ce08f8bb --- /dev/null +++ b/src/common/components/mock-components/front-rich-components/tabsbar/business/tabsbar.business.ts @@ -0,0 +1,87 @@ +import { Layer } from 'konva/lib/Layer'; +import { balanceSpacePerItem } from './balance-space'; +import { calcTextWidth } from './calc-text-width'; + +export const adjustTabWidths = (args: { + tabs: string[]; + containerWidth: number; + minTabWidth: number; + tabXPadding: number; + tabsGap: number; + font: { + fontSize: number; + fontFamily: string; + }; + konvaLayer?: Layer; +}) => { + const { + tabs, + containerWidth, + minTabWidth, + tabXPadding, + tabsGap, + font, + konvaLayer, + } = args; + const totalInnerXPadding = tabXPadding * 2; + const totalMinTabSpace = minTabWidth + totalInnerXPadding; + const containerWidthWithoutTabGaps = + containerWidth - (tabs.length - 1) * tabsGap; + + //Create info List with originalPositions and desired width + interface OriginalTabInfo { + originalTabPosition: number; + desiredWidth: number; + } + const arrangeTabsInfo = tabs.reduce( + (acc: OriginalTabInfo[], tab, index): OriginalTabInfo[] => { + const tabFullTextWidth = + calcTextWidth(tab, font.fontSize, font.fontFamily, konvaLayer) + + totalInnerXPadding; + const desiredWidth = Math.max(totalMinTabSpace, tabFullTextWidth); + return [ + ...acc, + { + originalTabPosition: index, + desiredWidth, + }, + ]; + }, + [] + ); + + // This order is necessary to build layer by layer the new sizes + const ascendentTabList = arrangeTabsInfo.sort( + (a, b) => a.desiredWidth - b.desiredWidth + ); + + const onlyWidthList = ascendentTabList.map(tab => tab.desiredWidth); + // Apply adjustments + const adjustedSizeList = balanceSpacePerItem( + onlyWidthList, + containerWidthWithoutTabGaps + ); + + // Reassemble new data with the original order + const reassembledData = ascendentTabList.reduce( + (accList: number[], current, index) => { + const newList = [...accList]; + newList[current.originalTabPosition] = adjustedSizeList[index]; + return newList; + }, + [] + ); + + // Calc item offset position (mixed with external variable to avoid adding to reducer() extra complexity) + let sumOfXposition = 0; + const relativeTabPosition = reassembledData.reduce( + (acc: number[], currentTab, index) => { + const currentElementXPos = index ? sumOfXposition : 0; + sumOfXposition += currentTab + tabsGap; + return [...acc, currentElementXPos]; + }, + [] + ); + + return { widthList: reassembledData, relativeTabPosition }; +}; diff --git a/src/common/components/mock-components/front-rich-components/tabsbar/tab-list.hook.ts b/src/common/components/mock-components/front-rich-components/tabsbar/tab-list.hook.ts new file mode 100644 index 00000000..e115f4c2 --- /dev/null +++ b/src/common/components/mock-components/front-rich-components/tabsbar/tab-list.hook.ts @@ -0,0 +1,64 @@ +import { useEffect, useState } from 'react'; +import { adjustTabWidths } from './business/tabsbar.business'; +import { + extractCSVHeaders, + splitCSVContentIntoRows, +} from '@/common/utils/active-element-selector.utils'; +import { useCanvasContext } from '@/core/providers'; + +interface TabListConfig { + text: string; + containerWidth: number; + minTabWidth: number; + tabXPadding: number; + tabsGap: number; + font: { + fontSize: number; + fontFamily: string; + }; +} + +export const useTabList = (tabsConfig: TabListConfig) => { + const { text, containerWidth, minTabWidth, tabXPadding, tabsGap, font } = + tabsConfig; + + const [tabWidthList, setTabWidthList] = useState<{ + widthList: number[]; + relativeTabPosition: number[]; + }>({ widthList: [], relativeTabPosition: [] }); + + const tabLabels = _extractTabLabelTexts(text); + + const konvaLayer = useCanvasContext().stageRef.current?.getLayers()[0]; + + useEffect(() => { + setTabWidthList( + adjustTabWidths({ + tabs: tabLabels, + containerWidth, + minTabWidth, + tabXPadding, + tabsGap, + font: { + fontSize: font.fontSize, + fontFamily: font.fontFamily, + }, + konvaLayer, + }) + ); + }, [text, containerWidth]); + + //Return an unique array with all the info required by each tab + return tabLabels.map((tab, index) => ({ + tab, + width: tabWidthList.widthList[index], + xPos: tabWidthList.relativeTabPosition[index], + })); +}; + +// Split text to tab labels List +function _extractTabLabelTexts(text: string) { + const csvData = splitCSVContentIntoRows(text); + const headers = extractCSVHeaders(csvData[0]); + return headers.map(header => header.text); +} diff --git a/src/common/components/mock-components/front-rich-components/tabsbar/tabsbar-shape.tsx b/src/common/components/mock-components/front-rich-components/tabsbar/tabsbar-shape.tsx index 7e872a5a..d76cb6cf 100644 --- a/src/common/components/mock-components/front-rich-components/tabsbar/tabsbar-shape.tsx +++ b/src/common/components/mock-components/front-rich-components/tabsbar/tabsbar-shape.tsx @@ -3,11 +3,8 @@ import { Group, Rect, Text } from 'react-konva'; import { ShapeSizeRestrictions, ShapeType } from '@/core/model'; import { fitSizeToShapeSizeRestrictions } from '@/common/utils/shapes/shape-restrictions'; import { ShapeProps } from '../../shape.model'; -import { - extractCSVHeaders, - splitCSVContentIntoRows, -} from '@/common/utils/active-element-selector.utils'; import { useGroupShapeProps } from '../../mock-components.utils'; +import { useTabList } from './tab-list.hook'; const tabsBarShapeSizeRestrictions: ShapeSizeRestrictions = { minWidth: 450, @@ -15,7 +12,7 @@ const tabsBarShapeSizeRestrictions: ShapeSizeRestrictions = { maxWidth: -1, maxHeight: -1, defaultWidth: 450, - defaultHeight: 150, + defaultHeight: 180, }; export const getTabsBarShapeSizeRestrictions = (): ShapeSizeRestrictions => @@ -42,16 +39,21 @@ export const TabsBarShape = forwardRef((props, ref) => { ); const { width: restrictedWidth, height: restrictedHeight } = restrictedSize; - const csvData = splitCSVContentIntoRows(text); - const headers = extractCSVHeaders(csvData[0]); - const tabLabels = headers.map(header => header.text); - - // Calculate tab dimensions and margin - const tabWidth = 106; // Width of each tab - const tabHeight = 30; // Tab height - const tabMargin = 10; // Horizontal margin between tabs + // Tab dimensions and margin + const tabHeight = 30; + const tabsGap = 10; + const tabXPadding = 20; + const tabFont = { fontSize: 14, fontFamily: 'Arial, sans-serif' }; const bodyHeight = restrictedHeight - tabHeight - 10; // Height of the tabs bar body - const totalTabsWidth = tabLabels.length * (tabWidth + tabMargin) + tabWidth; // Total width required plus one additional tab + + const tabList = useTabList({ + text, + containerWidth: restrictedWidth - tabsGap * 2, //left and right tabList margin + minTabWidth: 40, // Min-width of each tab, without xPadding + tabXPadding, + tabsGap, + font: tabFont, + }); const activeTab = otherProps?.activeElement ?? 0; @@ -68,36 +70,41 @@ export const TabsBarShape = forwardRef((props, ref) => { {/* Map through headerRow to create tabs */} - {tabLabels.map((header, index) => ( - - - - - ))} + {tabList.map(({ tab, width, xPos }, index) => { + return ( + + {/* || 0 Workaround to avoid thumbpage NaN issue with konva */} + + + + ); + })} ); }); 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 f7c516f6..edd483fe 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 @@ -8,7 +8,7 @@ import { BASIC_SHAPE } from '../front-components/shape.const'; import { useGroupShapeProps } from '../mock-components.utils'; const heading1SizeRestrictions: ShapeSizeRestrictions = { - minWidth: 150, + minWidth: 40, minHeight: 20, maxWidth: -1, maxHeight: -1, @@ -40,10 +40,8 @@ export const Heading1Shape = forwardRef((props, ref) => { ); const { width: restrictedWidth, height: restrictedHeight } = restrictedSize; - const { textColor, textDecoration, fontStyle, fontVariant } = useShapeProps( - otherProps, - BASIC_SHAPE - ); + const { textColor, textDecoration, fontStyle, fontVariant, fontSize } = + useShapeProps(otherProps, BASIC_SHAPE); const commonGroupProps = useGroupShapeProps( props, @@ -61,7 +59,7 @@ export const Heading1Shape = forwardRef((props, ref) => { height={restrictedHeight} text={text} fontFamily={BASIC_SHAPE.DEFAULT_FONT_FAMILY} - fontSize={30} + fontSize={fontSize} fill={textColor} align="center" verticalAlign="middle" diff --git a/src/common/components/mock-components/front-text-components/heading2-text-shape.tsx b/src/common/components/mock-components/front-text-components/heading2-text-shape.tsx index aeecde05..02d8184b 100644 --- a/src/common/components/mock-components/front-text-components/heading2-text-shape.tsx +++ b/src/common/components/mock-components/front-text-components/heading2-text-shape.tsx @@ -8,7 +8,7 @@ import { BASIC_SHAPE } from '../front-components/shape.const'; import { useGroupShapeProps } from '../mock-components.utils'; const heading2SizeRestrictions: ShapeSizeRestrictions = { - minWidth: 150, + minWidth: 40, minHeight: 20, maxWidth: -1, maxHeight: -1, @@ -40,10 +40,8 @@ export const Heading2Shape = forwardRef((props, ref) => { ); const { width: restrictedWidth, height: restrictedHeight } = restrictedSize; - const { textColor, fontVariant, fontStyle, textDecoration } = useShapeProps( - otherProps, - BASIC_SHAPE - ); + const { textColor, fontVariant, fontStyle, textDecoration, fontSize } = + useShapeProps(otherProps, BASIC_SHAPE); const commonGroupProps = useGroupShapeProps( props, @@ -61,7 +59,7 @@ export const Heading2Shape = forwardRef((props, ref) => { height={restrictedHeight} text={text} fontFamily={BASIC_SHAPE.DEFAULT_FONT_FAMILY} - fontSize={25} + fontSize={fontSize} fill={textColor} align="center" verticalAlign="middle" diff --git a/src/common/components/mock-components/front-text-components/heading3-text-shape.tsx b/src/common/components/mock-components/front-text-components/heading3-text-shape.tsx index 6a068cbf..4f02a862 100644 --- a/src/common/components/mock-components/front-text-components/heading3-text-shape.tsx +++ b/src/common/components/mock-components/front-text-components/heading3-text-shape.tsx @@ -8,7 +8,7 @@ import { useShapeProps } from '../../shapes/use-shape-props.hook'; import { useGroupShapeProps } from '../mock-components.utils'; const heading3SizeRestrictions: ShapeSizeRestrictions = { - minWidth: 150, + minWidth: 40, minHeight: 20, maxWidth: -1, maxHeight: -1, @@ -41,10 +41,8 @@ export const Heading3Shape = forwardRef((props, ref) => { const { width: restrictedWidth, height: restrictedHeight } = restrictedSize; - const { textColor, fontVariant, fontStyle, textDecoration } = useShapeProps( - otherProps, - BASIC_SHAPE - ); + const { textColor, fontVariant, fontStyle, textDecoration, fontSize } = + useShapeProps(otherProps, BASIC_SHAPE); const commonGroupProps = useGroupShapeProps( props, @@ -62,7 +60,7 @@ export const Heading3Shape = forwardRef((props, ref) => { height={restrictedHeight} text={text} fontFamily={BASIC_SHAPE.DEFAULT_FONT_FAMILY} - fontSize={20} + fontSize={fontSize} fill={textColor} align="center" verticalAlign="middle" diff --git a/src/common/components/mock-components/front-text-components/index.ts b/src/common/components/mock-components/front-text-components/index.ts index 8ef13f37..7accead3 100644 --- a/src/common/components/mock-components/front-text-components/index.ts +++ b/src/common/components/mock-components/front-text-components/index.ts @@ -4,3 +4,4 @@ export * from './heading3-text-shape'; export * from './normaltext-shape'; export * from './smalltext-shape'; export * from './paragraph-text-shape'; +export * from './link-text-shape'; diff --git a/src/common/components/mock-components/front-text-components/link-text-shape.tsx b/src/common/components/mock-components/front-text-components/link-text-shape.tsx new file mode 100644 index 00000000..e139ce70 --- /dev/null +++ b/src/common/components/mock-components/front-text-components/link-text-shape.tsx @@ -0,0 +1,77 @@ +import { forwardRef } from 'react'; +import { Group, Text } from 'react-konva'; +import { ShapeProps } from '../shape.model'; +import { ShapeSizeRestrictions, ShapeType } from '@/core/model'; +import { fitSizeToShapeSizeRestrictions } from '@/common/utils/shapes/shape-restrictions'; +import { BASIC_SHAPE } from '../front-components/shape.const'; +import { useShapeProps } from '../../shapes/use-shape-props.hook'; +import { useGroupShapeProps } from '../mock-components.utils'; + +const linkSizeRestrictions: ShapeSizeRestrictions = { + minWidth: 40, + minHeight: 20, + maxWidth: -1, + maxHeight: -1, + defaultWidth: 150, + defaultHeight: 25, +}; + +export const getLinkSizeRestrictions = (): ShapeSizeRestrictions => + linkSizeRestrictions; + +const shapeType: ShapeType = 'link'; + +export const LinkShape = forwardRef((props, ref) => { + const { + x, + y, + width, + height, + id, + onSelected, + text, + otherProps, + ...shapeProps + } = props; + const restrictedSize = fitSizeToShapeSizeRestrictions( + linkSizeRestrictions, + width, + height + ); + + const { width: restrictedWidth, height: restrictedHeight } = restrictedSize; + + const { textColor, textDecoration, fontSize } = useShapeProps( + otherProps, + BASIC_SHAPE + ); + + const commonGroupProps = useGroupShapeProps( + props, + restrictedSize, + shapeType, + ref + ); + + return ( + + + + ); +}); + +export default LinkShape; diff --git a/src/common/components/mock-components/front-text-components/normaltext-shape.tsx b/src/common/components/mock-components/front-text-components/normaltext-shape.tsx index 980dc489..6f3bd419 100644 --- a/src/common/components/mock-components/front-text-components/normaltext-shape.tsx +++ b/src/common/components/mock-components/front-text-components/normaltext-shape.tsx @@ -8,7 +8,7 @@ import { BASIC_SHAPE } from '../front-components/shape.const'; import { useGroupShapeProps } from '../mock-components.utils'; const normaltextSizeRestrictions: ShapeSizeRestrictions = { - minWidth: 150, + minWidth: 40, minHeight: 20, maxWidth: -1, maxHeight: 30, @@ -40,10 +40,8 @@ export const NormaltextShape = forwardRef((props, ref) => { ); const { width: restrictedWidth, height: restrictedHeight } = restrictedSize; - const { textColor, fontVariant, fontStyle, textDecoration } = useShapeProps( - otherProps, - BASIC_SHAPE - ); + const { textColor, fontVariant, fontStyle, textDecoration, fontSize } = + useShapeProps(otherProps, BASIC_SHAPE); const commonGroupProps = useGroupShapeProps( props, @@ -61,7 +59,7 @@ export const NormaltextShape = forwardRef((props, ref) => { height={restrictedHeight} text={text} fontFamily={BASIC_SHAPE.DEFAULT_FONT_FAMILY} - fontSize={18} + fontSize={fontSize} fill={textColor} align="center" verticalAlign="middle" 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 00824939..3d44c447 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 @@ -40,7 +40,7 @@ export const ParagraphShape = forwardRef((props, ref) => { ); const { width: restrictedWidth, height: restrictedHeight } = restrictedSize; - const { textColor } = useShapeProps(otherProps, BASIC_SHAPE); + const { textColor, fontSize } = useShapeProps(otherProps, BASIC_SHAPE); const commonGroupProps = useGroupShapeProps( props, @@ -58,7 +58,7 @@ export const ParagraphShape = forwardRef((props, ref) => { height={restrictedHeight} text={text} fontFamily={BASIC_SHAPE.DEFAULT_FONT_FAMILY} - fontSize={14} + fontSize={fontSize} fill={textColor} align="left" ellipsis={true} diff --git a/src/common/components/mock-components/front-text-components/smalltext-shape.tsx b/src/common/components/mock-components/front-text-components/smalltext-shape.tsx index aa392a5f..ec0393a2 100644 --- a/src/common/components/mock-components/front-text-components/smalltext-shape.tsx +++ b/src/common/components/mock-components/front-text-components/smalltext-shape.tsx @@ -40,10 +40,8 @@ export const SmalltextShape = forwardRef((props, ref) => { ); const { width: restrictedWidth, height: restrictedHeight } = restrictedSize; - const { textColor, fontVariant, fontStyle, textDecoration } = useShapeProps( - otherProps, - BASIC_SHAPE - ); + const { textColor, fontVariant, fontStyle, textDecoration, fontSize } = + useShapeProps(otherProps, BASIC_SHAPE); const commonGroupProps = useGroupShapeProps( props, @@ -61,7 +59,7 @@ export const SmalltextShape = forwardRef((props, ref) => { height={restrictedHeight} text={text} fontFamily={BASIC_SHAPE.DEFAULT_FONT_FAMILY} - fontSize={14} + fontSize={fontSize} fill={textColor} align="center" verticalAlign="middle" diff --git a/src/common/components/shapes/use-shape-props.hook.ts b/src/common/components/shapes/use-shape-props.hook.ts index 99625bb8..1a68f6d9 100644 --- a/src/common/components/shapes/use-shape-props.hook.ts +++ b/src/common/components/shapes/use-shape-props.hook.ts @@ -32,6 +32,11 @@ export const useShapeProps = ( [otherProps?.fontStyle] ); + const fontSize = useMemo( + () => otherProps?.fontSize ?? defaultStyleShape.DEFAULT_FONT_SIZE, + [otherProps?.fontSize] + ); + const textDecoration = useMemo( () => otherProps?.textDecoration ?? defaultStyleShape.DEFAULT_TEXT_DECORATION, @@ -74,6 +79,7 @@ export const useShapeProps = ( selectedBackgroundColor, fontVariant, fontStyle, + fontSize, textDecoration, }; }; diff --git a/src/core/local-disk/shapes-to-document.mapper.ts b/src/core/local-disk/shapes-to-document.mapper.ts index 2d01dbbe..d34a23c0 100644 --- a/src/core/local-disk/shapes-to-document.mapper.ts +++ b/src/core/local-disk/shapes-to-document.mapper.ts @@ -1,36 +1,118 @@ +import { FONT_SIZE_VALUES } from '@/common/components/mock-components/front-components/shape.const'; import { ShapeModel } from '../model'; import { DocumentModel } from '../providers/canvas/canvas.model'; -import { QuickMockFileContract, Page } from './local-disk.model'; +import { QuickMockFileContract } from './local-disk.model'; export const mapFromShapesArrayToQuickMockFileDocument = ( - shapes: ShapeModel[] + fullDocument: DocumentModel ): QuickMockFileContract => { - const pages: Page[] = shapes.reduce((acc, shape) => { - /* - * TODO: Add the correct id, name and version values. - */ - const newPage: Page = { - id: '1', - name: 'default', - shapes: [{ ...shape }], - }; - - return [...acc, newPage]; - }, [] as Page[]); - + // TODO: Serialize the activePageIndex? return { - version: '0.1', - pages, + version: '0.2', + pages: fullDocument.pages, }; }; export const mapFromQuickMockFileDocumentToApplicationDocument = ( fileDocument: QuickMockFileContract ): DocumentModel => { - const shapes: ShapeModel[] = fileDocument.pages.reduce((acc, page) => { - return [...acc, ...page.shapes]; - }, [] as ShapeModel[]); return { - shapes, + activePageIndex: 0, + pages: fileDocument.pages, + }; +}; + +const mapTextElementFromV0_1ToV0_2 = (shape: ShapeModel): ShapeModel => { + switch (shape.type) { + case 'heading1': + return { + ...shape, + otherProps: { + ...shape, + fontSize: FONT_SIZE_VALUES.HEADING1, + }, + }; + case 'heading2': + return { + ...shape, + otherProps: { + ...shape, + fontSize: FONT_SIZE_VALUES.HEADING2, + }, + }; + case 'heading3': + return { + ...shape, + otherProps: { + ...shape, + fontSize: FONT_SIZE_VALUES.HEADING3, + }, + }; + case 'link': + return { + ...shape, + otherProps: { + ...shape, + fontSize: FONT_SIZE_VALUES.LINK, + }, + }; + case 'normaltext': + return { + ...shape, + otherProps: { + ...shape, + fontSize: FONT_SIZE_VALUES.NORMALTEXT, + }, + }; + case 'smalltext': + return { + ...shape, + otherProps: { + ...shape, + fontSize: FONT_SIZE_VALUES.SMALLTEXT, + }, + }; + case 'paragraph': + return { + ...shape, + otherProps: { + ...shape, + fontSize: FONT_SIZE_VALUES.PARAGRAPH, + }, + }; + default: + return shape; + } +}; + +const setTextElementsDefaultFontSizeV0_1 = ( + shapes: ShapeModel[] +): ShapeModel[] => { + return shapes.map(mapTextElementFromV0_1ToV0_2); +}; + +// Example function to handle version 0.1 parsing +export const mapFromQuickMockFileDocumentToApplicationDocumentV0_1 = ( + fileDocument: QuickMockFileContract +): DocumentModel => { + // Combine all shapes into a single page + let combinedShapes: ShapeModel[] = fileDocument.pages.reduce( + (acc: ShapeModel[], page) => { + return acc.concat(page.shapes); + }, + [] + ); + + combinedShapes = setTextElementsDefaultFontSizeV0_1(combinedShapes); + + return { + activePageIndex: 0, + pages: [ + { + id: '1', + name: 'default', + shapes: combinedShapes, + }, + ], }; }; diff --git a/src/core/local-disk/shapes-to.document.mapper.spec.ts b/src/core/local-disk/shapes-to.document.mapper.spec.ts index f5c943cb..f5b713af 100644 --- a/src/core/local-disk/shapes-to.document.mapper.spec.ts +++ b/src/core/local-disk/shapes-to.document.mapper.spec.ts @@ -1,6 +1,7 @@ import { mapFromShapesArrayToQuickMockFileDocument, mapFromQuickMockFileDocumentToApplicationDocument, + mapFromQuickMockFileDocumentToApplicationDocumentV0_1, } from './shapes-to-document.mapper'; import { ShapeModel } from '../model'; import { QuickMockFileContract } from './local-disk.model'; @@ -8,20 +9,6 @@ import { DocumentModel } from '../providers/canvas/canvas.model'; describe('shapes to document mapper', () => { describe('mapFromShapesArrayToQuickMockFileDocument', () => { - it('Should return a ShapeModel with empty pages', () => { - // Arrange - const shapes: ShapeModel[] = []; - const expectedResult: QuickMockFileContract = { - version: '0.1', - pages: [], - }; - // Act - const result = mapFromShapesArrayToQuickMockFileDocument(shapes); - - // Assert - expect(result).toEqual(expectedResult); - }); - it('Should return a ShapeModel with one pages and shapes', () => { // Arrange const shapes: ShapeModel[] = [ @@ -36,29 +23,30 @@ describe('shapes to document mapper', () => { typeOfTransformer: ['rotate'], }, ]; + + const document: DocumentModel = { + activePageIndex: 0, + pages: [ + { + id: '1', + name: 'default', + shapes: shapes, + }, + ], + }; + const expectedResult: QuickMockFileContract = { - version: '0.1', + version: '0.2', pages: [ { id: '1', name: 'default', - shapes: [ - { - id: '1', - x: 0, - y: 0, - width: 100, - height: 100, - type: 'rectangle', - allowsInlineEdition: false, - typeOfTransformer: ['rotate'], - }, - ], + shapes: shapes, }, ], }; // Act - const result = mapFromShapesArrayToQuickMockFileDocument(shapes); + const result = mapFromShapesArrayToQuickMockFileDocument(document); // Assert expect(result).toEqual(expectedResult); @@ -88,45 +76,31 @@ describe('shapes to document mapper', () => { typeOfTransformer: ['rotate'], }, ]; - const expectedResult: QuickMockFileContract = { - version: '0.1', + + const document: DocumentModel = { + activePageIndex: 0, pages: [ { id: '1', name: 'default', - shapes: [ - { - id: '1', - x: 0, - y: 0, - width: 100, - height: 100, - type: 'rectangle', - allowsInlineEdition: false, - typeOfTransformer: ['rotate'], - }, - ], + shapes: shapes, }, + ], + }; + + const expectedResult: QuickMockFileContract = { + version: '0.2', + pages: [ { id: '1', name: 'default', - shapes: [ - { - id: '2', - x: 0, - y: 0, - width: 100, - height: 100, - type: 'circle', - allowsInlineEdition: true, - typeOfTransformer: ['rotate'], - }, - ], + shapes: shapes, }, ], }; + // Act - const result = mapFromShapesArrayToQuickMockFileDocument(shapes); + const result = mapFromShapesArrayToQuickMockFileDocument(document); // Assert expect(result).toEqual(expectedResult); @@ -137,11 +111,25 @@ describe('shapes to document mapper', () => { it('Should return a document model with a empty shapes array when we feed a empty pages array', () => { //arrange const fileDocument: QuickMockFileContract = { - version: '0.1', - pages: [], + version: '0.2', + pages: [ + { + id: '1', + name: 'default', + shapes: [], + }, + ], }; + const expectedResult: DocumentModel = { - shapes: [], + activePageIndex: 0, + pages: [ + { + id: '1', + name: 'default', + shapes: [], + }, + ], }; //act const result = @@ -153,7 +141,7 @@ describe('shapes to document mapper', () => { it('Should return a document model with a empty shapes array when we feed a file document with a one page but with empty shapes', () => { //arrange const fileDocument: QuickMockFileContract = { - version: '0.1', + version: '0.2', pages: [ { id: '1', @@ -162,9 +150,18 @@ describe('shapes to document mapper', () => { }, ], }; + const expectedResult: DocumentModel = { - shapes: [], + activePageIndex: 0, + pages: [ + { + id: '1', + name: 'default', + shapes: [], + }, + ], }; + //act const result = mapFromQuickMockFileDocumentToApplicationDocument(fileDocument); @@ -195,20 +192,29 @@ describe('shapes to document mapper', () => { }, ], }; + const expectedResult: DocumentModel = { - shapes: [ + activePageIndex: 0, + pages: [ { id: '1', - type: 'rectangle', - x: 0, - y: 0, - width: 100, - height: 100, - allowsInlineEdition: false, - typeOfTransformer: ['rotate'], + name: 'default', + shapes: [ + { + id: '1', + type: 'rectangle', + x: 0, + y: 0, + width: 100, + height: 100, + allowsInlineEdition: false, + typeOfTransformer: ['rotate'], + }, + ], }, ], }; + //act const result = mapFromQuickMockFileDocumentToApplicationDocument(fileDocument); @@ -218,72 +224,131 @@ describe('shapes to document mapper', () => { it('Should return a document model with shapes when we feed a file document with two pages and shapes', () => { //arrange + const shapespageA: ShapeModel[] = [ + { + id: '1', + type: 'rectangle', + x: 0, + y: 0, + width: 100, + height: 100, + allowsInlineEdition: false, + typeOfTransformer: ['rotate'], + }, + ]; + + const shapesPageB: ShapeModel[] = [ + { + id: '3', + type: 'browser', + x: 0, + y: 0, + width: 100, + height: 100, + allowsInlineEdition: true, + typeOfTransformer: [' rotate'], + }, + ]; + const fileDocument: QuickMockFileContract = { version: '0.1', pages: [ { id: '1', name: 'default', - shapes: [ - { - id: '1', - type: 'rectangle', - x: 0, - y: 0, - width: 100, - height: 100, - allowsInlineEdition: false, - typeOfTransformer: ['rotate'], - }, - ], + shapes: shapespageA, }, { id: '2', name: 'default', - shapes: [ - { - id: '3', - type: 'browser', - x: 0, - y: 0, - width: 100, - height: 100, - allowsInlineEdition: true, - typeOfTransformer: [' rotate'], - }, - ], + shapes: shapesPageB, }, ], }; + const expectedResult: DocumentModel = { - shapes: [ + activePageIndex: 0, + pages: [ { id: '1', - type: 'rectangle', - x: 0, - y: 0, - width: 100, - height: 100, - allowsInlineEdition: false, - typeOfTransformer: ['rotate'], + name: 'default', + shapes: shapespageA, }, { - id: '3', - type: 'browser', - x: 0, - y: 0, - width: 100, - height: 100, - allowsInlineEdition: true, - typeOfTransformer: [' rotate'], + id: '2', + name: 'default', + shapes: shapesPageB, }, ], }; + //act const result = mapFromQuickMockFileDocumentToApplicationDocument(fileDocument); //assert expect(result).toEqual(expectedResult); }); + + it('Should return a document model with shapes in one page when we feed a file document from version 0.1', () => { + //arrange + const shapespageA: ShapeModel[] = [ + { + id: '1', + type: 'rectangle', + x: 0, + y: 0, + width: 100, + height: 100, + allowsInlineEdition: false, + typeOfTransformer: ['rotate'], + }, + ]; + + const shapesPageB: ShapeModel[] = [ + { + id: '3', + type: 'browser', + x: 0, + y: 0, + width: 100, + height: 100, + allowsInlineEdition: true, + typeOfTransformer: [' rotate'], + }, + ]; + + const fileDocument: QuickMockFileContract = { + version: '0.1', + pages: [ + { + id: '1', + name: 'default', + shapes: shapespageA, + }, + { + id: '2', + name: 'default', + shapes: shapesPageB, + }, + ], + }; + + const expectedResult: DocumentModel = { + activePageIndex: 0, + pages: [ + { + id: '1', + name: 'default', + shapes: shapespageA.concat(shapesPageB), + }, + ], + }; + + //act + const result = + mapFromQuickMockFileDocumentToApplicationDocumentV0_1(fileDocument); + //assert + expect(result).toEqual(expectedResult); + }); }); }); diff --git a/src/core/local-disk/use-local-disk.hook.ts b/src/core/local-disk/use-local-disk.hook.ts index bdb71bc7..fbcb3f18 100644 --- a/src/core/local-disk/use-local-disk.hook.ts +++ b/src/core/local-disk/use-local-disk.hook.ts @@ -3,6 +3,7 @@ import { useCanvasContext } from '../providers'; import { mapFromShapesArrayToQuickMockFileDocument, mapFromQuickMockFileDocumentToApplicationDocument, + mapFromQuickMockFileDocumentToApplicationDocumentV0_1, } from './shapes-to-document.mapper'; import { fileInput, OnFileSelectedCallback } from '@/common/file-input'; import { QuickMockFileContract } from './local-disk.model'; @@ -12,10 +13,12 @@ const DEFAULT_FILE_EXTENSION = 'qm'; const DEFAULT_EXTENSION_DESCRIPTION = 'quick mock'; export const useLocalDisk = () => { - const { shapes, loadDocument, fileName, setFileName } = useCanvasContext(); + const { fullDocument, loadDocument, fileName, setFileName } = + useCanvasContext(); const serializeShapes = (): string => { - const quickMockDocument = mapFromShapesArrayToQuickMockFileDocument(shapes); + const quickMockDocument = + mapFromShapesArrayToQuickMockFileDocument(fullDocument); return JSON.stringify(quickMockDocument); }; @@ -55,9 +58,17 @@ export const useLocalDisk = () => { reader.onload = () => { const content = reader.result as string; const parseData: QuickMockFileContract = JSON.parse(content); - const appDocument = - mapFromQuickMockFileDocumentToApplicationDocument(parseData); - loadDocument(appDocument); + if (parseData.version === '0.1') { + // Handle version 0.1 parsing + const appDocument = + mapFromQuickMockFileDocumentToApplicationDocumentV0_1(parseData); + loadDocument(appDocument); + } else { + // Handle other versions + const appDocument = + mapFromQuickMockFileDocumentToApplicationDocument(parseData); + loadDocument(appDocument); + } }; reader.readAsText(file); }; diff --git a/src/core/model/index.ts b/src/core/model/index.ts index 4ade6751..843d0979 100644 --- a/src/core/model/index.ts +++ b/src/core/model/index.ts @@ -61,12 +61,14 @@ export type ShapeType = | 'verticalScrollBar' | 'horizontalScrollBar' | 'modal' + | 'modalCover' | 'tabsBar' | 'appBar' | 'appBar' | 'buttonBar' | 'tooltip' - | 'slider'; + | 'slider' + | 'link'; export const ShapeDisplayName: Record = { multiple: 'multiple', @@ -106,6 +108,7 @@ export const ShapeDisplayName: Record = { normaltext: 'Normal text', smalltext: 'Small text', paragraph: 'Paragraph', + link: 'Link', triangle: 'Triangle', 'horizontal-menu': 'Horizontal Menu', largeArrow: 'Large Arrow', @@ -118,6 +121,7 @@ export const ShapeDisplayName: Record = { calendar: 'Calendar', verticalScrollBar: 'Vertical Scroll Bar', modal: 'Modal', + modalCover: 'Modal Cover', tabsBar: 'Tabs Bar', appBar: 'AppBar', buttonBar: 'Button Bar', @@ -159,6 +163,7 @@ export interface OtherProps { textColor?: string; fontVariant?: string; fontStyle?: string; + fontSize?: number; textDecoration?: string; checked?: boolean; icon?: IconInfo; diff --git a/src/core/providers/canvas/canvas.business.ts b/src/core/providers/canvas/canvas.business.ts index 2458e7db..be5a6b9e 100644 --- a/src/core/providers/canvas/canvas.business.ts +++ b/src/core/providers/canvas/canvas.business.ts @@ -1,4 +1,5 @@ import { ShapeModel } from '@/core/model'; +import { DocumentModel } from './canvas.model'; export const removeShapesFromList = ( shapeIds: string[], @@ -10,3 +11,18 @@ export const removeShapesFromList = ( return shapeCollection.filter(shape => !shapeIds.includes(shape.id)); }; + +export const isPageIndexValid = (document: DocumentModel) => { + return ( + document.activePageIndex !== -1 && + document.activePageIndex < document.pages.length + ); +}; + +export const getActivePageShapes = (document: DocumentModel) => { + if (!isPageIndexValid(document)) { + return []; + } + + return document.pages[document.activePageIndex].shapes; +}; diff --git a/src/core/providers/canvas/canvas.model.ts b/src/core/providers/canvas/canvas.model.ts index 3807f144..e0e2e503 100644 --- a/src/core/providers/canvas/canvas.model.ts +++ b/src/core/providers/canvas/canvas.model.ts @@ -11,6 +11,28 @@ import { Node, NodeConfig } from 'konva/lib/Node'; export type ZIndexAction = 'top' | 'bottom' | 'up' | 'down'; +export interface Page { + id: string; + name: string; + shapes: ShapeModel[]; +} + +export interface DocumentModel { + pages: Page[]; + activePageIndex: number; +} + +export const createDefaultDocumentModel = (): DocumentModel => ({ + activePageIndex: 0, + pages: [ + { + id: '1', + name: 'Page 1', + shapes: [], + }, + ], +}); + export interface SelectionInfo { transformerRef: React.RefObject; shapeRefs: React.MutableRefObject; @@ -24,6 +46,7 @@ export interface SelectionInfo { | Konva.KonvaEventObject | Konva.KonvaEventObject ) => void; + clearSelection: () => void; selectedShapesRefs: React.MutableRefObject[] | null>; selectedShapesIds: string[]; selectedShapeType: ShapeType | null; @@ -40,7 +63,7 @@ export interface SelectionInfo { export interface CanvasContextModel { shapes: ShapeModel[]; scale: number; - clearCanvas: () => void; + createNewFullDocument: () => void; setScale: React.Dispatch>; addNewShape: ( type: ShapeType, @@ -71,12 +94,17 @@ export interface CanvasContextModel { setIsInlineEditing: React.Dispatch>; fileName: string; setFileName: (fileName: string) => void; + fullDocument: DocumentModel; + addNewPage: () => void; + duplicatePage: (pageIndex: number) => void; + getActivePage: () => Page; + setActivePage: (pageId: string) => void; + deletePage: (pageIndex: number) => void; + editPageTitle: (pageIndex: number, newName: string) => void; + swapPages: (id1: string, id2: string) => void; + activePageIndex: number; + isThumbnailContextMenuVisible: boolean; + setIsThumbnailContextMenuVisible: React.Dispatch< + React.SetStateAction + >; } - -export interface DocumentModel { - shapes: ShapeModel[]; -} - -export const createDefaultDocumentModel = (): DocumentModel => ({ - shapes: [], -}); diff --git a/src/core/providers/canvas/canvas.provider.tsx b/src/core/providers/canvas/canvas.provider.tsx index 6248d1f3..dea96574 100644 --- a/src/core/providers/canvas/canvas.provider.tsx +++ b/src/core/providers/canvas/canvas.provider.tsx @@ -8,8 +8,9 @@ import { useStateWithInterceptor } from './canvas.hook'; import { createDefaultDocumentModel, DocumentModel } from './canvas.model'; import { v4 as uuidv4 } from 'uuid'; import Konva from 'konva'; -import { removeShapesFromList } from './canvas.business'; +import { isPageIndexValid, removeShapesFromList } from './canvas.business'; import { useClipboard } from './use-clipboard.hook'; +import { produce } from 'immer'; interface Props { children: React.ReactNode; @@ -22,6 +23,8 @@ export const CanvasProvider: React.FC = props => { const stageRef = React.useRef(null); const [isInlineEditing, setIsInlineEditing] = React.useState(false); const [fileName, setFileName] = React.useState(''); + const [isThumbnailContextMenuVisible, setIsThumbnailContextMenuVisible] = + React.useState(false); const { addSnapshot, @@ -40,16 +43,113 @@ export const CanvasProvider: React.FC = props => { const selectionInfo = useSelection(document, setDocument); + const addNewPage = () => { + setDocument(lastDocument => + produce(lastDocument, draft => { + const newActiveIndex = draft.pages.length; + draft.pages.push({ + id: uuidv4(), + name: `Page ${newActiveIndex + 1}`, + shapes: [], + }); + draft.activePageIndex = newActiveIndex; + }) + ); + }; + + const duplicatePage = (pageIndex: number) => { + const newShapes: ShapeModel[] = document.pages[pageIndex].shapes.map( + shape => { + const newShape: ShapeModel = { ...shape }; + newShape.id = uuidv4(); + return newShape; + } + ); + + setDocument(lastDocument => + produce(lastDocument, draft => { + const newPage = { + id: uuidv4(), + name: `${document.pages[pageIndex].name} - copy`, + shapes: newShapes, + }; + const newIndex = draft.activePageIndex + 1; + draft.pages.splice(newIndex, 0, newPage); + draft.activePageIndex = newIndex; + }) + ); + }; + + const deletePage = (pageIndex: number) => { + const newActivePageId = + pageIndex < document.pages.length - 1 + ? document.pages[pageIndex + 1].id // If it's not the last page, select the next one + : document.pages[pageIndex - 1].id; // Otherwise, select the previous one + + setDocument(lastDocument => + produce(lastDocument, draft => { + draft.pages = draft.pages.filter( + currentPage => document.pages[pageIndex].id !== currentPage.id + ); + }) + ); + + setActivePage(newActivePageId); + }; + + const getActivePage = () => { + return document.pages[document.activePageIndex]; + }; + + const setActivePage = (pageId: string) => { + selectionInfo.clearSelection(); + selectionInfo.shapeRefs.current = {}; + + setDocument(lastDocument => + produce(lastDocument, draft => { + const pageIndex = draft.pages.findIndex(page => page.id === pageId); + if (pageIndex !== -1) { + draft.activePageIndex = pageIndex; + } + }) + ); + }; + + const editPageTitle = (pageIndex: number, newName: string) => { + setDocument(lastDocument => + produce(lastDocument, draft => { + draft.pages[pageIndex].name = newName; + }) + ); + }; + + const swapPages = (id1: string, id2: string) => { + setDocument(lastDocument => + produce(lastDocument, draft => { + const index1 = draft.pages.findIndex(page => page.id === id1); + const index2 = draft.pages.findIndex(page => page.id === id2); + if (index1 !== -1 && index2 !== -1) { + const temp = draft.pages[index1]; + draft.pages[index1] = draft.pages[index2]; + draft.pages[index2] = temp; + } + }) + ); + }; + const pasteShapes = (shapes: ShapeModel[]) => { const newShapes: ShapeModel[] = shapes.map(shape => { shape.id = uuidv4(); return shape; }); - setDocument(prevDocument => ({ - ...prevDocument, - shapes: [...prevDocument.shapes, ...newShapes], - })); + if (isPageIndexValid(document)) { + setDocument(lastDocument => + produce(lastDocument, draft => { + draft.pages[lastDocument.activePageIndex].shapes.push(...newShapes); + }) + ); + } // Just select the new pasted shapes // need to wait for the shapes to be rendered (previous set document is async) @@ -71,20 +171,28 @@ export const CanvasProvider: React.FC = props => { }; const { copyShapeToClipboard, pasteShapeFromClipboard, canCopy, canPaste } = - useClipboard(pasteShapes, document.shapes, selectionInfo); + useClipboard( + pasteShapes, + document.pages[document.activePageIndex].shapes, + selectionInfo + ); - const clearCanvas = () => { - setDocument({ shapes: [] }); + const createNewFullDocument = () => { + setDocument(createDefaultDocumentModel()); }; const deleteSelectedShapes = () => { - setDocument(prevDocument => ({ - ...prevDocument, - shapes: removeShapesFromList( - selectionInfo.selectedShapesIds, - prevDocument.shapes - ), - })); + if (isPageIndexValid(document)) { + setDocument(lastDocument => + produce(lastDocument, draft => { + draft.pages[lastDocument.activePageIndex].shapes = + removeShapesFromList( + selectionInfo.selectedShapesIds, + draft.pages[lastDocument.activePageIndex].shapes + ); + }) + ); + } }; // TODO: instenad of x,y use Coord and reduce the number of arguments @@ -94,14 +202,17 @@ export const CanvasProvider: React.FC = props => { y: number, otherProps?: OtherProps ) => { + if (!isPageIndexValid(document)) { + return ''; + } + const newShape = createShape({ x, y }, type, otherProps); - setDocument(({ shapes }) => { - const newShapes = [...shapes, newShape]; - return { - shapes: newShapes, - }; - }); + setDocument(lastDocument => + produce(lastDocument, draft => { + draft.pages[lastDocument.activePageIndex].shapes.push(newShape); + }) + ); return newShape.id; }; @@ -112,27 +223,43 @@ export const CanvasProvider: React.FC = props => { size: Size, skipHistory: boolean = false ) => { + if (!isPageIndexValid(document)) { + return; + } + if (skipHistory) { - setShapesSkipHistory(({ shapes }) => ({ - shapes: shapes.map(shape => - shape.id === id ? { ...shape, ...position, ...size } : shape - ), - })); + setShapesSkipHistory(fullDocument => { + return produce(fullDocument, draft => { + draft.pages[document.activePageIndex].shapes = draft.pages[ + document.activePageIndex + ].shapes.map(shape => + shape.id === id ? { ...shape, ...position, ...size } : shape + ); + }); + }); } else { - setDocument(({ shapes }) => ({ - shapes: shapes.map(shape => - shape.id === id ? { ...shape, ...position, ...size } : shape - ), - })); + setDocument(fullDocument => { + return produce(fullDocument, draft => { + draft.pages[document.activePageIndex].shapes = draft.pages[ + document.activePageIndex + ].shapes.map(shape => + shape.id === id ? { ...shape, ...position, ...size } : shape + ); + }); + }); } }; const updateShapePosition = (id: string, { x, y }: Coord) => { - setDocument(({ shapes }) => ({ - shapes: shapes.map(shape => - shape.id === id ? { ...shape, x, y } : shape - ), - })); + if (isPageIndexValid(document)) { + setDocument(fullDocument => { + return produce(fullDocument, draft => { + draft.pages[document.activePageIndex].shapes = draft.pages[ + document.activePageIndex + ].shapes.map(shape => (shape.id === id ? { ...shape, x, y } : shape)); + }); + }); + } }; const doUndo = () => { @@ -164,10 +291,10 @@ export const CanvasProvider: React.FC = props => { return ( = props => { setIsInlineEditing, fileName, setFileName, + fullDocument: document, + addNewPage, + duplicatePage, + getActivePage, + setActivePage, + deletePage, + editPageTitle, + swapPages, + activePageIndex: document.activePageIndex, + isThumbnailContextMenuVisible, + setIsThumbnailContextMenuVisible, }} > {children} diff --git a/src/core/providers/canvas/use-selection.hook.ts b/src/core/providers/canvas/use-selection.hook.ts index e8c491c3..e5ca7ac9 100644 --- a/src/core/providers/canvas/use-selection.hook.ts +++ b/src/core/providers/canvas/use-selection.hook.ts @@ -3,6 +3,8 @@ import Konva from 'konva'; import { OtherProps, ShapeModel, ShapeRefs, ShapeType } from '@/core/model'; import { DocumentModel, SelectionInfo, ZIndexAction } from './canvas.model'; import { performZIndexAction } from './zindex.util'; +import { getActivePageShapes, isPageIndexValid } from './canvas.business'; +import { produce } from 'immer'; export const useSelection = ( document: DocumentModel, @@ -28,7 +30,11 @@ export const useSelection = ( // Remove unused shapes and reset selectedShapeId if it no longer exists useEffect(() => { - const shapes = document.shapes; + if (!isPageIndexValid(document)) { + return; + } + + const shapes = getActivePageShapes(document); const currentIds = shapes.map(shape => shape.id); // 1. First cleanup Refs, let's get the list of shape and if there are any @@ -50,7 +56,7 @@ export const useSelection = ( setSelectedShapeType(null); } } - }, [document.shapes, selectedShapesIds]); + }, [document.pages, selectedShapesIds]); const isDeselectSingleItem = (arrayIds: string[]) => { return ( @@ -81,11 +87,17 @@ export const useSelection = ( type: ShapeType, isUserDoingMultipleSelection: boolean ) => { + // When chaging active pages, the refs are not yet updated + // check if this is something temporary or final solution + if (Object.keys(shapeRefs.current).length === 0) { + return; + } + // I want to know if the ids is string or array const arrayIds = typeof ids === 'string' ? [ids] : ids; if (!isUserDoingMultipleSelection) { - // No multiple selectio, just replace selection with current selected item(s) + // No multiple selection, just replace selection with current selected item(s) selectedShapesRefs.current = arrayIds.map( id => shapeRefs.current[id].current ); @@ -113,41 +125,55 @@ export const useSelection = ( setSelectedShapeType(type); }; + const clearSelection = () => { + transformerRef.current?.nodes([]); + selectedShapesRefs.current = []; + setSelectedShapesIds([]); + setSelectedShapeType(null); + }; + const handleClearSelection = ( mouseEvent?: | Konva.KonvaEventObject | Konva.KonvaEventObject ) => { if (!mouseEvent || mouseEvent.target === mouseEvent.target.getStage()) { - transformerRef.current?.nodes([]); - selectedShapesRefs.current = []; - setSelectedShapesIds([]); - setSelectedShapeType(null); + clearSelection(); } }; const setZIndexOnSelected = (action: ZIndexAction) => { - setDocument(prevDocument => ({ - shapes: performZIndexAction( - selectedShapesIds, - action, - prevDocument.shapes - ), - })); + if (!isPageIndexValid(document)) return; + + setDocument(prevDocument => + produce(prevDocument, draft => { + draft.pages[prevDocument.activePageIndex].shapes = performZIndexAction( + selectedShapesIds, + action, + getActivePageShapes(prevDocument) + ); + }) + ); }; const updateTextOnSelected = (text: string) => { + if (!isPageIndexValid(document)) return; + // Only when selection is one if (selectedShapesIds.length !== 1) { return; } const selectedShapeId = selectedShapesIds[0]; - setDocument(prevDocument => ({ - shapes: prevDocument.shapes.map(shape => - shape.id === selectedShapeId ? { ...shape, text } : shape - ), - })); + setDocument(prevDocument => + produce(prevDocument, draft => { + draft.pages[prevDocument.activePageIndex].shapes = draft.pages[ + prevDocument.activePageIndex + ].shapes.map(shape => + shape.id === selectedShapeId ? { ...shape, text } : shape + ); + }) + ); }; // TODO: Rather implement this using immmer @@ -156,6 +182,8 @@ export const useSelection = ( key: K, value: OtherProps[K] ) => { + if (!isPageIndexValid(document)) return; + // TODO: Right now applying this only to single selection // in the future we could apply to all selected shapes // BUT, we have to show only common shapes (pain in the neck) @@ -165,13 +193,18 @@ export const useSelection = ( } const selectedShapeId = selectedShapesIds[0]; - setDocument(prevDocument => ({ - shapes: prevDocument.shapes.map(shape => - shape.id === selectedShapeId - ? { ...shape, otherProps: { ...shape.otherProps, [key]: value } } - : shape - ), - })); + + setDocument(prevDocument => + produce(prevDocument, draft => { + draft.pages[prevDocument.activePageIndex].shapes = draft.pages[ + prevDocument.activePageIndex + ].shapes.map(shape => + shape.id === selectedShapeId + ? { ...shape, otherProps: { ...shape.otherProps, [key]: value } } + : shape + ); + }) + ); }; // Added index, right now we got multiple selection @@ -187,7 +220,9 @@ export const useSelection = ( const selectedShapeId = selectedShapesIds[index]; - return document.shapes.find(shape => shape.id === selectedShapeId); + return getActivePageShapes(document).find( + shape => shape.id === selectedShapeId + ); }; return { @@ -195,6 +230,7 @@ export const useSelection = ( shapeRefs, handleSelected, handleClearSelection, + clearSelection, selectedShapesRefs, selectedShapesIds, selectedShapeType, diff --git a/src/pods/about/members.ts b/src/pods/about/members.ts index 39bf23b1..4524b760 100644 --- a/src/pods/about/members.ts +++ b/src/pods/about/members.ts @@ -99,6 +99,30 @@ export const memberList: Member[] = [ { id: '13', + name: 'Jorge', + surname: 'Miranda de la Quintana', + urlLinkedin: + 'https://www.linkedin.com/in/jorge-miranda-de-la-quintana-65049b272/', + image: './assets/jorge-miranda.jpeg', + }, + + { + id: '14', + name: 'Josemi', + surname: 'Toribio', + urlLinkedin: 'https://www.linkedin.com/in/josemitoribiocastro/', + image: './assets/josemi-toribio.jpeg', + }, + { + id: '15', + name: 'Alberto', + surname: 'Escribano', + urlLinkedin: 'https://www.linkedin.com/in/alberto-escribano/', + image: './assets/alberto-escribano.jpeg', + }, + + { + id: '16', name: 'Gabriel', surname: 'Ionut', urlLinkedin: 'https://www.linkedin.com/in/gabriel-ionut-birsan-b14816307/', @@ -106,7 +130,7 @@ export const memberList: Member[] = [ }, { - id: '14', + id: '17', name: 'Antonio', surname: 'Contreras', urlLinkedin: @@ -115,7 +139,7 @@ export const memberList: Member[] = [ }, { - id: '15', + id: '18', name: 'Braulio', surname: 'Diez', urlLinkedin: 'https://www.linkedin.com/in/brauliodiez/', diff --git a/src/pods/canvas/canvas.pod.tsx b/src/pods/canvas/canvas.pod.tsx index 9363dd00..930989ff 100644 --- a/src/pods/canvas/canvas.pod.tsx +++ b/src/pods/canvas/canvas.pod.tsx @@ -132,6 +132,7 @@ export const CanvasPod = () => { onMouseDown={handleMouseDown} onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} + id="konva-stage" // data-id did not work for some reason > { diff --git a/src/pods/canvas/model/inline-editable.model.ts b/src/pods/canvas/model/inline-editable.model.ts index d7ef916e..90246131 100644 --- a/src/pods/canvas/model/inline-editable.model.ts +++ b/src/pods/canvas/model/inline-editable.model.ts @@ -19,6 +19,7 @@ const inlineEditableShapes = new Set([ 'heading1', 'heading2', 'heading3', + 'link', 'normaltext', 'smalltext', 'paragraph', @@ -30,7 +31,9 @@ const inlineEditableShapes = new Set([ 'buttonBar', 'tabsBar', 'tooltip', + 'timepickerinput', 'datepickerinput', + 'browser', ]); // Check if a shape type allows inline editing @@ -65,7 +68,10 @@ const shapeTypesWithDefaultText = new Set([ 'appBar', 'buttonBar', 'tabsBar', + 'link', + 'timepickerinput', 'datepickerinput', + 'browser', ]); // Map of ShapeTypes to their default text values @@ -97,7 +103,10 @@ const defaultTextValueMap: Partial> = { appBar: 'AppBar', buttonBar: 'Button 1, Button 2, Button 3', tabsBar: 'Tab 1, Tab 2, Tab 3', + link: 'Link', + timepickerinput: 'hh:mm', datepickerinput: new Date().toLocaleDateString(), + browser: 'https://example.com', }; export const generateDefaultTextValue = ( diff --git a/src/pods/canvas/model/shape-other-props.utils.ts b/src/pods/canvas/model/shape-other-props.utils.ts index 8935e3cb..afff3625 100644 --- a/src/pods/canvas/model/shape-other-props.utils.ts +++ b/src/pods/canvas/model/shape-other-props.utils.ts @@ -1,6 +1,8 @@ import { INPUT_SHAPE, BASIC_SHAPE, + FONT_SIZE_VALUES, + LINK_SHAPE, } from '@/common/components/mock-components/front-components/shape.const'; import { ShapeType, OtherProps } from '@/core/model'; @@ -123,6 +125,7 @@ export const generateDefaultOtherProps = ( fontVariant: `${INPUT_SHAPE.DEFAULT_FONT_VARIANT}`, fontStyle: `${INPUT_SHAPE.DEFAULT_FONT_STYLE}`, textDecoration: `${INPUT_SHAPE.DEFAULT_TEXT_DECORATION}`, + fontSize: FONT_SIZE_VALUES.HEADING1, }; case 'heading2': @@ -131,6 +134,7 @@ export const generateDefaultOtherProps = ( fontVariant: `${INPUT_SHAPE.DEFAULT_FONT_VARIANT}`, fontStyle: `${INPUT_SHAPE.DEFAULT_FONT_STYLE}`, textDecoration: `${INPUT_SHAPE.DEFAULT_TEXT_DECORATION}`, + fontSize: FONT_SIZE_VALUES.HEADING2, }; case 'heading3': return { @@ -138,6 +142,13 @@ export const generateDefaultOtherProps = ( fontVariant: `${INPUT_SHAPE.DEFAULT_FONT_VARIANT}`, fontStyle: `${INPUT_SHAPE.DEFAULT_FONT_STYLE}`, textDecoration: `${INPUT_SHAPE.DEFAULT_TEXT_DECORATION}`, + fontSize: FONT_SIZE_VALUES.HEADING3, + }; + case 'link': + return { + textColor: `${LINK_SHAPE.DEFAULT_FILL_TEXT}`, + textDecoration: 'underline', + fontSize: FONT_SIZE_VALUES.LINK, }; case 'normaltext': return { @@ -145,6 +156,7 @@ export const generateDefaultOtherProps = ( fontVariant: `${INPUT_SHAPE.DEFAULT_FONT_VARIANT}`, fontStyle: `${INPUT_SHAPE.DEFAULT_FONT_STYLE}`, textDecoration: `${INPUT_SHAPE.DEFAULT_TEXT_DECORATION}`, + fontSize: FONT_SIZE_VALUES.NORMALTEXT, }; case 'smalltext': return { @@ -152,8 +164,12 @@ export const generateDefaultOtherProps = ( fontVariant: `${INPUT_SHAPE.DEFAULT_FONT_VARIANT}`, fontStyle: `${INPUT_SHAPE.DEFAULT_FONT_STYLE}`, textDecoration: `${INPUT_SHAPE.DEFAULT_TEXT_DECORATION}`, + fontSize: FONT_SIZE_VALUES.SMALLTEXT, }; case 'paragraph': + return { + fontSize: FONT_SIZE_VALUES.PARAGRAPH, + }; case 'label': return { textColor: '#000000', @@ -185,6 +201,7 @@ export const generateDefaultOtherProps = ( categories: ['IT'], }, iconSize: 'M', + stroke: BASIC_SHAPE.DEFAULT_STROKE_COLOR, }; case 'image': return { diff --git a/src/pods/canvas/model/shape-size.mapper.ts b/src/pods/canvas/model/shape-size.mapper.ts index 57a03ace..ed814f90 100644 --- a/src/pods/canvas/model/shape-size.mapper.ts +++ b/src/pods/canvas/model/shape-size.mapper.ts @@ -38,6 +38,7 @@ import { getPostItShapeSizeRestrictions, getRectangleShapeSizeRestrictions, getStarShapeSizeRestrictions, + getModalCoverShapeSizeRestrictions, // other imports } from '@/common/components/mock-components/front-basic-shapes'; import { @@ -63,6 +64,7 @@ import { getHeading1SizeRestrictions, getHeading2SizeRestrictions, getHeading3SizeRestrictions, + getLinkSizeRestrictions, getNormaltextSizeRestrictions, getParagraphSizeRestrictions, getSmalltextSizeRestrictions, @@ -117,6 +119,7 @@ const shapeSizeMap: Record ShapeSizeRestrictions> = { normaltext: getNormaltextSizeRestrictions, smalltext: getSmalltextSizeRestrictions, paragraph: getParagraphSizeRestrictions, + link: getLinkSizeRestrictions, largeArrow: getLargeArrowShapeSizeRestrictions, radiobutton: getRadioButtonShapeSizeRestrictions, checkbox: getCheckboxShapeSizeRestrictions, @@ -128,6 +131,7 @@ const shapeSizeMap: Record ShapeSizeRestrictions> = { calendar: getCalendarShapeSizeRestrictions, verticalScrollBar: getVerticalScrollBarShapeSizeRestrictions, modal: getModalShapeSizeRestrictions, + modalCover: getModalCoverShapeSizeRestrictions, tabsBar: getTabsBarShapeSizeRestrictions, appBar: getAppBarShapeSizeRestrictions, buttonBar: getButtonBarShapeSizeRestrictions, diff --git a/src/pods/canvas/model/transformer.model.ts b/src/pods/canvas/model/transformer.model.ts index 3d159a84..12769dc7 100644 --- a/src/pods/canvas/model/transformer.model.ts +++ b/src/pods/canvas/model/transformer.model.ts @@ -60,6 +60,7 @@ export const generateTypeOfTransformer = (shapeType: ShapeType): string[] => { case 'heading3': case 'normaltext': case 'smalltext': + case 'link': case 'horizontalScrollBar': case 'appBar': case 'buttonBar': diff --git a/src/pods/canvas/shape-renderer/index.tsx b/src/pods/canvas/shape-renderer/index.tsx index d9005801..17d019d6 100644 --- a/src/pods/canvas/shape-renderer/index.tsx +++ b/src/pods/canvas/shape-renderer/index.tsx @@ -49,6 +49,7 @@ import { renderHorizontalLine, renderVerticalLine, renderCircle, + renderModalCover, renderStar, renderPostit, renderLargeArrowShape, @@ -57,6 +58,7 @@ import { renderHeading1, renderHeading2, renderHeading3, + renderLink, renderNormaltext, } from './simple-text-components'; import { renderSmalltext } from './simple-text-components/smalltext.renderer'; @@ -150,6 +152,8 @@ export const renderShapeComponent = ( return renderSmalltext(shape, shapeRenderedProps); case 'paragraph': return renderParagraph(shape, shapeRenderedProps); + case 'link': + return renderLink(shape, shapeRenderedProps); case 'largeArrow': return renderLargeArrowShape(shape, shapeRenderedProps); case 'icon': @@ -166,6 +170,8 @@ export const renderShapeComponent = ( return renderVerticalScrollBar(shape, shapeRenderedProps); case 'modal': return renderModal(shape, shapeRenderedProps); + case 'modalCover': + return renderModalCover(shape, shapeRenderedProps); case 'tabsBar': return renderTabsBar(shape, shapeRenderedProps); case 'appBar': diff --git a/src/pods/canvas/shape-renderer/simple-basic-shapes/index.ts b/src/pods/canvas/shape-renderer/simple-basic-shapes/index.ts index a676e622..85121f0f 100644 --- a/src/pods/canvas/shape-renderer/simple-basic-shapes/index.ts +++ b/src/pods/canvas/shape-renderer/simple-basic-shapes/index.ts @@ -7,3 +7,4 @@ export * from './vertical-line.renderer'; export * from './circle.renderer'; export * from './star.renderer'; export * from './large-arrow.renderer'; +export * from './modal-cover.rerender'; diff --git a/src/pods/canvas/shape-renderer/simple-basic-shapes/modal-cover.rerender.tsx b/src/pods/canvas/shape-renderer/simple-basic-shapes/modal-cover.rerender.tsx new file mode 100644 index 00000000..99f0ddcd --- /dev/null +++ b/src/pods/canvas/shape-renderer/simple-basic-shapes/modal-cover.rerender.tsx @@ -0,0 +1,30 @@ +import { ModalCoverShape } from '@/common/components/mock-components/front-basic-shapes'; +import { ShapeRendererProps } from '../model'; +import { ShapeModel } from '@/core/model'; + +export const renderModalCover = ( + shape: ShapeModel, + shapeRenderedProps: ShapeRendererProps +) => { + const { handleSelected, shapeRefs, handleDragEnd, handleTransform } = + shapeRenderedProps; + + return ( + + ); +}; diff --git a/src/pods/canvas/shape-renderer/simple-component/icon.renderer.tsx b/src/pods/canvas/shape-renderer/simple-component/icon.renderer.tsx index 702c3ecb..6cb0573b 100644 --- a/src/pods/canvas/shape-renderer/simple-component/icon.renderer.tsx +++ b/src/pods/canvas/shape-renderer/simple-component/icon.renderer.tsx @@ -27,6 +27,7 @@ export const renderIcon = ( onTransformEnd={handleTransform} iconInfo={shape.otherProps?.icon} iconSize={shape.otherProps?.iconSize} + stroke={shape.otherProps?.stroke} /> ); }; diff --git a/src/pods/canvas/shape-renderer/simple-component/timepickerinput.renderer.tsx b/src/pods/canvas/shape-renderer/simple-component/timepickerinput.renderer.tsx index 99bc8425..6476b6b6 100644 --- a/src/pods/canvas/shape-renderer/simple-component/timepickerinput.renderer.tsx +++ b/src/pods/canvas/shape-renderer/simple-component/timepickerinput.renderer.tsx @@ -25,6 +25,8 @@ export const renderTimepickerinput = ( onDragEnd={handleDragEnd(shape.id)} onTransform={handleTransform} onTransformEnd={handleTransform} + isEditable={shape.allowsInlineEdition} + text={shape.text} otherProps={shape.otherProps} /> ); diff --git a/src/pods/canvas/shape-renderer/simple-container/browserwindow.renderer.tsx b/src/pods/canvas/shape-renderer/simple-container/browserwindow.renderer.tsx index ccfd8809..aadcf152 100644 --- a/src/pods/canvas/shape-renderer/simple-container/browserwindow.renderer.tsx +++ b/src/pods/canvas/shape-renderer/simple-container/browserwindow.renderer.tsx @@ -24,6 +24,8 @@ export const renderBrowserWindow = ( onDragEnd={handleDragEnd(shape.id)} onTransform={handleTransform} onTransformEnd={handleTransform} + isEditable={shape.allowsInlineEdition} + text={shape.text} /> ); }; diff --git a/src/pods/canvas/shape-renderer/simple-text-components/index.ts b/src/pods/canvas/shape-renderer/simple-text-components/index.ts index ec4f348d..c34757f4 100644 --- a/src/pods/canvas/shape-renderer/simple-text-components/index.ts +++ b/src/pods/canvas/shape-renderer/simple-text-components/index.ts @@ -2,3 +2,4 @@ export * from './heading1.renderer'; export * from './heading2.renderer'; export * from './heading3.renderer'; export * from './normaltext.renderer'; +export * from './link.renderer'; diff --git a/src/pods/canvas/shape-renderer/simple-text-components/link.renderer.tsx b/src/pods/canvas/shape-renderer/simple-text-components/link.renderer.tsx new file mode 100644 index 00000000..11db6336 --- /dev/null +++ b/src/pods/canvas/shape-renderer/simple-text-components/link.renderer.tsx @@ -0,0 +1,33 @@ +import { LinkShape } from '@/common/components/mock-components/front-text-components'; +import { ShapeRendererProps } from '../model'; +import { ShapeModel } from '@/core/model'; + +export const renderLink = ( + shape: ShapeModel, + shapeRenderedProps: ShapeRendererProps +) => { + const { handleSelected, shapeRefs, handleDragEnd, handleTransform } = + shapeRenderedProps; + + return ( + + ); +}; diff --git a/src/pods/canvas/use-monitor-shape.hook.ts b/src/pods/canvas/use-monitor-shape.hook.ts index d9229b88..1f0b6db2 100644 --- a/src/pods/canvas/use-monitor-shape.hook.ts +++ b/src/pods/canvas/use-monitor-shape.hook.ts @@ -24,38 +24,40 @@ export const useMonitorShape = ( if (!destination) return; invariant(destination); - const type = source.data.type as ShapeType; + if (source.data.type !== 'thumbPage') { + const type = source.data.type as ShapeType; - const screenPosition = - extractScreenCoordinatesFromPragmaticLocation(location); + const screenPosition = + extractScreenCoordinatesFromPragmaticLocation(location); - let positionX = 0; - let positionY = 0; - if (screenPosition) { - invariant(dropRef.current); - const { x: divRelativeX, y: divRelativeY } = - portScreenPositionToDivCoordinates( - dropRef.current as HTMLDivElement, - screenPosition - ); + let positionX = 0; + let positionY = 0; + if (screenPosition) { + invariant(dropRef.current); + const { x: divRelativeX, y: divRelativeY } = + portScreenPositionToDivCoordinates( + dropRef.current as HTMLDivElement, + screenPosition + ); - invariant(stageRef.current); - const stage = stageRef.current; - const { scrollLeft, scrollTop } = getScrollFromDiv( - dropRef as unknown as React.MutableRefObject - ); - const konvaCoord = convertFromDivElementCoordsToKonvaCoords(stage, { - screenPosition, - relativeDivPosition: { x: divRelativeX, y: divRelativeY }, - scroll: { x: scrollLeft, y: scrollTop }, - }); + invariant(stageRef.current); + const stage = stageRef.current; + const { scrollLeft, scrollTop } = getScrollFromDiv( + dropRef as unknown as React.MutableRefObject + ); + const konvaCoord = convertFromDivElementCoordsToKonvaCoords(stage, { + screenPosition, + relativeDivPosition: { x: divRelativeX, y: divRelativeY }, + scroll: { x: scrollLeft, y: scrollTop }, + }); - positionX = - konvaCoord.x - - calculateShapeOffsetToXDropCoordinate(konvaCoord.x, type); - positionY = konvaCoord.y; + positionX = + konvaCoord.x - + calculateShapeOffsetToXDropCoordinate(konvaCoord.x, type); + positionY = konvaCoord.y; + } + addNewShape(type, positionX, positionY); } - addNewShape(type, positionX, positionY); }, }); }, []); diff --git a/src/pods/canvas/use-multiple-selection-shape.hook.tsx b/src/pods/canvas/use-multiple-selection-shape.hook.tsx index 676e1083..006a415b 100644 --- a/src/pods/canvas/use-multiple-selection-shape.hook.tsx +++ b/src/pods/canvas/use-multiple-selection-shape.hook.tsx @@ -13,6 +13,7 @@ import { calculateScaledCoordsFromCanvasDivCoordinatesNoScroll } from './canvas. import { Stage } from 'konva/lib/Stage'; import { isUserDoingMultipleSelectionUsingCtrlOrCmdKey } from '@/common/utils/shapes'; import { KonvaEventObject } from 'konva/lib/Node'; +import { useCanvasContext } from '@/core/providers'; // There's a bug here: if you make a multiple selectin and start dragging // inside the selection but on a blank area it won't drag the selection @@ -49,6 +50,8 @@ export const useMultipleSelectionShapeHook = ( visible: false, }); + const { setIsThumbnailContextMenuVisible } = useCanvasContext(); + const isDraggingSelection = (mouseCoords: Coord) => { if (!transformerRef.current) { return false; @@ -88,7 +91,6 @@ export const useMultipleSelectionShapeHook = ( e: KonvaEventObject | KonvaEventObject ) => { const transformerRect = transformerRef.current?.getClientRect(); - console.log(transformerRect); const mousePosition = e.target?.getStage()?.getPointerPosition() ?? { x: 0, y: 0, @@ -166,6 +168,8 @@ export const useMultipleSelectionShapeHook = ( height: 0, visible: true, }); + + setIsThumbnailContextMenuVisible(false); }; const handleMouseMove = (e: any) => { diff --git a/src/pods/context-menu/use-context-menu.hook.tsx b/src/pods/context-menu/use-context-menu.hook.tsx index d2c53fbb..3b8e1177 100644 --- a/src/pods/context-menu/use-context-menu.hook.tsx +++ b/src/pods/context-menu/use-context-menu.hook.tsx @@ -26,7 +26,11 @@ export const ContextMenu: React.FC = ({ dropRef }) => { const handleRightClick = (event: MouseEvent) => { event.preventDefault(); - if (selectionInfo.getSelectedShapeData()) { + if ( + selectionInfo.getSelectedShapeData() && + stageRef.current && + stageRef.current.container().contains(event.target as Node) + ) { setShowContextMenu(true); setContextMenuPosition({ x: event.clientX, y: event.clientY }); } diff --git a/src/pods/galleries/basic-shapes-gallery/basic-gallery-data/index.ts b/src/pods/galleries/basic-shapes-gallery/basic-gallery-data/index.ts index 0c4b912e..62c9d6bf 100644 --- a/src/pods/galleries/basic-shapes-gallery/basic-gallery-data/index.ts +++ b/src/pods/galleries/basic-shapes-gallery/basic-gallery-data/index.ts @@ -1,14 +1,15 @@ import { ItemInfo } from '@/common/components/gallery/components/model'; export const mockBasicShapesCollection: ItemInfo[] = [ - { thumbnailSrc: '/shapes/postit.svg', type: 'postit' }, - { thumbnailSrc: '/shapes/image.svg', type: 'image' }, - { thumbnailSrc: '/shapes/rectangle.svg', type: 'rectangle' }, - { thumbnailSrc: '/shapes/triangle.svg', type: 'triangle' }, { thumbnailSrc: '/shapes/circle.svg', type: 'circle' }, { thumbnailSrc: '/shapes/diamond.svg', type: 'diamond' }, - { thumbnailSrc: '/shapes/star.svg', type: 'star' }, { thumbnailSrc: '/shapes/horizontalLine.svg', type: 'horizontalLine' }, - { thumbnailSrc: '/shapes/verticalLine.svg', type: 'verticalLine' }, + { thumbnailSrc: '/shapes/image.svg', type: 'image' }, { thumbnailSrc: '/shapes/largeArrow.svg', type: 'largeArrow' }, + { thumbnailSrc: '/shapes/modalCover.svg', type: 'modalCover' }, + { thumbnailSrc: '/shapes/postit.svg', type: 'postit' }, + { thumbnailSrc: '/shapes/rectangle.svg', type: 'rectangle' }, + { thumbnailSrc: '/shapes/star.svg', type: 'star' }, + { thumbnailSrc: '/shapes/triangle.svg', type: 'triangle' }, + { thumbnailSrc: '/shapes/verticalLine.svg', type: 'verticalLine' }, ]; diff --git a/src/pods/galleries/component-gallery/component-gallery-data/index.ts b/src/pods/galleries/component-gallery/component-gallery-data/index.ts index ec5f7b14..19bb8c29 100644 --- a/src/pods/galleries/component-gallery/component-gallery-data/index.ts +++ b/src/pods/galleries/component-gallery/component-gallery-data/index.ts @@ -1,24 +1,24 @@ import { ItemInfo } from '@/common/components/gallery/components/model'; export const mockWidgetCollection: ItemInfo[] = [ - { thumbnailSrc: '/widgets/icon.svg', type: 'icon' }, - { thumbnailSrc: '/widgets/label.svg', type: 'label' }, - { thumbnailSrc: '/widgets/input.svg', type: 'input' }, { thumbnailSrc: '/widgets/button.svg', type: 'button' }, - { thumbnailSrc: '/widgets/textarea.svg', type: 'textarea' }, - { thumbnailSrc: '/widgets/combobox.svg', type: 'combobox' }, - { thumbnailSrc: '/widgets/radiobutton.svg', type: 'radiobutton' }, { thumbnailSrc: '/widgets/checkbox.svg', type: 'checkbox' }, - { thumbnailSrc: '/widgets/toggleswitch.svg', type: 'toggleswitch' }, - { thumbnailSrc: '/widgets/progressbar.svg', type: 'progressbar' }, - { thumbnailSrc: '/widgets/listbox.svg', type: 'listbox' }, - { thumbnailSrc: '/widgets/slider.svg', type: 'slider' }, + { thumbnailSrc: '/widgets/combobox.svg', type: 'combobox' }, { thumbnailSrc: '/widgets/datepicker.svg', type: 'datepickerinput' }, - { thumbnailSrc: '/widgets/timepicker.svg', type: 'timepickerinput' }, - { thumbnailSrc: '/widgets/tooltip.svg', type: 'tooltip' }, - { thumbnailSrc: '/widgets/verticalscrollbar.svg', type: 'verticalScrollBar' }, { thumbnailSrc: '/widgets/horizontalscrollbar.svg', type: 'horizontalScrollBar', }, + { thumbnailSrc: '/widgets/icon.svg', type: 'icon' }, + { thumbnailSrc: '/widgets/input.svg', type: 'input' }, + { thumbnailSrc: '/widgets/label.svg', type: 'label' }, + { thumbnailSrc: '/widgets/listbox.svg', type: 'listbox' }, + { thumbnailSrc: '/widgets/progressbar.svg', type: 'progressbar' }, + { thumbnailSrc: '/widgets/radiobutton.svg', type: 'radiobutton' }, + { thumbnailSrc: '/widgets/slider.svg', type: 'slider' }, + { thumbnailSrc: '/widgets/textarea.svg', type: 'textarea' }, + { thumbnailSrc: '/widgets/timepicker.svg', type: 'timepickerinput' }, + { thumbnailSrc: '/widgets/toggleswitch.svg', type: 'toggleswitch' }, + { thumbnailSrc: '/widgets/tooltip.svg', type: 'tooltip' }, + { thumbnailSrc: '/widgets/verticalscrollbar.svg', type: 'verticalScrollBar' }, ]; diff --git a/src/pods/galleries/container-gallery/container-gallery-data/index.ts b/src/pods/galleries/container-gallery/container-gallery-data/index.ts index 0def6bb8..6cb3a0d8 100644 --- a/src/pods/galleries/container-gallery/container-gallery-data/index.ts +++ b/src/pods/galleries/container-gallery/container-gallery-data/index.ts @@ -2,7 +2,7 @@ import { ItemInfo } from '@/common/components/gallery/components/model'; export const mockContainerCollection: ItemInfo[] = [ { thumbnailSrc: '/containers/browser.svg', type: 'browser' }, - { thumbnailSrc: '/containers/tablet.svg', type: 'tablet' }, { thumbnailSrc: 'containers/mobile.svg', type: 'mobilePhone' }, { thumbnailSrc: 'containers/modal-dialog.svg', type: 'modalDialog' }, + { thumbnailSrc: '/containers/tablet.svg', type: 'tablet' }, ]; diff --git a/src/pods/galleries/rich-components-gallery/rich-components-gallery-data/index.ts b/src/pods/galleries/rich-components-gallery/rich-components-gallery-data/index.ts index d2689139..983d76ae 100644 --- a/src/pods/galleries/rich-components-gallery/rich-components-gallery-data/index.ts +++ b/src/pods/galleries/rich-components-gallery/rich-components-gallery-data/index.ts @@ -1,26 +1,23 @@ import { ItemInfo } from '@/common/components/gallery/components/model'; export const mockRichComponentsCollection: ItemInfo[] = [ - { thumbnailSrc: '/rich-components/audioPlayer.svg', type: 'audioPlayer' }, - { thumbnailSrc: '/rich-components/table.svg', type: 'table' }, { thumbnailSrc: '/rich-components/accordion.svg', type: 'accordion' }, + { thumbnailSrc: '/rich-components/appBar.svg', type: 'appBar' }, + { thumbnailSrc: '/rich-components/audioPlayer.svg', type: 'audioPlayer' }, + { thumbnailSrc: '/rich-components/barchart.svg', type: 'bar' }, + { thumbnailSrc: '/rich-components/breadcrumb.svg', type: 'breadcrumb' }, + { thumbnailSrc: '/rich-components/button-bar-group.svg', type: 'buttonBar' }, + { thumbnailSrc: '/rich-components/calendar.svg', type: 'calendar' }, { thumbnailSrc: '/rich-components/horizontal-menu.svg', type: 'horizontal-menu', }, - { thumbnailSrc: '/rich-components/button-bar-group.svg', type: 'buttonBar' }, - { - thumbnailSrc: '/rich-components/vertical-menu.svg', - type: 'vertical-menu', - }, - { thumbnailSrc: '/rich-components/appBar.svg', type: 'appBar' }, - { thumbnailSrc: '/rich-components/breadcrumb.svg', type: 'breadcrumb' }, + { thumbnailSrc: '/rich-components/line-chart.svg', type: 'linechart' }, + { thumbnailSrc: '/rich-components/map.svg', type: 'map' }, { thumbnailSrc: '/rich-components/modal.svg', type: 'modal' }, + { thumbnailSrc: '/rich-components/pie.svg', type: 'pie' }, + { thumbnailSrc: '/rich-components/table.svg', type: 'table' }, { thumbnailSrc: '/rich-components/tabsbar.svg', type: 'tabsBar' }, - { thumbnailSrc: '/rich-components/calendar.svg', type: 'calendar' }, + { thumbnailSrc: '/rich-components/vertical-menu.svg', type: 'vertical-menu' }, { thumbnailSrc: '/rich-components/videoPlayer.svg', type: 'videoPlayer' }, - { thumbnailSrc: '/rich-components/pie.svg', type: 'pie' }, - { thumbnailSrc: '/rich-components/line-chart.svg', type: 'linechart' }, - { thumbnailSrc: '/rich-components/barchart.svg', type: 'bar' }, - { thumbnailSrc: '/rich-components/map.svg', type: 'map' }, ]; diff --git a/src/pods/galleries/text-component-gallery/text-component-galley-data/index.ts b/src/pods/galleries/text-component-gallery/text-component-galley-data/index.ts index 7a2a18f7..3a1f8fda 100644 --- a/src/pods/galleries/text-component-gallery/text-component-galley-data/index.ts +++ b/src/pods/galleries/text-component-gallery/text-component-galley-data/index.ts @@ -4,6 +4,7 @@ export const mockTextCollection: ItemInfo[] = [ { thumbnailSrc: '/text/heading1.svg', type: 'heading1' }, { thumbnailSrc: '/text/heading2.svg', type: 'heading2' }, { thumbnailSrc: '/text/heading3.svg', type: 'heading3' }, + { thumbnailSrc: '/text/link.svg', type: 'link' }, { thumbnailSrc: '/text/normaltext.svg', type: 'normaltext' }, { thumbnailSrc: '/text/smalltext.svg', type: 'smalltext' }, { thumbnailSrc: '/text/paragraph.svg', type: 'paragraph' }, diff --git a/src/pods/properties/components/active-element-selector/active-element-selector.component.tsx b/src/pods/properties/components/active-element-selector/active-element-selector.component.tsx index dd827294..9b0c6b71 100644 --- a/src/pods/properties/components/active-element-selector/active-element-selector.component.tsx +++ b/src/pods/properties/components/active-element-selector/active-element-selector.component.tsx @@ -27,7 +27,7 @@ export const ActiveElementSelector: React.FC = ({ // Checking whether the type is tabsBar and parsing the text const isElementTypeSupported = - type === 'tabsBar' || 'buttonBar' || 'horizontal-menu'; + type === 'tabsBar' || 'buttonBar' || 'horizontal-menu' || 'timepickerinput'; const elementNames = isElementTypeSupported && text ? extractElementNames(text) : []; diff --git a/src/pods/properties/components/font-size/font-size.module.css b/src/pods/properties/components/font-size/font-size.module.css new file mode 100644 index 00000000..8e51ebc3 --- /dev/null +++ b/src/pods/properties/components/font-size/font-size.module.css @@ -0,0 +1,32 @@ +.container { + display: flex; + gap: 0.5em; + align-items: center; + padding: var(--space-xs) var(--space-md); + border-bottom: 1px solid var(--primary-300); +} + +.container :first-child { + flex: 1; +} + +.button { + border: none; + color: var(--text-color); + background-color: inherit; + width: var(--space-lg); + height: var(--space-lg); + border-radius: var(--border-radius-s); + font-size: var(--fs-xs); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: var(--space-s); + transition: all 0.3s ease-in-out; + cursor: pointer; +} + +.button:hover { + background-color: var(--primary-100); +} diff --git a/src/pods/properties/components/font-size/font-size.tsx b/src/pods/properties/components/font-size/font-size.tsx new file mode 100644 index 00000000..684cadc5 --- /dev/null +++ b/src/pods/properties/components/font-size/font-size.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import classes from './font-size.module.css'; + +interface Props { + fontSize: number | undefined; + label: string; + onChange: (fontSize: number) => void; +} + +export const FontSize: React.FC = props => { + const { label, fontSize, onChange } = props; + const handleChange = (e: React.ChangeEvent) => { + const value = e.target.value; + onChange(Number(value)); + }; + + return ( +
+

{label}

+ +
+ ); +}; diff --git a/src/pods/properties/components/font-size/index.ts b/src/pods/properties/components/font-size/index.ts new file mode 100644 index 00000000..9835905b --- /dev/null +++ b/src/pods/properties/components/font-size/index.ts @@ -0,0 +1 @@ +export * from './font-size'; diff --git a/src/pods/properties/components/icon-selector/modal/icons.ts b/src/pods/properties/components/icon-selector/modal/icons.ts index b3748605..1e527216 100644 --- a/src/pods/properties/components/icon-selector/modal/icons.ts +++ b/src/pods/properties/components/icon-selector/modal/icons.ts @@ -578,25 +578,13 @@ export const iconCollection: IconInfo[] = [ 'phone', 'smartphone', 'tablet', - 'application', - 'play store', - 'google play', ], categories: ['IT'], }, { name: 'Apple', filename: 'apple.svg', - searchTerms: [ - 'apple', - 'system', - 'mobile', - 'phone', - 'smartphone', - 'tablet', - 'application', - 'app store', - ], + searchTerms: ['apple', 'system', 'mobile', 'phone', 'smartphone', 'tablet'], categories: ['IT'], }, { @@ -1187,7 +1175,7 @@ export const iconCollection: IconInfo[] = [ { name: 'Chat', filename: 'chat.svg', - searchTerms: ['chat', 'message', 'conversation', 'chatting'], + searchTerms: ['chat', 'message', 'conversation', 'chatting', 'comment'], categories: ['IT'], }, { @@ -1328,4 +1316,695 @@ export const iconCollection: IconInfo[] = [ searchTerms: ['calendar', 'days', 'date', 'hours', 'time', 'schedule'], categories: ['IT'], }, + { + name: 'Cursor', + filename: 'cursor.svg', + searchTerms: ['cursor', 'pointer', 'select', 'mouse'], + categories: ['IT'], + }, + { + name: 'Cursor click', + filename: 'cursorclick.svg', + searchTerms: ['cursor', 'click', 'pointer', 'select', 'mouse'], + categories: ['IT'], + }, + { + name: 'Microphone', + filename: 'microphone.svg', + searchTerms: ['microphone', 'record', 'micro', 'mic'], + categories: ['IT'], + }, + { + name: 'Microphone off', + filename: 'microphoneslash.svg', + searchTerms: ['microphone', 'mute', 'silence', 'mic'], + categories: ['IT'], + }, + { + name: 'Webcam', + filename: 'webcam.svg', + searchTerms: ['webcam', 'camera', 'video', 'camcorder'], + categories: ['IT'], + }, + { + name: 'Webcam off', + filename: 'webcamslash.svg', + searchTerms: ['webcam', 'camera', 'slash', 'camcorder', 'off'], + categories: ['IT'], + }, + { + name: 'Empty Battery', + filename: 'emptybattery.svg', + searchTerms: ['battery', 'empty', 'discharged', 'energy'], + categories: ['IT'], + }, + { + name: 'Sign in', + filename: 'signin.svg', + searchTerms: ['sign in', 'login', 'enter', 'access'], + categories: ['IT'], + }, + { + name: 'Sign out', + filename: 'signout.svg', + searchTerms: ['sign out', 'logout', 'exit', 'leave'], + categories: ['IT'], + }, + { + name: 'Arrow Bend left', + filename: 'arrowbendupleft.svg', + searchTerms: ['arrow', 'left', 'bend', 'move'], + categories: ['IT'], + }, + { + name: 'Arrow Bend right', + filename: 'arrowbendupright.svg', + searchTerms: ['arrow', 'right', 'bend', 'move'], + categories: ['IT'], + }, + { + name: 'Dots Square', + filename: 'dotssquare.svg', + searchTerms: ['dots', 'square', 'menu', 'more'], + categories: ['IT'], + }, + { + name: 'Dots Vertical', + filename: 'dotsvertical.svg', + searchTerms: ['dots', 'vertical', 'menu', 'options'], + categories: ['IT'], + }, + { + name: 'Google Drive', + filename: 'drive.svg', + searchTerms: ['drive', 'google', 'cloud', 'storage'], + categories: ['IT'], + }, + { + name: 'Linux', + filename: 'linux.svg', + searchTerms: ['linux', 'system', 'software', 'desktop'], + categories: ['IT'], + }, + { + name: 'Windows', + filename: 'windows.svg', + searchTerms: ['windows', 'system', 'software', 'desktop'], + categories: ['IT'], + }, + { + name: 'Moon', + filename: 'moon.svg', + searchTerms: ['moon', 'night', 'dark', 'sky'], + categories: ['IT'], + }, + { + name: 'Alarm', + filename: 'alarm.svg', + searchTerms: ['alarm', 'clock', 'alert', 'ring'], + categories: ['IT'], + }, + { + name: 'Gitlab', + filename: 'gitlab.svg', + searchTerms: ['gitlab', 'code', 'repository', 'version control'], + categories: ['IT'], + }, + { + name: 'List dots', + filename: 'listdots.svg', + searchTerms: ['list', 'categorize', 'unordered', 'classify'], + categories: ['IT'], + }, + { + name: 'List checks', + filename: 'listchecks.svg', + searchTerms: ['list', 'categorize', 'unordered', 'classify'], + categories: ['IT'], + }, + { + name: 'List dashes', + filename: 'listdashes.svg', + searchTerms: ['list', 'categorize', 'unordered', 'classify'], + categories: ['IT'], + }, + { + name: 'List heart', + filename: 'listheart.svg', + searchTerms: ['list', 'categorize', 'unordered', 'classify'], + categories: ['IT'], + }, + { + name: 'Search list', + filename: 'searchlist.svg', + searchTerms: ['search', 'list', 'find', 'lookup', 'query'], + categories: ['IT'], + }, + { + name: 'List numbers', + filename: 'listnumbers.svg', + searchTerms: ['list', 'categorize', 'ordered', 'classify', 'numbers'], + categories: ['IT'], + }, + { + name: 'Add list', + filename: 'addlist.svg', + searchTerms: ['add', 'list', 'categorize', 'unordered', 'classify'], + categories: ['IT'], + }, + { + name: 'list star', + filename: 'liststar.svg', + searchTerms: ['list', 'categorize', 'unordered', 'classify', 'favorite'], + categories: ['IT'], + }, + { + name: 'Help', + filename: 'help.svg', + searchTerms: ['help', 'question', 'support', 'assist'], + categories: ['IT'], + }, + { + name: 'Star', + filename: 'star.svg', + searchTerms: ['star', 'favorite', 'like', 'rate'], + categories: ['IT'], + }, + { + name: 'Text align center', + filename: 'textaligncenter.svg', + searchTerms: ['text', 'align', 'center', 'format', 'paragraph'], + categories: ['IT'], + }, + { + name: 'Text align justify', + filename: 'textalignjustify.svg', + searchTerms: ['text', 'align', 'justify', 'format', 'paragraph'], + categories: ['IT'], + }, + { + name: 'Text align left', + filename: 'textalignleft.svg', + searchTerms: ['text', 'align', 'left', 'format', 'paragraph'], + categories: ['IT'], + }, + { + name: 'Text align right', + filename: 'textalignright.svg', + searchTerms: ['text', 'align', 'right', 'format', 'paragraph'], + categories: ['IT'], + }, + { + name: 'Text indent', + filename: 'textindent.svg', + searchTerms: ['text', 'indent', 'format', 'paragraph'], + categories: ['IT'], + }, + { + name: 'Upload', + filename: 'upload.svg', + searchTerms: ['upload', 'transfer', 'load', 'charge', 'import'], + categories: ['IT'], + }, + { + name: 'Warning circle', + filename: 'warningcircle.svg', + searchTerms: ['warning', 'circle', 'alert', 'caution'], + categories: ['IT'], + }, + { + name: 'Warning', + filename: 'warning.svg', + searchTerms: ['warning', 'alert', 'caution', 'attention'], + categories: ['IT'], + }, + { + name: 'Shield check', + filename: 'shieldcheck.svg', + searchTerms: ['shield', 'checked', 'security', 'verified'], + categories: ['IT'], + }, + { + name: 'Shield checkered', + filename: 'shieldcheckered.svg', + searchTerms: ['shield', 'protection', 'security', 'defense'], + categories: ['IT'], + }, + { + name: 'Shield desactivated', + filename: 'shieldslash.svg', + searchTerms: ['shield', 'desactivated', 'security', 'off'], + categories: ['IT'], + }, + { + name: 'Shield warning', + filename: 'shieldwarning.svg', + searchTerms: ['shield', 'warning', 'security', 'alert'], + categories: ['IT'], + }, + { + name: 'Shield', + filename: 'normalshield.svg', + searchTerms: ['shield', 'protection', 'security', 'defense'], + categories: ['IT'], + }, + { + name: 'Scissors', + filename: 'scissors.svg', + searchTerms: ['scissors', 'cut', 'tool', 'clip'], + categories: ['IT'], + }, + { + name: 'Phone', + filename: 'phone.svg', + searchTerms: ['phone', 'landline', 'device', 'telephone'], + categories: ['IT'], + }, + { + name: 'Phone call', + filename: 'phonecall.svg', + searchTerms: ['phone', 'call', 'landline', 'device', 'telephone'], + categories: ['IT'], + }, + { + name: 'Phone hang', + filename: 'phonehang.svg', + searchTerms: ['phone', 'hang', 'landline', 'device', 'telephone'], + categories: ['IT'], + }, + { + name: 'Phone disconnected', + filename: 'phoneslash.svg', + searchTerms: ['phone', 'disconnected', 'landline', 'device', 'telephone'], + categories: ['IT'], + }, + { + name: 'Phone pause', + filename: 'phonepause.svg', + searchTerms: ['phone', 'pause', 'landline', 'device', 'telephone'], + categories: ['IT'], + }, + { + name: 'Call phone incoming', + filename: 'callphoneincoming.svg', + searchTerms: [ + 'call', + 'phone', + 'incoming', + 'landline', + 'device', + 'telephone', + ], + categories: ['IT'], + }, + { + name: 'Phone list', + filename: 'phonelist.svg', + searchTerms: ['phone', 'list', 'landline', 'device', 'telephone'], + categories: ['IT'], + }, + { + name: 'Music note', + filename: 'musicnote.svg', + searchTerms: ['music', 'note', 'sound', 'audio', 'melody'], + categories: ['IT'], + }, + { + name: 'File document', + filename: 'filedoc.svg', + searchTerms: ['file', 'document', 'digital', 'sheet'], + categories: ['IT'], + }, + { + name: 'File pdf', + filename: 'filepdf.svg', + searchTerms: ['file', 'pdf', 'document', 'digital', 'sheet'], + categories: ['IT'], + }, + { + name: 'File png', + filename: 'filepng.svg', + searchTerms: ['file', 'png', 'document', 'digital', 'sheet'], + categories: ['IT'], + }, + { + name: 'File powerpoint', + filename: 'filepowerpoint.svg', + searchTerms: ['file', 'powerpoint', 'document', 'digital', 'sheet'], + categories: ['IT'], + }, + { + name: 'File jpg', + filename: 'filejpg.svg', + searchTerms: ['file', 'jpg', 'document', 'digital', 'sheet'], + categories: ['IT'], + }, + { + name: 'File excel', + filename: 'fileexcel.svg', + searchTerms: ['file', 'excel', 'document', 'digital', 'sheet'], + categories: ['IT'], + }, + { + name: 'Arrows clockwise', + filename: 'arrowsclockwise.svg', + searchTerms: ['arrows', 'clockwise', 'direction', 'rotate'], + categories: ['IT'], + }, + { + name: 'Arrows counter clockwise', + filename: 'arrowscounterclockwise.svg', + searchTerms: ['arrows', 'counter clockwise', 'direction', 'rotate'], + categories: ['IT'], + }, + { + name: 'Arrow fat down', + filename: 'arrowfatdown.svg', + searchTerms: ['arrow', 'fat', 'down', 'move'], + categories: ['IT'], + }, + { + name: 'Arrow fat left', + filename: 'arrowfatleft.svg', + searchTerms: ['arrow', 'fat', 'left', 'move'], + categories: ['IT'], + }, + { + name: 'Arrow fat right', + filename: 'arrowfatright.svg', + searchTerms: ['arrow', 'fat', 'right', 'move'], + categories: ['IT'], + }, + { + name: 'Arrow fat up', + filename: 'arrowfatup.svg', + searchTerms: ['arrow', 'fat', 'up', 'move'], + categories: ['IT'], + }, + { + name: 'Check fat', + filename: 'checkfat.svg', + searchTerms: ['check', 'confirmation', 'validate', 'success'], + categories: ['IT'], + }, + { + name: 'Check', + filename: 'check.svg', + searchTerms: ['check', 'confirmation', 'validate', 'success'], + categories: ['IT'], + }, + { + name: 'Double check', + filename: 'doublecheck.svg', + searchTerms: ['check', 'confirmation', 'validate', 'double'], + categories: ['IT'], + }, + { + name: 'Start', + filename: 'home.svg', + searchTerms: ['start', 'home', 'begin', 'launch'], + categories: ['IT'], + }, + { + name: 'Company', + filename: 'company.svg', + searchTerms: ['company', 'business', 'enterprise', 'corporation'], + categories: ['IT'], + }, + { + name: 'Factory', + filename: 'factory.svg', + searchTerms: ['factory', 'industry', 'manufacture', 'production'], + categories: ['IT'], + }, + { + name: 'Keyboard', + filename: 'keyboard.svg', + searchTerms: ['keyboard', 'device', 'computer', 'write'], + categories: ['IT'], + }, + { + name: 'Printer', + filename: 'printer.svg', + searchTerms: ['printer', 'device', 'computer', 'imprint'], + categories: ['IT'], + }, + { + name: 'Plug', + filename: 'plug.svg', + searchTerms: ['plug', 'device', 'connect', 'power'], + categories: ['IT'], + }, + { + name: 'Copyright', + filename: 'copyright.svg', + searchTerms: [ + 'copyright', + 'rights', + 'protected', + 'intellectual', + 'property', + ], + categories: ['IT'], + }, + { + name: 'Caret down', + filename: 'caretdown.svg', + searchTerms: ['caret', 'down', 'arrow', 'move'], + categories: ['IT'], + }, + { + name: 'Caret left', + filename: 'caretleft.svg', + searchTerms: ['caret', 'left', 'arrow', 'move'], + categories: ['IT'], + }, + { + name: 'Caret up', + filename: 'caretup.svg', + searchTerms: ['caret', 'up', 'arrow', 'move'], + categories: ['IT'], + }, + { + name: 'Caret right', + filename: 'caretright.svg', + searchTerms: ['caret', 'right', 'arrow', 'move'], + categories: ['IT'], + }, + { + name: 'Camera', + filename: 'camera.svg', + searchTerms: ['camera', 'photo', 'shot', 'capture', 'snapshot'], + categories: ['IT'], + }, + { + name: 'Flag', + filename: 'flag.svg', + searchTerms: ['flag', 'signal', 'banderole', 'banner'], + categories: ['IT'], + }, + { + name: 'First aid', + filename: 'firstaid.svg', + searchTerms: ['hospital', 'medical', 'emergency', 'health'], + categories: ['IT'], + }, + { + name: 'Hammer', + filename: 'hammer.svg', + searchTerms: ['hammer', 'tool', 'build', 'repair'], + categories: ['IT'], + }, + { + name: 'Joystick', + filename: 'joystick.svg', + searchTerms: ['joystick', 'game', 'play', 'controller'], + categories: ['IT'], + }, + { + name: 'Controller', + filename: 'controller.svg', + searchTerms: ['controller', 'game', 'play', 'gamepad'], + categories: ['IT'], + }, + { + name: 'Key', + filename: 'key.svg', + searchTerms: ['key', 'secure', 'password', 'access'], + categories: ['IT'], + }, + { + name: 'Lock', + filename: 'lock.svg', + searchTerms: ['lock', 'secure', 'password', 'access'], + categories: ['IT'], + }, + { + name: 'Unlock', + filename: 'lockopen.svg', + searchTerms: ['unlock', 'open', 'access', 'password'], + categories: ['IT'], + }, + { + name: 'Flash', + filename: 'lightning.svg', + searchTerms: ['flash', 'lightning', 'energy', 'power'], + categories: ['IT'], + }, + { + name: 'Auto flash', + filename: 'autoflash.svg', + searchTerms: ['auto', 'flash', 'lightning', 'energy', 'power'], + categories: ['IT'], + }, + { + name: 'Flash slash', + filename: 'flashslash.svg', + searchTerms: ['flash', 'slash', 'lightning', 'energy', 'power'], + categories: ['IT'], + }, + { + name: 'Another Mouse', + filename: 'alternativemouse.svg', + searchTerms: ['mouse', 'device', 'computer', 'click'], + categories: ['IT'], + }, + { + name: 'Power', + filename: 'power.svg', + searchTerms: ['power', 'on', 'off', 'energy'], + categories: ['IT'], + }, + { + name: 'Spinner', + filename: 'spinner.svg', + searchTerms: ['spinner', 'loading', 'wait', 'progress'], + categories: ['IT'], + }, + { + name: 'Spinner gap', + filename: 'spinnergap.svg', + searchTerms: ['spinner', 'loading', 'wait', 'progress'], + categories: ['IT'], + }, + { + name: 'Subtitles', + filename: 'subtitles.svg', + searchTerms: ['subtitles', 'caption', 'language', 'translate'], + categories: ['IT'], + }, + { + name: 'Table', + filename: 'table.svg', + searchTerms: ['table', 'data', 'information', 'spreadsheet'], + categories: ['IT'], + }, + { + name: 'PI symbol', + filename: 'pisymbol.svg', + searchTerms: ['pi', 'symbol', 'math', 'constant', 'number'], + categories: ['IT'], + }, + { + name: 'CopyPaste', + filename: 'clipboard.svg', + searchTerms: ['copy', 'paste', 'clipboard', 'duplicate'], + categories: ['IT'], + }, + { + name: 'Text Bolder', + filename: 'textbolder.svg', + searchTerms: ['text', 'bolder', 'bold', 'format', 'style'], + categories: ['IT'], + }, + { + name: 'Text Italic', + filename: 'textitalic.svg', + searchTerms: ['text', 'italic', 'format', 'style'], + categories: ['IT'], + }, + { + name: 'Text Underline', + filename: 'textunderline.svg', + searchTerms: ['text', 'underline', 'format', 'style'], + categories: ['IT'], + }, + { + name: 'UppercaseLowercase', + filename: 'uppercaselowercase.svg', + searchTerms: ['uppercase', 'lowercase', 'text', 'format', 'style'], + categories: ['IT'], + }, + { + name: 'Paragraph', + filename: 'textparagraph.svg', + searchTerms: ['paragraph', 'text', 'format', 'style'], + categories: ['IT'], + }, + { + name: 'Bucket', + filename: 'paintbucket.svg', + searchTerms: ['bucket', 'paint', 'color', 'fill'], + categories: ['IT'], + }, + { + name: 'Man', + filename: 'gendermale.svg', + searchTerms: ['male', 'symbol', 'gender', 'man'], + categories: ['IT'], + }, + { + name: 'Women', + filename: 'genderfemale.svg', + searchTerms: ['female', 'symbol', 'gender', 'women'], + categories: ['IT'], + }, + { + name: 'Dots six vertical', + filename: 'dotssixvertical.svg', + searchTerms: ['dots', 'vertical', 'menu', 'options'], + categories: ['IT'], + }, + { + name: 'Hand grabbing', + filename: 'handgrabbing.svg', + searchTerms: ['hand', 'catch', 'take', 'hold'], + categories: ['IT'], + }, + { + name: 'Hand swipe left', + filename: 'handswipeleft.svg', + searchTerms: ['hand', 'swipe', 'left', 'gesture'], + categories: ['IT'], + }, + { + name: 'Hand swipe right', + filename: 'handswiperight.svg', + searchTerms: ['hand', 'swipe', 'right', 'gesture'], + categories: ['IT'], + }, + { + name: 'Hand tap', + filename: 'handtap.svg', + searchTerms: ['hand', 'tap', 'gesture', 'touch'], + categories: ['IT'], + }, + { + name: 'Warning octagon', + filename: 'warningoctagon.svg', + searchTerms: ['warning', 'attention', 'alert', 'caution'], + categories: ['IT'], + }, + { + name: 'Inbox', + filename: 'tray.svg', + searchTerms: ['inbox', 'email', 'message', 'postbag'], + categories: ['IT'], + }, + { + name: 'Tag', + filename: 'tag.svg', + searchTerms: ['tag', 'label', 'mark', 'identify'], + categories: ['IT'], + }, ]; diff --git a/src/pods/properties/components/stroke-style/stroke.style.component.tsx b/src/pods/properties/components/stroke-style/stroke.style.component.tsx index e1af3ad6..4529bc4c 100644 --- a/src/pods/properties/components/stroke-style/stroke.style.component.tsx +++ b/src/pods/properties/components/stroke-style/stroke.style.component.tsx @@ -31,6 +31,7 @@ export const StrokeStyle: React.FC = props => { + ); diff --git a/src/pods/properties/properties.pod.tsx b/src/pods/properties/properties.pod.tsx index 635afcf9..452577cd 100644 --- a/src/pods/properties/properties.pod.tsx +++ b/src/pods/properties/properties.pod.tsx @@ -12,6 +12,7 @@ import { ActiveElementSelector } from './components/active-element-selector/acti import { FontStyle } from './components/font-style'; import { FontVariant } from './components/font-variant/font-variant'; import { TextDecoration } from './components/text-decoration/text-decoration'; +import { FontSize } from './components/font-size'; export const PropertiesPod = () => { const { selectionInfo } = useCanvasContext(); @@ -165,6 +166,15 @@ export const PropertiesPod = () => { } /> )} + {selectedShapeData?.otherProps?.fontSize && ( + + updateOtherPropsOnSelected('fontSize', fontSize) + } + /> + )} )} {selectedShapeData?.otherProps?.activeElement !== undefined && ( diff --git a/src/pods/thumb-pages/components/context-menu/context-menu.component.module.css b/src/pods/thumb-pages/components/context-menu/context-menu.component.module.css new file mode 100644 index 00000000..82de8324 --- /dev/null +++ b/src/pods/thumb-pages/components/context-menu/context-menu.component.module.css @@ -0,0 +1,40 @@ +.context-menu { + position: absolute; + top: 50%; + left: 50%; + width: 80%; + height: auto; + transform: translate(-50%, -50%); + border: 1px solid var(--primary-500); + background-color: var(--primary-100); + opacity: 0.98; +} + +.container { + display: flex; + gap: 0.5em; + align-items: center; + font-size: var(--fs-xs); + padding: var(--space-xs) var(--space-md); + border-bottom: 1px solid var(--primary-300); + cursor: pointer; +} + +.container :first-child { + flex: 1; +} + +.container:hover { + background-color: var(--primary-200); +} + +.disabled { + cursor: not-allowed; + opacity: 0.5; + background-color: var(--primary-200); +} + +.shortcut { + color: var(--primary-400); + font-weight: 500; +} diff --git a/src/pods/thumb-pages/components/context-menu/context-menu.component.tsx b/src/pods/thumb-pages/components/context-menu/context-menu.component.tsx new file mode 100644 index 00000000..62d8e73c --- /dev/null +++ b/src/pods/thumb-pages/components/context-menu/context-menu.component.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import { useCanvasContext } from '@/core/providers'; +import classes from './context-menu.component.module.css'; +import { CopyIcon, DeleteIcon, PencilIcon } from '@/common/components/icons'; + +interface ThumbPageContextMenuProps { + contextMenuRef: React.RefObject; + setShowContextMenu: (show: boolean) => void; + pageIndex: number; + setPageTitleBeingEdited: (index: number) => void; +} + +export const ThumbPageContextMenu: React.FunctionComponent< + ThumbPageContextMenuProps +> = props => { + const { + contextMenuRef, + setShowContextMenu, + pageIndex, + setPageTitleBeingEdited, + } = props; + const { + setIsThumbnailContextMenuVisible, + fullDocument, + duplicatePage, + deletePage, + } = useCanvasContext(); + + enum ContextButtonType { + 'Duplicate', + 'Rename', + 'Delete', + } + + const handleClickOnContextButton = ( + event: React.MouseEvent, + buttonClicked: ContextButtonType + ) => { + event.stopPropagation(); + switch (buttonClicked) { + case ContextButtonType.Duplicate: + duplicatePage(pageIndex); + break; + case ContextButtonType.Rename: + setPageTitleBeingEdited(pageIndex); + break; + case ContextButtonType.Delete: + if (fullDocument.pages.length !== 1) { + deletePage(pageIndex); + } + break; + } + setShowContextMenu(false); + setIsThumbnailContextMenuVisible(false); + }; + + return ( +
+
+ handleClickOnContextButton(event, ContextButtonType.Duplicate) + } + className={classes.container} + > +

Duplicate

+ +
+
+ handleClickOnContextButton(event, ContextButtonType.Rename) + } + className={classes.container} + > +

Rename

+ +
+
+ handleClickOnContextButton(event, ContextButtonType.Delete) + } + className={ + fullDocument.pages.length === 1 + ? `${classes.container} ${classes.disabled}` + : `${classes.container}` + } + > +

Delete

+ +
+
+ ); +}; diff --git a/src/pods/thumb-pages/components/context-menu/index.ts b/src/pods/thumb-pages/components/context-menu/index.ts new file mode 100644 index 00000000..61e476da --- /dev/null +++ b/src/pods/thumb-pages/components/context-menu/index.ts @@ -0,0 +1 @@ +export * from './context-menu.component'; diff --git a/src/pods/thumb-pages/components/drag-drop-thumb.hook.ts b/src/pods/thumb-pages/components/drag-drop-thumb.hook.ts new file mode 100644 index 00000000..530b8149 --- /dev/null +++ b/src/pods/thumb-pages/components/drag-drop-thumb.hook.ts @@ -0,0 +1,58 @@ +import { useCanvasContext } from '@/core/providers'; +import { + draggable, + dropTargetForElements, +} from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; +import { useEffect, useState } from 'react'; +import invariant from 'tiny-invariant'; + +export const useDragDropThumb = ( + divRef: React.RefObject, + pageIndex: number +) => { + const { fullDocument } = useCanvasContext(); + const page = fullDocument.pages[pageIndex]; + const [dragging, setDragging] = useState(false); + const [isDraggedOver, setIsDraggedOver] = useState(false); + + // Drag + useEffect(() => { + const el = divRef.current; + invariant(el); + return draggable({ + element: el, + getInitialData: () => ({ + pageId: page.id, //fullDocument.pages[pageIndex].id, + type: 'thumbPage', + }), + onDragStart: () => { + setDragging(true); + }, + onDrop: () => setDragging(false), + }); + }, [divRef.current, pageIndex, fullDocument.pages]); + + // Drop + useEffect(() => { + const el = divRef.current; + invariant(el); + + return dropTargetForElements({ + element: el, + getData: () => ({ + pageId: page.id, //fullDocument.pages[pageIndex].id, + type: 'thumbPage', + }), + onDragEnter: () => setIsDraggedOver(true), + onDragLeave: () => setIsDraggedOver(false), + onDrop: () => { + setIsDraggedOver(false); + }, + }); + }, [divRef.current, pageIndex, fullDocument.pages]); + + return { + dragging, + isDraggedOver, + }; +}; diff --git a/src/pods/thumb-pages/components/index.ts b/src/pods/thumb-pages/components/index.ts new file mode 100644 index 00000000..f096ec4a --- /dev/null +++ b/src/pods/thumb-pages/components/index.ts @@ -0,0 +1,2 @@ +export * from './thumb-page'; +export * from './page-title-inline-edit.component'; diff --git a/src/pods/thumb-pages/components/page-title-inline-edit.component.tsx b/src/pods/thumb-pages/components/page-title-inline-edit.component.tsx new file mode 100644 index 00000000..da42d0fd --- /dev/null +++ b/src/pods/thumb-pages/components/page-title-inline-edit.component.tsx @@ -0,0 +1,54 @@ +import React, { useEffect, useState } from 'react'; +import { useCanvasContext } from '@/core/providers'; + +interface PageTitleInlineEditProps { + pageIndex: number; + setPageTitleBeingEdited: (index: number | null) => void; +} + +export const PageTitleInlineEdit: React.FC = ({ + pageIndex, + setPageTitleBeingEdited, +}) => { + const { fullDocument, editPageTitle, setIsInlineEditing } = + useCanvasContext(); + const [inputValue, setInputValue] = useState( + fullDocument.pages[pageIndex].name + ); + const inputRef = React.useRef(null); + + const updatePageTitle = () => { + editPageTitle(pageIndex, inputValue); + setPageTitleBeingEdited(null); + setIsInlineEditing(false); + }; + + const handleAction = ( + event: React.FormEvent | React.FocusEvent + ) => { + if (event.type === 'submit') { + event.preventDefault(); + } + updatePageTitle(); + }; + + useEffect(() => { + if (inputRef.current) { + inputRef.current.focus(); + inputRef.current.select(); + setIsInlineEditing(true); + } + }, []); + + return ( +
+ setInputValue(e.target.value)} + onBlur={handleAction} + /> +
+ ); +}; diff --git a/src/pods/thumb-pages/components/thumb-page.business.ts b/src/pods/thumb-pages/components/thumb-page.business.ts new file mode 100644 index 00000000..42096b6b --- /dev/null +++ b/src/pods/thumb-pages/components/thumb-page.business.ts @@ -0,0 +1,41 @@ +import { ShapeModel, Size } from '@/core/model'; +import { calculateCanvasBounds } from '@/pods/toolbar/components/export-button/export-button.utils'; + +export const calculateScaleBasedOnBounds = ( + shapes: ShapeModel[], + divSize: Size +) => { + const bounds = calculateCanvasBounds(shapes); + const canvasSizeRough = { + width: bounds.x + bounds.width, + height: bounds.y + bounds.height, + }; + + const canvasSize = { + width: canvasSizeRough.width > 800 ? canvasSizeRough.width : 800, + height: canvasSizeRough.height > 600 ? canvasSizeRough.height : 600, + }; + + const scaleFactorX = divSize.width / canvasSize.width; + const scaleFactorY = divSize.height / canvasSize.height; + return Math.min(scaleFactorX, scaleFactorY); +}; + +/* +export const calculateScaleBasedOnBounds = ( + canvasBounds: CanvasBounds +): number => { + let canvasSize = { + width: canvasBounds.x + canvasBounds.width, + height: canvasBounds.y + canvasBounds.height, + }; + + const newCanvasBounds = { + width: canvasSize.width > 800 ? canvasSize.width : 800, + height: canvasSize.height > 600 ? canvasSize.height : 600, + }; + + const scaleFactorX = 250 / newCanvasBounds.width; + const scaleFactorY = 180 / newCanvasBounds.height; + return Math.min(scaleFactorX, scaleFactorY); +};*/ diff --git a/src/pods/thumb-pages/components/thumb-page.module.css b/src/pods/thumb-pages/components/thumb-page.module.css new file mode 100644 index 00000000..7ba5d480 --- /dev/null +++ b/src/pods/thumb-pages/components/thumb-page.module.css @@ -0,0 +1,30 @@ +.container { + width: 100%; + flex: 1; + border: 1px solid; + border-color: black; + border-radius: 3px; + position: relative; + background-color: white; + display: flex; + align-items: center; + justify-content: center; +} + +.icon-container > svg { + position: absolute; + bottom: 8px; + right: 8px; + width: 12px; + height: 12px; + + cursor: pointer; +} + +.icon-container > svg:hover { + background-color: var(--primary-100); +} + +.noclick { + pointer-events: none; +} diff --git a/src/pods/thumb-pages/components/thumb-page.tsx b/src/pods/thumb-pages/components/thumb-page.tsx new file mode 100644 index 00000000..507cc037 --- /dev/null +++ b/src/pods/thumb-pages/components/thumb-page.tsx @@ -0,0 +1,139 @@ +import { ShapeRefs, Size } from '@/core/model'; +import { useCanvasContext } from '@/core/providers'; +import { renderShapeComponent } from '@/pods/canvas/shape-renderer'; +import { KonvaEventObject } from 'konva/lib/Node'; +import { createRef, useRef } from 'react'; +import { Layer, Stage } from 'react-konva'; +import { calculateScaleBasedOnBounds } from './thumb-page.business'; +import { ThumbPageContextMenu } from './context-menu'; +import { useContextMenu } from '../use-context-menu-thumb.hook'; +import { CaretDown } from '@/common/components/icons'; +import classes from './thumb-page.module.css'; + +import React from 'react'; +import { useDragDropThumb } from './drag-drop-thumb.hook'; + +interface Props { + pageIndex: number; + isVisible: boolean; + onSetActivePage: (pageId: string) => void; + setPageTitleBeingEdited: (index: number) => void; +} + +export const ThumbPage: React.FunctionComponent = props => { + const { fullDocument, activePageIndex } = useCanvasContext(); + const { pageIndex, onSetActivePage, setPageTitleBeingEdited, isVisible } = + props; + const page = fullDocument.pages[pageIndex]; + const shapes = page.shapes; + const fakeShapeRefs = useRef({}); + + const [finalScale, setFinalScale] = React.useState(1); + const [canvasSize, setCanvasSize] = React.useState({ + width: 1, + height: 1, + }); + + const divRef = useRef(null); + const [key, setKey] = React.useState(0); + + const { dragging, isDraggedOver } = useDragDropThumb(divRef, pageIndex); + + const handleResizeAndForceRedraw = () => { + const newCanvaSize = { + width: divRef.current?.clientWidth || 1, + height: divRef.current?.clientHeight || 1, + }; + + setCanvasSize(newCanvaSize); + setFinalScale(calculateScaleBasedOnBounds(shapes, newCanvaSize)); + setTimeout(() => { + setKey(key => key + 1); + }, 100); + }; + + React.useLayoutEffect(() => { + handleResizeAndForceRedraw(); + }, []); + + React.useEffect(() => { + if (!isVisible) return; + handleResizeAndForceRedraw(); + }, [isVisible]); + + React.useEffect(() => { + setTimeout(() => { + handleResizeAndForceRedraw(); + }, 100); + }, [shapes, activePageIndex]); + + React.useEffect(() => { + window.addEventListener('resize', handleResizeAndForceRedraw); + + return () => { + window.removeEventListener('resize', handleResizeAndForceRedraw); + }; + }, [divRef.current]); + + const { + showContextMenu, + contextMenuRef, + setShowContextMenu, + handleShowContextMenu, + } = useContextMenu(); + + return ( + <> +
onSetActivePage(page.id)} + onContextMenu={handleShowContextMenu} + style={{ + opacity: dragging ? 0.4 : 1, + background: isDraggedOver ? 'lightblue' : 'white', + }} + key={key} + > +
+ + + {shapes.map(shape => { + if (!fakeShapeRefs.current[shape.id]) { + fakeShapeRefs.current[shape.id] = createRef(); + } + return renderShapeComponent(shape, { + handleSelected: () => {}, + shapeRefs: fakeShapeRefs, + handleDragEnd: + (_: string) => (_: KonvaEventObject) => {}, + handleTransform: () => {}, + }); + })} + + +
+ + + + + {showContextMenu && ( + + )} +
+ + ); +}; diff --git a/src/pods/thumb-pages/index.ts b/src/pods/thumb-pages/index.ts new file mode 100644 index 00000000..864664f5 --- /dev/null +++ b/src/pods/thumb-pages/index.ts @@ -0,0 +1 @@ +export * from './thumb-pages.pod'; diff --git a/src/pods/thumb-pages/monitor-drop-thumb.hook.ts b/src/pods/thumb-pages/monitor-drop-thumb.hook.ts new file mode 100644 index 00000000..c2d8e151 --- /dev/null +++ b/src/pods/thumb-pages/monitor-drop-thumb.hook.ts @@ -0,0 +1,25 @@ +import { useEffect } from 'react'; +import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; +import { useCanvasContext } from '@/core/providers'; + +export const useMonitorDropThumb = () => { + const { fullDocument, swapPages } = useCanvasContext(); + + // Monitor + useEffect(() => { + return monitorForElements({ + onDrop({ source, location }) { + const destination = location.current.dropTargets[0]; + if (!destination || source.data.pageId === destination.data.pageId) { + return; + } + if (destination.data.type === 'thumbPage') { + swapPages( + String(source.data.pageId), + String(destination.data.pageId) + ); + } + }, + }); + }, [fullDocument.pages]); +}; diff --git a/src/pods/thumb-pages/thumb-pages.module.css b/src/pods/thumb-pages/thumb-pages.module.css new file mode 100644 index 00000000..832abe0c --- /dev/null +++ b/src/pods/thumb-pages/thumb-pages.module.css @@ -0,0 +1,68 @@ +.container { + display: flex; + padding: var(--space-s); + gap: var(--space-s); + align-items: center; + justify-content: center; + flex-direction: column; +} + +.thumb { + width: 100%; + height: 240px; + display: flex; + align-items: center; + justify-content: center; + animation: cubic-bezier(1, 0, 0, 1) 0.3s 1 normal thumb; +} +.activeThumb { + width: 100%; + height: 240px; + background-color: var(--primary-100); + border-radius: 5px; + display: flex; + align-items: center; + justify-content: center; + animation: ease 0.3s 1 normal thumb; +} + +/* .activeText { + color: white; +} */ + +.addButton { + margin-top: var(--space-md); + margin-bottom: var(--space-md); + border: 1px solid var(--primary-700); + background-color: transparent; + width: 35px; + height: 35px; + border-radius: 100%; + font-size: var(--fs-lg); + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.3s; + cursor: pointer; +} + +.addButton:hover { + background-color: var(--primary-700); + color: white; +} + +@keyframes thumb { + 0% { + height: 0; + width: 0; + opacity: 0; + } + 15% { + opacity: 0; + } + 100% { + opacity: 1; + width: 100%; + height: 240px; + } +} diff --git a/src/pods/thumb-pages/thumb-pages.pod.tsx b/src/pods/thumb-pages/thumb-pages.pod.tsx new file mode 100644 index 00000000..30de47c4 --- /dev/null +++ b/src/pods/thumb-pages/thumb-pages.pod.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import classes from './thumb-pages.module.css'; +import { useCanvasContext } from '@/core/providers'; +import { PageTitleInlineEdit, ThumbPage } from './components'; +import { PlusIcon } from '@/common/components/icons'; +import { useMonitorDropThumb } from './monitor-drop-thumb.hook'; + +interface Props { + isVisible: boolean; +} + +export const ThumbPagesPod: React.FC = props => { + const { isVisible } = props; + const { fullDocument, addNewPage, setActivePage, getActivePage } = + useCanvasContext(); + const [pageTitleBeingEdited, setPageTitleBeingEdited] = React.useState< + number | null + >(null); + + const handleAddNewPage = () => { + addNewPage(); + }; + + const handleSetActivePage = (pageId: string) => { + setActivePage(pageId); + }; + + useMonitorDropThumb(); + + return ( +
+ {fullDocument.pages.map((page, index) => ( + +
+ + {pageTitleBeingEdited === index ? ( + + ) : ( +
setPageTitleBeingEdited(index)} + className={ + page.id === getActivePage().id ? classes.activeText : '' + } + > + {page.name} +
+ )} +
+
+ ))} + +
+ ); +}; diff --git a/src/pods/thumb-pages/use-context-menu-thumb.hook.tsx b/src/pods/thumb-pages/use-context-menu-thumb.hook.tsx new file mode 100644 index 00000000..5d5a65a6 --- /dev/null +++ b/src/pods/thumb-pages/use-context-menu-thumb.hook.tsx @@ -0,0 +1,42 @@ +import { useCanvasContext } from '@/core/providers'; +import { useEffect, useRef, useState } from 'react'; + +export const useContextMenu = () => { + const [showContextMenu, setShowContextMenu] = useState(false); + const contextMenuRef = useRef(null); + const { setIsThumbnailContextMenuVisible } = useCanvasContext(); + + const handleShowContextMenu = ( + event: React.MouseEvent + ) => { + event.preventDefault(); + if (!showContextMenu) { + setIsThumbnailContextMenuVisible(true); + setShowContextMenu(true); + } + }; + + useEffect(() => { + const closeContextMenu = (event: MouseEvent) => { + if ( + contextMenuRef.current && + !contextMenuRef.current.contains(event.target as Node) + ) { + setShowContextMenu(false); + setIsThumbnailContextMenuVisible(false); + } + }; + + window.addEventListener('mousedown', closeContextMenu); + return () => { + window.removeEventListener('mousedown', closeContextMenu); + }; + }, [showContextMenu, setIsThumbnailContextMenuVisible]); + + return { + showContextMenu, + contextMenuRef, + setShowContextMenu, + handleShowContextMenu, + }; +}; diff --git a/src/pods/toolbar/components/new-button/new-button.tsx b/src/pods/toolbar/components/new-button/new-button.tsx index fa694298..54501807 100644 --- a/src/pods/toolbar/components/new-button/new-button.tsx +++ b/src/pods/toolbar/components/new-button/new-button.tsx @@ -4,7 +4,7 @@ import { useCanvasContext } from '@/core/providers'; import { ToolbarButton } from '../toolbar-button'; export const NewButton = () => { - const { clearCanvas } = useCanvasContext(); + const { createNewFullDocument: clearCanvas } = useCanvasContext(); const handleClick = () => { clearCanvas(); diff --git a/src/scenes/accordion-section-visibility.hook.ts b/src/scenes/accordion-section-visibility.hook.ts new file mode 100644 index 00000000..b38b10d9 --- /dev/null +++ b/src/scenes/accordion-section-visibility.hook.ts @@ -0,0 +1,29 @@ +import { useEffect, useRef, useState } from 'react'; + +export const useAccordionSectionVisibility = () => { + const [isThumbPagesPodOpen, setIsThumbPagesPodOpen] = useState(false); + const thumbPagesPodRef = useRef(null); + + useEffect(() => { + const handleToggle = () => { + setIsThumbPagesPodOpen(thumbPagesPodRef.current?.open ?? false); + }; + + const detailsElement = thumbPagesPodRef.current; + if (detailsElement) { + detailsElement.addEventListener('toggle', handleToggle); + } + + // Cleanup event listener on component unmount + return () => { + if (detailsElement) { + detailsElement.removeEventListener('toggle', handleToggle); + } + }; + }, []); + + return { + thumbPagesPodRef, + isThumbPagesPodOpen, + }; +}; diff --git a/src/scenes/main.module.css b/src/scenes/main.module.css index 2c370d81..4a8ec047 100644 --- a/src/scenes/main.module.css +++ b/src/scenes/main.module.css @@ -16,6 +16,7 @@ .container { overflow-y: auto; + position: relative; } .container[open] { flex: 1; diff --git a/src/scenes/main.scene.tsx b/src/scenes/main.scene.tsx index 086b26f9..c25929b3 100644 --- a/src/scenes/main.scene.tsx +++ b/src/scenes/main.scene.tsx @@ -12,12 +12,25 @@ import { } from '@/pods'; import { PropertiesPod } from '@/pods/properties'; import { FooterPod } from '@/pods/footer/footer.pod'; +import { ThumbPagesPod } from '@/pods/thumb-pages'; +import { useAccordionSectionVisibility } from './accordion-section-visibility.hook'; export const MainScene = () => { + const { isThumbPagesPodOpen, thumbPagesPodRef } = + useAccordionSectionVisibility(); + return (
+
+ Pages + +
Devices