From 6be821925860ca6eb3f3ab6d970a8dd7904a9a95 Mon Sep 17 00:00:00 2001 From: Guste Gaubaite <219.guste@gmail.com> Date: Fri, 17 Oct 2025 17:35:43 +0200 Subject: [PATCH 1/6] refactor: move shortcuts from toolbar to common hooks --- src/common/shortcut/index.ts | 3 + src/common/shortcut/shortcut.const.ts | 104 +++++++++++++++++++ src/common/shortcut/shortcut.hook.spec.tsx | 112 +++++++++++++++++++++ src/common/shortcut/shortcut.hook.tsx | 45 +++++++++ src/common/shortcut/shortcut.model.ts | 6 ++ 5 files changed, 270 insertions(+) create mode 100644 src/common/shortcut/index.ts create mode 100644 src/common/shortcut/shortcut.const.ts create mode 100644 src/common/shortcut/shortcut.hook.spec.tsx create mode 100644 src/common/shortcut/shortcut.hook.tsx create mode 100644 src/common/shortcut/shortcut.model.ts diff --git a/src/common/shortcut/index.ts b/src/common/shortcut/index.ts new file mode 100644 index 00000000..0c5d098d --- /dev/null +++ b/src/common/shortcut/index.ts @@ -0,0 +1,3 @@ +export * from './shortcut.const'; +export * from './shortcut.hook'; +export * from './shortcut.model'; diff --git a/src/common/shortcut/shortcut.const.ts b/src/common/shortcut/shortcut.const.ts new file mode 100644 index 00000000..3fee1730 --- /dev/null +++ b/src/common/shortcut/shortcut.const.ts @@ -0,0 +1,104 @@ +import { ShortcutOptions } from './shortcut.model'; + +interface Shortcut { + [key: string]: ShortcutOptions; +} + +export const SHORTCUTS: Shortcut = { + addCollection: { + description: 'Add Collection', + id: 'add-collection-button-shortcut', + targetKey: ['c'], + targetKeyLabel: 'C', + }, + addRelation: { + description: 'Add Relation', + id: 'add-relation-button-shortcut', + targetKey: ['r'], + targetKeyLabel: 'R', + }, + delete: { + description: 'Delete', + id: 'delete-button-shortcut', + targetKey: ['backspace'], + targetKeyLabel: 'Backspace', + }, + export: { + description: 'Export', + id: 'export-button-shortcut', + targetKey: ['e'], + targetKeyLabel: 'E', + }, + new: { + description: 'New', + id: 'new-button-shortcut', + targetKey: ['n'], + targetKeyLabel: 'N', + }, + open: { + description: 'Open', + id: 'open-button-shortcut', + targetKey: ['o'], + targetKeyLabel: 'O', + }, + redo: { + description: 'Redo', + id: 'redo-button-shortcut', + targetKey: ['y'], + targetKeyLabel: 'Y', + }, + save: { + description: 'Save', + id: 'save-button-shortcut', + targetKey: ['s'], + targetKeyLabel: 'S', + }, + settings: { + description: 'Settings', + id: 'settings-button-shortcut', + targetKey: ['t'], + targetKeyLabel: 'T', + }, + undo: { + description: 'Undo', + id: 'undo-button-shortcut', + targetKey: ['z'], + targetKeyLabel: 'Z', + }, + zoomIn: { + description: 'Zoom In', + id: 'zoom-in-button-shortcut', + targetKey: ['=', '+'], + targetKeyLabel: '"+"', + }, + zoomOut: { + description: 'Zoom Out', + id: 'zoom-out-button-shortcut', + targetKey: ['-', '-'], + targetKeyLabel: '"-"', + }, + duplicate: { + description: 'Duplicate', + id: 'duplicate-button-shortcut', + targetKey: ['d'], + targetKeyLabel: 'D', + }, + copy: { + description: 'Copy', + id: 'copy-button-shortcut', + targetKey: ['c'], + targetKeyLabel: 'C', + }, + paste: { + description: 'Paste', + id: 'paste-button-shortcut', + targetKey: ['v'], + targetKeyLabel: 'V', + }, + import: { + description: 'Import', + id: 'import-button-shortcut', + targetKey: ['i'], + targetKeyLabel: 'I', + }, +}; diff --git a/src/common/shortcut/shortcut.hook.spec.tsx b/src/common/shortcut/shortcut.hook.spec.tsx new file mode 100644 index 00000000..9c05d8a3 --- /dev/null +++ b/src/common/shortcut/shortcut.hook.spec.tsx @@ -0,0 +1,112 @@ +import { renderHook } from '@testing-library/react'; +import useShortcut from './shortcut.hook'; +import { vi } from 'vitest'; + +describe('useShortcut', () => { + let targetKey: string[]; + let callback: () => void; + + beforeEach(() => { + targetKey = ['a']; + callback = vi.fn(); + + Object.defineProperty(window.navigator, 'userAgent', { + value: 'Mac', + configurable: true, + }); + }); + + it('should call the callback when the right key is pressed', async () => { + renderHook(() => useShortcut({ targetKey, callback })); + + const event = new KeyboardEvent('keydown', { + key: 'a', + code: 'KeyA', + ctrlKey: true, + }); + + window.dispatchEvent(event); + + expect(callback).toHaveBeenCalled(); + }); + + it('should not call the callback when the wrong key is pressed', async () => { + renderHook(() => useShortcut({ targetKey, callback })); + + const event = new KeyboardEvent('keydown', { + key: 'b', + code: 'KeyB', + ctrlKey: true, + }); + + window.dispatchEvent(event); + + expect(callback).not.toHaveBeenCalled(); + }); + + it('should add "Alt" to the event if the user is on Windows or Linux', async () => { + Object.defineProperty(window.navigator, 'userAgent', { + value: 'Windows', + configurable: true, + }); + + renderHook(() => useShortcut({ targetKey, callback })); + + const event = new KeyboardEvent('keydown', { + key: 'a', + code: 'KeyA', + altKey: true, + }); + + window.dispatchEvent(event); + + expect(callback).toHaveBeenCalled(); + }); + + it('should add "Ctrl" to the event if the user is on MacOS', async () => { + renderHook(() => useShortcut({ targetKey, callback })); + + const event = new KeyboardEvent('keydown', { + key: 'a', + code: 'KeyA', + ctrlKey: true, + }); + + window.dispatchEvent(event); + + expect(callback).toHaveBeenCalled(); + }); + + it('should not call the callback when the user is on Mac and "Alt" is pressed', async () => { + renderHook(() => useShortcut({ targetKey, callback })); + + const event = new KeyboardEvent('keydown', { + key: 'a', + code: 'KeyA', + altKey: true, + }); + + window.dispatchEvent(event); + + expect(callback).not.toHaveBeenCalled(); + }); + + it('should not call the callback when the user is on Windows or Linux and "Ctrl" is pressed', async () => { + Object.defineProperty(window.navigator, 'userAgent', { + value: 'Windows', + configurable: true, + }); + + renderHook(() => useShortcut({ targetKey, callback })); + + const event = new KeyboardEvent('keydown', { + key: 'a', + code: 'KeyA', + ctrlKey: true, + }); + + window.dispatchEvent(event); + + expect(callback).not.toHaveBeenCalled(); + }); +}); diff --git a/src/common/shortcut/shortcut.hook.tsx b/src/common/shortcut/shortcut.hook.tsx new file mode 100644 index 00000000..1421f4b0 --- /dev/null +++ b/src/common/shortcut/shortcut.hook.tsx @@ -0,0 +1,45 @@ +import { isMacOS, isWindowsOrLinux } from '@/common/helpers/platform.helpers'; +import { useEffect } from 'react'; + +export interface ShortcutHookProps { + targetKey: string[]; + callback: () => void; +} + +/** + * This hook is used to create a keyboard shortcut + * it uses Ctrl + key for Windows and Cmd + key for Mac + * to avoid conflicts with the browser shortcuts + * @param {String[]} targetKey The key that will trigger the shortcut + * @param {Function} callback The function to be called when the shortcut is triggered + * @return {void} + */ + +const useShortcut = ({ targetKey, callback }: ShortcutHookProps) => { + const handleKeyPress = (event: KeyboardEvent) => { + const isAltKeyPressed = event.getModifierState('Alt'); + const isCtrlKeyPressed = event.getModifierState('Control'); + + if ( + (isWindowsOrLinux() && isAltKeyPressed) || + (isMacOS() && isCtrlKeyPressed) + ) { + if (targetKey.includes(event.key)) { + event.preventDefault(); + callback(); + } + } + }; + + useEffect(() => { + const onKeyDown = (event: KeyboardEvent) => handleKeyPress(event); + window.addEventListener('keydown', onKeyDown); + + return () => { + window.removeEventListener('keydown', onKeyDown); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [targetKey, callback]); +}; + +export default useShortcut; diff --git a/src/common/shortcut/shortcut.model.ts b/src/common/shortcut/shortcut.model.ts new file mode 100644 index 00000000..13bff58b --- /dev/null +++ b/src/common/shortcut/shortcut.model.ts @@ -0,0 +1,6 @@ +export interface ShortcutOptions { + id: string; + targetKey: string[]; + targetKeyLabel: string; + description: string; +} From 74f78e6a05fdaea8c4f909c2ead81608701b57bb Mon Sep 17 00:00:00 2001 From: Guste Gaubaite <219.guste@gmail.com> Date: Fri, 17 Oct 2025 17:40:55 +0200 Subject: [PATCH 2/6] feat: create ActionButton component in common components which replaces toolbar button to make it reusable for both toolbar and footer --- .../action-button.component.module.css | 68 +++++++++++++++++++ .../action-button/action-button.component.tsx | 67 ++++++++++++++++++ src/common/components/action-button/index.ts | 1 + 3 files changed, 136 insertions(+) create mode 100644 src/common/components/action-button/action-button.component.module.css create mode 100644 src/common/components/action-button/action-button.component.tsx create mode 100644 src/common/components/action-button/index.ts diff --git a/src/common/components/action-button/action-button.component.module.css b/src/common/components/action-button/action-button.component.module.css new file mode 100644 index 00000000..07cbbe14 --- /dev/null +++ b/src/common/components/action-button/action-button.component.module.css @@ -0,0 +1,68 @@ +.button { + background: none; + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + fill: var(--text-color); +} + +@media screen and (max-device-width: 1090px) { + .button { + padding: 6px; + font-size: var(--fs-s); + } +} + +.tooltip { + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + background-color: hsla(0, 0%, 20%, 0.7); + border-radius: 3px; + color: var(--text-color); + font-size: 14px; + line-height: 14px; + padding: 7px 14px; + position: absolute; + transform: translate(0%, 90px); + visibility: hidden; + z-index: 2; +} + +.tooltipBottom { + transform: translate(0%, 90px); +} + +.tooltipTop { + transform: translate(0%, -90px); +} + +.tooltipTopLeft { + transform: translate(-50%, -90px); +} + +@media (hover: hover) { + .button:hover .tooltip { + animation: fadeIn 1s linear; + animation-fill-mode: forwards; + } +} + +@media screen and (max-device-width: 1090px) { + .tooltip { + display: none; + } +} + +@keyframes fadeIn { + 0% { + opacity: 0; + } + 75% { + opacity: 0; + } + 100% { + opacity: 1; + visibility: visible; + } +} diff --git a/src/common/components/action-button/action-button.component.tsx b/src/common/components/action-button/action-button.component.tsx new file mode 100644 index 00000000..385a37e7 --- /dev/null +++ b/src/common/components/action-button/action-button.component.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { isMacOS } from '@/common/helpers/platform.helpers'; +import classes from './action-button.component.module.css'; +import { ShortcutOptions } from '@/common/shortcut'; +import useShortcut from '@/common/shortcut/shortcut.hook'; + +interface Props { + icon?: React.ReactNode; + label: string; + onClick?: () => void; + className?: string; + disabled?: boolean; + shortcutOptions?: ShortcutOptions; + showLabel?: boolean; + tooltipPosition?: 'top' | 'bottom'; +} + +export const ActionButton: React.FC = ({ + disabled, + icon, + onClick = () => {}, + className, + label, + shortcutOptions, + showLabel = true, + tooltipPosition = 'bottom', +}) => { + const shortcutCommand = isMacOS() ? 'Ctrl' : 'Alt'; + const showTooltip = shortcutOptions && !disabled; + const tooltipText = `(${shortcutCommand} + ${shortcutOptions?.targetKeyLabel})`; + + const tooltipPositionClass = + tooltipPosition === 'top' ? classes.tooltipTop : classes.tooltipBottom; + + const tooltipClasses = `${classes.tooltip} ${tooltipPositionClass}`; + + const buttonClasses = className + ? `${classes.button} ${className}`.trim() + : classes.button; + + useShortcut({ + ...shortcutOptions, + targetKey: shortcutOptions?.targetKey || [], + callback: onClick, + }); + + return ( + + ); +}; diff --git a/src/common/components/action-button/index.ts b/src/common/components/action-button/index.ts new file mode 100644 index 00000000..5f1b4359 --- /dev/null +++ b/src/common/components/action-button/index.ts @@ -0,0 +1 @@ +export * from './action-button.component'; From d597c68b8547d6dcda48bdd1cc3564f79d3424c8 Mon Sep 17 00:00:00 2001 From: Guste Gaubaite <219.guste@gmail.com> Date: Fri, 17 Oct 2025 20:03:14 +0200 Subject: [PATCH 3/6] feat: add zoom buttons to footer (desktop only) --- src/App.css | 530 +++++++++--------- .../footer/components/zoom-button/index.ts | 2 + .../zoom-in-button.component.module.css | 4 + .../zoom-button/zoom-in-button.component.tsx | 24 + .../zoom-button/zoom-out-button.component.tsx | 23 + src/pods/footer/footer.component.tsx | 5 + src/pods/footer/footer.pod.module.css | 8 +- 7 files changed, 332 insertions(+), 264 deletions(-) create mode 100644 src/pods/footer/components/zoom-button/index.ts create mode 100644 src/pods/footer/components/zoom-button/zoom-in-button.component.module.css create mode 100644 src/pods/footer/components/zoom-button/zoom-in-button.component.tsx create mode 100644 src/pods/footer/components/zoom-button/zoom-out-button.component.tsx diff --git a/src/App.css b/src/App.css index edaec624..a3c08c3f 100644 --- a/src/App.css +++ b/src/App.css @@ -1,360 +1,366 @@ *, *:after, *:before { - box-sizing: border-box; + box-sizing: border-box; } :root { - --padding-table: 38px; - --relation-color: #ecad5a; - --border-radius-table: var(--border-radius-s); - --checkbox-size: 22px; - --space-cells: 9px; - --color-error: rgb(247, 44, 44); - --border-toolbar: 1.5px solid var(--primary-700); - - /* spacing */ - --space-unit: 4px; - --space-xxs: calc(var(--space-unit) / 2); - --space-xs: calc(var(--space-unit) * 2); - --space-sm: calc(var(--space-unit) * 3); - --space-md: calc(var(--space-unit) * 4); - --space-lg: calc(var(--space-unit) * 6); - --space-xl: calc(var(--space-unit) * 8); - --space-xxl: calc(var(--space-unit) * 12); - --space-xxxl: calc(var(--space-unit) * 16); - - /* colors */ - --primary-50: #eef9f1; - --primary-100: #dcf2e3; - --primary-200: #b9e5c7; - --primary-300: #96d9ab; - --primary-400: #73cc8f; - --primary-500: #50bf73; - --primary-600: #40995c; - --primary-700: #307345; - --primary-800: #204c2e; - --primary-900: #102617; - - --secondary-50: #fbf5ed; - --secondary-100: #f7eada; - --secondary-200: #f0d5b5; - --secondary-300: #e8c091; - --secondary-400: #e1ab6c; - --secondary-500: #d99647; - --secondary-600: #ae7839; - --secondary-700: #825a2b; - --secondary-800: #573c1c; - --secondary-900: #2b1e0e; - - /* text */ - --text-color: #f3eded; - --text-disabled: #919191; - --text-dark: #202020; - - /* background */ - --background-50: #e2e7eb; - --background-100: #cad3dc; - --background-200: #a2b3c2; - --background-300: #7b8fa1; - --background-400: #455a6c; - --background-500: #2b3c50; - --background-600: #243446; - --background-700: #1c2d3f; - --background-800: #142231; - --background-900: #0f1924; - - /*font-sizes*/ - --fs-xs: 12px; - --fs-s: 14px; - --fs-m: 16px; - --fs-md: 18px; - --fs-l: 20px; - --fs-xl: 24px; - --fs-xxl: 32px; - - /*font-weight*/ - --fw-light: 300; - --fw-regular: 400; - --fw-medium: 500; - --fw-bold: 600; - --fw-extrabold: 700; - - /*border-radius*/ - --border-radius-unit: 4px; - --border-radius-xxs: calc(var(--border-radius-unit) / 2); - --border-radius-xs: calc(var(--border-radius-unit) * 2); - --border-radius-s: calc(var(--border-radius-unit) * 3); - --border-radius-m: calc(var(--border-radius-unit) * 4); - --border-radius-l: calc(var(--border-radius-unit) * 6); - --border-radius-xl: calc(var(--border-radius-unit) * 8); - --border-radius-xxl: calc(var(--border-radius-unit) * 12); - --border-radius-xxxl: calc(var(--border-radius-unit) * 16); - - --input-border-color: var(--background-400); - --edit-table-header: var(--background-900); - --input-border-color-active: var(--primary-300); - --input-radio-border-color: var(--background-400); - - /* Modal */ - --background-dialog: var(--background-700); - --veil-modal: #0d0d1185; - - /* Canvas */ - --bg-canvas: var(--background-800); - --bg-toolbar: var(--background-400); - --bg-table: var(--background-700); - --header-table: var(--primary-300); - - /* Input */ - --bg-input: var(--background-500); - --bg-input-disabled: var(--background-600); - --hover-input: var(--background-500); - - /* buttons */ - --button-secondary: var(--secondary-400); - --hover-button-secondary: var(--secondary-600); - --hover-button: var(--primary-300); - - /* border */ - --primary-border-color: var(--primary-300); - --secondary-border-color: var(--background-300); - --shadow-filter: var(--primary-600); - - /* checkbox */ - --bg-checkbox: var(--background-500); - --border-checkbox: var(--background-400); - --hover-checkbox: var(--background-200); - - /*About*/ - --color-project: var(--primary-300); - --color-team: var(--secondary-300); - - /* Footer*/ - --footer-background: var(--background-700); - --footer-text-color: var(--primary-300); - - /*Main styles*/ - font-family: Inter, system-ui, Helvetica, Arial, sans-serif; - font-size: var(--fs-m); - line-height: 1.5; - font-weight: var(--fw-regular); - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - -webkit-text-size-adjust: 100%; + --padding-table: 38px; + --relation-color: #ecad5a; + --border-radius-table: var(--border-radius-s); + --checkbox-size: 22px; + --space-cells: 9px; + --color-error: rgb(247, 44, 44); + --border-toolbar: 1.5px solid var(--primary-700); + + /* spacing */ + --space-unit: 4px; + --space-xxs: calc(var(--space-unit) / 2); + --space-xs: calc(var(--space-unit) * 2); + --space-sm: calc(var(--space-unit) * 3); + --space-md: calc(var(--space-unit) * 4); + --space-lg: calc(var(--space-unit) * 6); + --space-xl: calc(var(--space-unit) * 8); + --space-xxl: calc(var(--space-unit) * 12); + --space-xxxl: calc(var(--space-unit) * 16); + + /* colors */ + --primary-50: #eef9f1; + --primary-100: #dcf2e3; + --primary-200: #b9e5c7; + --primary-300: #96d9ab; + --primary-400: #73cc8f; + --primary-500: #50bf73; + --primary-600: #40995c; + --primary-700: #307345; + --primary-800: #204c2e; + --primary-900: #102617; + + --secondary-50: #fbf5ed; + --secondary-100: #f7eada; + --secondary-200: #f0d5b5; + --secondary-300: #e8c091; + --secondary-400: #e1ab6c; + --secondary-500: #d99647; + --secondary-600: #ae7839; + --secondary-700: #825a2b; + --secondary-800: #573c1c; + --secondary-900: #2b1e0e; + + /* text */ + --text-color: #f3eded; + --text-disabled: #919191; + --text-dark: #202020; + + /* background */ + --background-50: #e2e7eb; + --background-100: #cad3dc; + --background-200: #a2b3c2; + --background-300: #7b8fa1; + --background-400: #455a6c; + --background-500: #2b3c50; + --background-600: #243446; + --background-700: #1c2d3f; + --background-800: #142231; + --background-900: #0f1924; + + /*font-sizes*/ + --fs-xs: 12px; + --fs-s: 14px; + --fs-m: 16px; + --fs-md: 18px; + --fs-l: 20px; + --fs-xl: 24px; + --fs-xxl: 32px; + + /*font-weight*/ + --fw-light: 300; + --fw-regular: 400; + --fw-medium: 500; + --fw-bold: 600; + --fw-extrabold: 700; + + /*border-radius*/ + --border-radius-unit: 4px; + --border-radius-xxs: calc(var(--border-radius-unit) / 2); + --border-radius-xs: calc(var(--border-radius-unit) * 2); + --border-radius-s: calc(var(--border-radius-unit) * 3); + --border-radius-m: calc(var(--border-radius-unit) * 4); + --border-radius-l: calc(var(--border-radius-unit) * 6); + --border-radius-xl: calc(var(--border-radius-unit) * 8); + --border-radius-xxl: calc(var(--border-radius-unit) * 12); + --border-radius-xxxl: calc(var(--border-radius-unit) * 16); + + --input-border-color: var(--background-400); + --edit-table-header: var(--background-900); + --input-border-color-active: var(--primary-300); + --input-radio-border-color: var(--background-400); + + /* Modal */ + --background-dialog: var(--background-700); + --veil-modal: #0d0d1185; + + /* Canvas */ + --bg-canvas: var(--background-800); + --bg-toolbar: var(--background-400); + --bg-table: var(--background-700); + --header-table: var(--primary-300); + + /* Input */ + --bg-input: var(--background-500); + --bg-input-disabled: var(--background-600); + --hover-input: var(--background-500); + + /* buttons */ + --button-secondary: var(--secondary-400); + --hover-button-secondary: var(--secondary-600); + --hover-button: var(--primary-300); + + /* border */ + --primary-border-color: var(--primary-300); + --secondary-border-color: var(--background-300); + --shadow-filter: var(--primary-600); + + /* checkbox */ + --bg-checkbox: var(--background-500); + --border-checkbox: var(--background-400); + --hover-checkbox: var(--background-200); + + /*About*/ + --color-project: var(--primary-300); + --color-team: var(--secondary-300); + + /* Footer*/ + --footer-background: var(--background-700); + --footer-text-color: var(--primary-300); + + /*Main styles*/ + font-family: Inter, system-ui, Helvetica, Arial, sans-serif; + font-size: var(--fs-m); + line-height: 1.5; + font-weight: var(--fw-regular); + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; } body { - margin: 0; - text-align: center; + margin: 0; + text-align: center; } .light { - /* text */ - --text-color: #202020; - --text-disabled: #aeaeae; - - /* Modal */ - --background-dialog: #f6f7f9; - --veil-modal: #23232385; - - /* Canvas */ - --bg-canvas: #f9fafb; - --bg-table: #f6f7f9; - --header-table: var(--primary-300); - - /* Toolbar */ - --bg-toolbar: #f0f2f5; - --border-toolbar: 1.5px solid var(--primary-200); - - /* Input */ - --bg-input: #f3f5f7; - --bg-input-disabled: #f0f2f5; - --hover-input: #f3f5f7; - --input-border-color: var(--background-200); - --input-border-color-active: var(--primary-500); - --input-radio-border-color: var(--background-200); - - /*Edit-table*/ - --edit-table-header: #e0e6eb; - - /* buttons */ - --button-secondary: var(--secondary-600); - --hover-button-secondary: var(--secondary-600); - --hover-button: var(--primary-300); - - /* border */ - --primary-border-color: var(--primary-400); - --secondary-border-color: #b5b9bc; - --shadow-filter: var(--primary-600); - - /* checkbox */ - --bg-checkbox: #e2e7eb; - - /* Not working*/ - --hover-checkbox: var(--background-300); - - /*About*/ - --color-project: var(--primary-500); - --color-team: var(--secondary-600); - - /* Footer*/ - --footer-background: #f3f5f7; - --footer-text-color: var(--primary-700); + /* text */ + --text-color: #202020; + --text-disabled: #aeaeae; + + /* Modal */ + --background-dialog: #f6f7f9; + --veil-modal: #23232385; + + /* Canvas */ + --bg-canvas: #f9fafb; + --bg-table: #f6f7f9; + --header-table: var(--primary-300); + + /* Toolbar */ + --bg-toolbar: #f0f2f5; + --border-toolbar: 1.5px solid var(--primary-200); + + /* Input */ + --bg-input: #f3f5f7; + --bg-input-disabled: #f0f2f5; + --hover-input: #f3f5f7; + --input-border-color: var(--background-200); + --input-border-color-active: var(--primary-500); + --input-radio-border-color: var(--background-200); + + /*Edit-table*/ + --edit-table-header: #e0e6eb; + + /* buttons */ + --button-secondary: var(--secondary-600); + --hover-button-secondary: var(--secondary-600); + --hover-button: var(--primary-300); + + /* border */ + --primary-border-color: var(--primary-400); + --secondary-border-color: #b5b9bc; + --shadow-filter: var(--primary-600); + + /* checkbox */ + --bg-checkbox: #e2e7eb; + + /* Not working*/ + --hover-checkbox: var(--background-300); + + /*About*/ + --color-project: var(--primary-500); + --color-team: var(--secondary-600); + + /* Footer*/ + --footer-background: #f3f5f7; + --footer-text-color: var(--primary-700); } /* Buttons */ button { - border-radius: var(--border-radius-s); - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: var(--fs-s); - font-weight: var(--fw-medium); - font-family: inherit; - background-color: inherit; - cursor: pointer; - transition: border-color 0.25s; - color: var(--text-color); - transition: all 0.3s ease-in-out; + border-radius: var(--border-radius-s); + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: var(--fs-s); + font-weight: var(--fw-medium); + font-family: inherit; + background-color: inherit; + cursor: pointer; + transition: border-color 0.25s; + color: var(--text-color); + transition: all 0.3s ease-in-out; } button:focus-visible { - outline: 1px auto var(--primary-300); + outline: 1px auto var(--primary-300); } button:hover { - background-color: var(--hover-button); - color: var(--text-dark); + background-color: var(--hover-button); + color: var(--text-dark); } button:disabled { - color: var(--text-disabled); - cursor: default; + color: var(--text-disabled); + cursor: default; } button:disabled:hover { - background: transparent; + background: transparent; } .button-secondary { - background-color: var(--secondary-300); - color: var(--text-dark); - margin-top: var(--space-md); - border-radius: var(--border-radius-s); + background-color: var(--secondary-300); + color: var(--text-dark); + margin-top: var(--space-md); + border-radius: var(--border-radius-s); } .button-secondary:hover { - background-color: var(--hover-button-secondary); + background-color: var(--hover-button-secondary); } .button-tertiary { - background-color: var(--background-500); + background-color: var(--background-500); } .button-tertiary:hover { - background-color: var(--background-300); - color: var(--text-dark); + background-color: var(--background-300); + color: var(--text-dark); } .button-secondary:disabled, .button-tertiary:disabled { - color: var(--text-disabled); - cursor: default; - background-color: var(--background-500); + color: var(--text-disabled); + cursor: default; + background-color: var(--background-500); } .button-secondary:disabled:hover, .button-tertiary:disabled:hover { - background-color: var(--background-500); + background-color: var(--background-500); } .light .button-tertiary { - background-color: var(--background-200); + background-color: var(--background-200); } .light .button-tertiary:hover { - background-color: var(--background-300); + background-color: var(--background-300); } .light .button-secondary:disabled, .light .button-tertiary:disabled { - background-color: #efefef; - color: #929292; + background-color: #efefef; + color: #929292; } .light .button-secondary:disabled:hover, .light .button-tertiary:disabled:hover { - background-color: #efefef; + background-color: #efefef; } .two-buttons { - display: flex; - align-items: baseline; - justify-content: center; - margin-top: var(--space-md); - gap: var(--space-lg); + display: flex; + align-items: baseline; + justify-content: center; + margin-top: var(--space-md); + gap: var(--space-lg); } /* Input */ input, select, textarea { - background-color: var(--bg-input); - border-radius: var(--border-radius-xs); - color: var(--text-color); - padding: var(--space-xs); - width: 100%; - border: none; - outline: none; - - border: 1px solid var(--input-border-color); - transition: all 0.2s ease; + background-color: var(--bg-input); + border-radius: var(--border-radius-xs); + color: var(--text-color); + padding: var(--space-xs); + width: 100%; + border: none; + outline: none; + + border: 1px solid var(--input-border-color); + transition: all 0.2s ease; } input:focus, select:focus { - border: 1px solid var(--input-border-color-active); - background-color: var(--hover-input); + border: 1px solid var(--input-border-color-active); + background-color: var(--hover-input); } select:hover, input:hover { - background-color: var(--hover-input); - box-shadow: 0 0 4px var(--hover-checkbox); + background-color: var(--hover-input); + box-shadow: 0 0 4px var(--hover-checkbox); } select { - padding: 7px; + padding: 7px; } /* Checkbox */ input[type='checkbox'] { - margin: 0; - width: var(--checkbox-size); - height: var(--checkbox-size); - cursor: pointer; + margin: 0; + width: var(--checkbox-size); + height: var(--checkbox-size); + cursor: pointer; } .light input[type='checkbox'] { - color-scheme: light; + color-scheme: light; } .dark input[type='checkbox'] { - color-scheme: dark; + color-scheme: dark; } .mobile-only { - display: none; + display: none; } @media screen and (max-device-width: 1090px) { - .hide-mobile { - display: none; - } - - .mobile-only { - display: block; - } -} \ No newline at end of file + .hide-mobile { + display: none; + } + + .mobile-only { + display: block; + } +} + +@media screen and (min-device-width: 1091px) { + .hide-desktop { + display: none; + } +} diff --git a/src/pods/footer/components/zoom-button/index.ts b/src/pods/footer/components/zoom-button/index.ts new file mode 100644 index 00000000..ab4362f4 --- /dev/null +++ b/src/pods/footer/components/zoom-button/index.ts @@ -0,0 +1,2 @@ +export * from './zoom-in-button.component'; +export * from './zoom-out-button.component'; diff --git a/src/pods/footer/components/zoom-button/zoom-in-button.component.module.css b/src/pods/footer/components/zoom-button/zoom-in-button.component.module.css new file mode 100644 index 00000000..2dd6e76f --- /dev/null +++ b/src/pods/footer/components/zoom-button/zoom-in-button.component.module.css @@ -0,0 +1,4 @@ +.button :global([role='tooltip']) { + transform: translate(-60%, -90px); + white-space: nowrap; +} diff --git a/src/pods/footer/components/zoom-button/zoom-in-button.component.tsx b/src/pods/footer/components/zoom-button/zoom-in-button.component.tsx new file mode 100644 index 00000000..2d6b8732 --- /dev/null +++ b/src/pods/footer/components/zoom-button/zoom-in-button.component.tsx @@ -0,0 +1,24 @@ +import { useCanvasViewSettingsContext } from '@/core/providers'; +import { ZoomIn } from '@/common/components/icons'; +import { ActionButton } from '@/common/components/action-button'; +import { SHORTCUTS } from '@/common/shortcut/shortcut.const'; +import classes from './zoom-in-button.component.module.css'; + +const MINIMUM_ZOOM_FACTOR_ALLOWED = 2.5; + +export const ZoomInButton = () => { + const { zoomIn, canvasViewSettings } = useCanvasViewSettingsContext(); + + return ( + } + label="Zoom In" + onClick={zoomIn} + className={`${classes.button} hide-mobile`} + disabled={canvasViewSettings.zoomFactor < MINIMUM_ZOOM_FACTOR_ALLOWED} + shortcutOptions={SHORTCUTS.zoomIn} + showLabel={false} + tooltipPosition="top" + /> + ); +}; diff --git a/src/pods/footer/components/zoom-button/zoom-out-button.component.tsx b/src/pods/footer/components/zoom-button/zoom-out-button.component.tsx new file mode 100644 index 00000000..1052bfda --- /dev/null +++ b/src/pods/footer/components/zoom-button/zoom-out-button.component.tsx @@ -0,0 +1,23 @@ +import { useCanvasViewSettingsContext } from '@/core/providers'; +import { ZoomOut } from '@/common/components/icons/zoom-out-icon.component'; +import { ActionButton } from '@/common/components/action-button'; +import { SHORTCUTS } from '@/common/shortcut/shortcut.const'; + +const MAXIMUM_ZOOM_FACTOR_ALLOWED = 7.8; + +export const ZoomOutButton = () => { + const { zoomOut, canvasViewSettings } = useCanvasViewSettingsContext(); + + return ( + } + label="Zoom Out" + onClick={zoomOut} + className="hide-mobile" + disabled={canvasViewSettings.zoomFactor > MAXIMUM_ZOOM_FACTOR_ALLOWED} + shortcutOptions={SHORTCUTS.zoomOut} + showLabel={false} + tooltipPosition="top" + /> + ); +}; diff --git a/src/pods/footer/footer.component.tsx b/src/pods/footer/footer.component.tsx index a1ff96a6..30c275ed 100644 --- a/src/pods/footer/footer.component.tsx +++ b/src/pods/footer/footer.component.tsx @@ -11,6 +11,7 @@ import { getFileNameCanvasIsPristine, getFileNameCanvasDirty, } from '@/pods/footer/footer.business'; +import { ZoomInButton, ZoomOutButton } from './components/zoom-button'; const NEW_DOCUMENT_NAME = 'New Document'; const ASTERISK = '*'; @@ -31,6 +32,10 @@ export const FooterComponent: React.FC = () => { return (