From 5263cfd2b65c92b06493f42e818f5a08760edd83 Mon Sep 17 00:00:00 2001 From: Prozilla Date: Wed, 13 Dec 2023 18:21:37 +0100 Subject: [PATCH] Added actions --- package.json | 2 +- src/components/actions/Actions.jsx | 121 ++++++++++++++++++ src/components/actions/Actions.module.css | 91 +++++++++++++ .../actions/actions/Action.module.css | 0 .../actions/actions/ClickAction.jsx | 13 ++ .../file-explorer/FileExplorer.jsx | 36 ++++-- .../applications/settings/AboutSettings.jsx | 5 +- .../applications/settings/Settings.module.css | 22 +--- src/components/desktop/Desktop.jsx | 65 ++++++---- src/components/modals/ModalView.jsx | 9 +- src/components/modals/ModalsView.jsx | 4 +- src/components/modals/ModalsView.module.css | 2 +- .../modals/context-menu/ContextMenu.jsx | 26 ---- .../context-menu/ContextMenu.module.css | 33 ----- .../modals/dialog-box/DialogBox.jsx | 20 +-- src/components/task-bar/TaskBar.jsx | 61 ++++++--- src/components/utils/Button.jsx | 33 ++++- src/components/utils/Button.module.css | 21 ++- src/components/windows/WindowView.jsx | 43 +++---- src/constants/taskBar.js | 1 + src/constants/windows.js | 3 +- src/features/utils/browser.js | 2 + src/features/windows/windows.js | 5 +- src/hooks/modals/ContextMenu.js | 29 +++-- src/hooks/utils/mouse.js | 2 +- src/hooks/utils/screen.js | 20 +++ 26 files changed, 460 insertions(+), 209 deletions(-) create mode 100644 src/components/actions/Actions.jsx create mode 100644 src/components/actions/Actions.module.css create mode 100644 src/components/actions/actions/Action.module.css create mode 100644 src/components/actions/actions/ClickAction.jsx delete mode 100644 src/components/modals/context-menu/ContextMenu.jsx delete mode 100644 src/components/modals/context-menu/ContextMenu.module.css create mode 100644 src/constants/taskBar.js create mode 100644 src/hooks/utils/screen.js diff --git a/package.json b/package.json index 759ac65..177da2a 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "default-case": "off", "arrow-parens": "error", "space-infix-ops": "warn", - "react/no-multi-comp": "error", + "react/no-multi-comp": ["error", { "ignoreStateless": true }], "jsdoc/no-undefined-types": "warn", "jsdoc/require-param": "warn", "jsdoc/check-tag-names": "warn", diff --git a/src/components/actions/Actions.jsx b/src/components/actions/Actions.jsx new file mode 100644 index 0000000..a0e2121 --- /dev/null +++ b/src/components/actions/Actions.jsx @@ -0,0 +1,121 @@ +import { Children, cloneElement, isValidElement, useEffect, useRef, useState } from "react"; +import { useShortcuts } from "../../hooks/utils/keyboard.js"; +import style from "./Actions.module.css"; +import { useScreenDimensions } from "../../hooks/utils/screen.js"; +import { TASK_BAR_HEIGHT } from "../../constants/taskBar.js"; + +export const STYLES = { + CONTEXT_MENU: style["Context-menu"], + SHORTCUTS_LISTENER: style["Shortcuts-listener"], +}; + +/** + * @callback actionsType + * @param {object} props + * @param {string=} props.className + * @param {Function} props.onAnyTrigger + * @param {import("react").ElementType} props.children + * @param {*} props.triggerParams + * @param {boolean} props.avoidTaskBar + */ + +/** + * @type {actionsType} + * @example + * { + * reloadViewport(); + * }} + * /> + */ +export function Actions({ children, className, onAnyTrigger, triggerParams, avoidTaskBar = true }) { + const isListener = (className === STYLES.SHORTCUTS_LISTENER); + + const ref = useRef(null); + const [initiated, setInitiated] = useState(false); + const [alignLeft, setAlignLeft] = useState(false); + const [alignTop, setAlignTop] = useState(false); + const [screenWidth, screenHeight] = useScreenDimensions(); + + const options = {}; + const shortcuts = {}; + + const iterateOverChildren = (children) => { + let actionId = 0; + const newChildren = Children.map(children, (child) => { + if (!isValidElement(child)) + return child; + + actionId++; + + const { label, shortcut, onTrigger } = child.props; + if (label != null && onTrigger != null) { + options[actionId] = onTrigger; + + if (shortcut != null) + shortcuts[actionId] = shortcut; + } + + if (isListener) { + iterateOverChildren(child.props.children); + return; + } + + return cloneElement(child, { + ...child.props, + actionId, + children: iterateOverChildren(child.props.children), + onTrigger: (event) => { + onAnyTrigger?.(event, triggerParams); + onTrigger?.(event, triggerParams); + } + }); + }); + + return newChildren; + }; + + useShortcuts({ options, shortcuts, useCategories: false }); + + useEffect(() => { + if (ref.current == null || screenWidth == null || screenHeight == null) + return; + + const rect = ref.current.getBoundingClientRect(); + const maxX = screenWidth; + let maxY = screenHeight; + + if (avoidTaskBar) + maxY -= TASK_BAR_HEIGHT; + + const isOverflowingRight = (rect.x + rect.width > maxX); + const isOverflowingBottom = (rect.y + rect.height > maxY); + + if (isOverflowingRight) + setAlignLeft(true); + if (isOverflowingBottom) + setAlignTop(true); + + setInitiated(true); + }, [alignLeft, avoidTaskBar, screenHeight, screenWidth]); + + if (isListener) + return iterateOverChildren(children); + + const classNames = [style.Container]; + if (className != null) + classNames.push(className); + if (alignLeft) + classNames.push(style["Align-left"]); + if (alignTop) + classNames.push(style["Align-top"]); + if (!initiated) + classNames.push(style.Uninitiated); + + return (
+ {iterateOverChildren(children)} +
); +} \ No newline at end of file diff --git a/src/components/actions/Actions.module.css b/src/components/actions/Actions.module.css new file mode 100644 index 0000000..e62a27f --- /dev/null +++ b/src/components/actions/Actions.module.css @@ -0,0 +1,91 @@ +.Container { + /* These variables describe the position of the container relative to top left corner of its parent */ + --left: 0; + --top: 0; + --right: calc(1 - var(--left)); + --bottom: calc(1 - var(--top)); + + position: absolute; + top: 0; + left: 0; + opacity: 1; + transition: opacity 100ms ease-out; +} + +.Container.Uninitiated { + opacity: 0; +} + +.Container.Align-left { + --left: 1; + + left: unset; + right: 0; +} + +.Container.Align-top { + --top: 1; + + top: unset; + bottom: 0; +} + +.Context-menu.Container { + --border-radius: 0.5rem; + + padding: 0.375rem; + border-top-left-radius: calc((1 - var(--right) * var(--bottom)) * var(--border-radius)) !important; + border-top-right-radius: calc((1 - var(--left) * var(--bottom)) * var(--border-radius)) !important; + border-bottom-left-radius: calc((1 - var(--right) * var(--top)) * var(--border-radius)) !important; + border-bottom-right-radius: calc((1 - var(--left) * var(--top)) * var(--border-radius)) !important; + background-color: var(--background-color-b) !important; +} + +.Context-menu .Button { + display: flex; + gap: 0.75rem; + justify-content: space-between; + width: 100%; + padding: 0.25rem 0.5rem; + background: none; + border: none; + border-radius: 0.5rem; + outline: none; + font-size: 0.875rem; + text-align: start; + white-space: nowrap; + cursor: pointer; +} + +.Context-menu .Button:hover, +.Context-menu .Button:focus-visible { + background-color: hsla(var(--background-color-a-hsl), 75%); +} + +.Context-menu .Label { + display: flex; + gap: 0.5rem; + justify-content: center; + align-items: center; +} + +.Context-menu .Label p, +.Context-menu .Shortcut { + margin: 0; +} + +.Context-menu .Icon { + display: flex; + width: 0.875rem; + height: 0.875rem; +} + +.Context-menu .Icon > svg { + width: 100%; + height: 100%; + object-fit: contain; +} + +.Context-menu .Shortcut { + color: var(--foreground-color-b); +} \ No newline at end of file diff --git a/src/components/actions/actions/Action.module.css b/src/components/actions/actions/Action.module.css new file mode 100644 index 0000000..e69de29 diff --git a/src/components/actions/actions/ClickAction.jsx b/src/components/actions/actions/ClickAction.jsx new file mode 100644 index 0000000..a199d95 --- /dev/null +++ b/src/components/actions/actions/ClickAction.jsx @@ -0,0 +1,13 @@ +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { formatShortcut } from "../../../features/utils/string.js"; +import styles from "../Actions.module.css"; + +export function ClickAction({ actionId, label, shortcut, onTrigger, icon }) { + return (); +} \ No newline at end of file diff --git a/src/components/applications/file-explorer/FileExplorer.jsx b/src/components/applications/file-explorer/FileExplorer.jsx index c0d252f..0c6d094 100644 --- a/src/components/applications/file-explorer/FileExplorer.jsx +++ b/src/components/applications/file-explorer/FileExplorer.jsx @@ -2,7 +2,7 @@ import { useState } from "react"; import { useVirtualRoot } from "../../../hooks/virtual-drive/virtualRootContext.js"; import styles from "./FileExplorer.module.css"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faArrowUp, faCaretLeft, faCaretRight, faCog, faDesktop, faFileLines, faHouse, faImage, faPlus, faSearch } from "@fortawesome/free-solid-svg-icons"; +import { faArrowUp, faCaretLeft, faCaretRight, faCog, faDesktop, faFileLines, faHouse, faImage, faPlus, faSearch, faTrash } from "@fortawesome/free-solid-svg-icons"; import { useWindowsManager } from "../../../hooks/windows/windowsManagerContext.js"; import { useContextMenu } from "../../../hooks/modals/contextMenu.js"; import { QuickAccessButton } from "./QuickAccessButton.jsx"; @@ -10,6 +10,8 @@ import { useDialogBox } from "../../../hooks/modals/dialogBox.js"; import Vector2 from "../../../features/math/vector2.js"; import { DIALOG_CONTENT_TYPES } from "../../../constants/modals.js"; import { DirectoryList } from "./directory-list/DirectoryList.jsx"; +import { Actions } from "../../actions/Actions.jsx"; +import { ClickAction } from "../../actions/actions/ClickAction.jsx"; /** * @param {import("../../windows/WindowView.jsx").windowProps} props @@ -21,19 +23,25 @@ export function FileExplorer({ startPath, app, modalsManager }) { const windowsManager = useWindowsManager(); const [showHidden] = useState(true); - const { onContextMenu: onContextMenuFile } = useContextMenu({ - modalsManager, - options: { - "Open": (file) => { file.open(windowsManager); }, - "Delete": (file) => { file.delete(); }, - } + const { onContextMenu: onContextMenuFile } = useContextMenu({ modalsManager, Actions: (props) => + + { + file.open(windowsManager); + }}/> + { + file.delete(); + }}/> + }); - const { onContextMenu: onContextMenuFolder } = useContextMenu({ - modalsManager, - options: { - "Open": (folder) => { changeDirectory(folder.name); }, - "Delete": (folder) => { folder.delete(); } - } + const { onContextMenu: onContextMenuFolder } = useContextMenu({ modalsManager, Actions: (props) => + + { + changeDirectory(folder.name); + }}/> + { + folder.delete(); + }}/> + }); // const { onContextMenu: onNew } = useContextMenu({ // modalsManager, @@ -50,7 +58,7 @@ export function FileExplorer({ startPath, app, modalsManager }) { const directory = absolute ? virtualRoot.navigate(path) : currentDirectory.navigate(path); - console.log(directory); + console.debug(directory); if (directory) { setCurrentDirectory(directory); diff --git a/src/components/applications/settings/AboutSettings.jsx b/src/components/applications/settings/AboutSettings.jsx index 22378bd..8f74830 100644 --- a/src/components/applications/settings/AboutSettings.jsx +++ b/src/components/applications/settings/AboutSettings.jsx @@ -29,10 +29,7 @@ export function AboutSettings() { diff --git a/src/components/applications/settings/Settings.module.css b/src/components/applications/settings/Settings.module.css index 4be41c2..9c56374 100644 --- a/src/components/applications/settings/Settings.module.css +++ b/src/components/applications/settings/Settings.module.css @@ -131,28 +131,18 @@ } .Button { - --hover-color: var(--background-color-b); + --normal-color: var(--background-color-a) !important; + --hover-color: var(--background-color-b) !important; - margin-bottom: 0.75rem; + margin-bottom: 0.75rem !important; padding: 0.5rem 1rem; - color: var(--foreground-color-a); - background-color: var(--background-color-a); - border: none; border-radius: 0.5rem; - outline: none; - transition: background-color 100ms ease-in-out; - cursor: pointer; -} - -.Button:hover, .Button:focus-visible { - background-color: var(--hover-color); } .Button-red { - --hover-color: var(--red-b); - - color: var(--background-color-a); - background-color: var(--red-a); + --text-color: var(--background-color-a) !important; + --normal-color: var(--red-a) !important; + --hover-color: var(--red-b) !important; } .Progress-bar-container { diff --git a/src/components/desktop/Desktop.jsx b/src/components/desktop/Desktop.jsx index 1f13c29..2f1e916 100644 --- a/src/components/desktop/Desktop.jsx +++ b/src/components/desktop/Desktop.jsx @@ -13,6 +13,9 @@ import { useVirtualRoot } from "../../hooks/virtual-drive/virtualRootContext.js" import { DirectoryList } from "../applications/file-explorer/directory-list/DirectoryList.jsx"; import { APPS, APP_NAMES } from "../../constants/applications.js"; import Vector2 from "../../features/math/vector2.js"; +import { Actions } from "../actions/Actions.jsx"; +import { ClickAction } from "../actions/actions/ClickAction.jsx"; +import { faArrowsRotate, faFolder, faPaintBrush, faTrash } from "@fortawesome/free-solid-svg-icons"; export const Desktop = memo(() => { const settingsManager = useSettingsManager(); @@ -21,35 +24,44 @@ export const Desktop = memo(() => { const windowsManager = useWindowsManager(); const virtualRoot = useVirtualRoot(); - const { onContextMenu } = useContextMenu({ - modalsManager, - options: { - "Refresh": () => { reloadViewport(); }, - "Change appearance": () => { windowsManager.open("settings", { initialTabIndex: 0 }); }, - [`Open in ${APP_NAMES.FILE_EXPLORER}`]: () => { + const { onContextMenu, ShortcutsListener } = useContextMenu({ modalsManager, Actions: (props) => + + { + reloadViewport(); + }}/> + { + windowsManager.open("settings", { initialTabIndex: 0 }); + }}/> + { windowsManager.open(APPS.FILE_EXPLORER, { startPath: "~/Desktop" }); - }, - } + }}/> + }); - const { onContextMenu: onContextMenuFile } = useContextMenu({ - modalsManager, - options: { - "Open": (file) => { file.open(windowsManager); }, - [`Reveal in ${APP_NAMES.FILE_EXPLORER}`]: (file) => { - windowsManager.open(APPS.FILE_EXPLORER, { startPath: file.parent.path }); - }, - "Delete": (file) => { file.delete(); }, - } + const { onContextMenu: onContextMenuFile } = useContextMenu({ modalsManager, Actions: (props) => + + { + file.open(windowsManager); + }}/> + { + file.parent.open(windowsManager); + }}/> + { + file.delete(); + }}/> + }); - const { onContextMenu: onContextMenuFolder } = useContextMenu({ - modalsManager, - options: { - "Open": (folder) => { folder.open(windowsManager); }, - [`Reveal in ${APP_NAMES.FILE_EXPLORER}`]: (folder) => { - windowsManager.open(APPS.FILE_EXPLORER, { startPath: folder.parent.path }); - }, - "Delete": (folder) => { folder.delete(); }, - } + const { onContextMenu: onContextMenuFolder } = useContextMenu({ modalsManager, Actions: (props) => + + { + folder.open(windowsManager); + }}/> + { + folder.parent.open(windowsManager); + }}/> + { + folder.delete(); + }}/> + }); useEffect(() => { @@ -67,6 +79,7 @@ export const Desktop = memo(() => { }; return (<> +
{ }; }, [modal]); - const container = (
+ const Container = () => (
); if (modal.dismissible) { return ( { modal.close(); }}> - {container} + ); } else { - return container; + return ; } }); \ No newline at end of file diff --git a/src/components/modals/ModalsView.jsx b/src/components/modals/ModalsView.jsx index 48c95bc..cd8e563 100644 --- a/src/components/modals/ModalsView.jsx +++ b/src/components/modals/ModalsView.jsx @@ -11,7 +11,7 @@ import styles from "./ModalsView.module.css"; * @param {import("react").CSSProperties} root.style * @param {import("react").className} root.className */ -export const ModalsView = memo(({ modalsManager, modals, style, className }) => { +export const ModalsView = memo(({ modalsManager, modals, style, className, ...props }) => { const ref = useRef(null); useEffect(() => { @@ -20,7 +20,7 @@ export const ModalsView = memo(({ modalsManager, modals, style, className }) => }, [modalsManager, ref]); return ( -
+
{modals?.map((modal) => )} diff --git a/src/components/modals/ModalsView.module.css b/src/components/modals/ModalsView.module.css index e21f5ff..55b39bd 100644 --- a/src/components/modals/ModalsView.module.css +++ b/src/components/modals/ModalsView.module.css @@ -1,4 +1,4 @@ .Container { position: relative; - z-index: 9; + z-index: 11; } \ No newline at end of file diff --git a/src/components/modals/context-menu/ContextMenu.jsx b/src/components/modals/context-menu/ContextMenu.jsx deleted file mode 100644 index a774129..0000000 --- a/src/components/modals/context-menu/ContextMenu.jsx +++ /dev/null @@ -1,26 +0,0 @@ -import Modal from "../../../features/modals/modal.js"; -import { formatShortcut } from "../../../features/utils/string.js"; -import styles from "./ContextMenu.module.css"; - -/** - * @param {object} props - * @param {Modal} props.modal - * @param {Object} props.options - * @param {Object} props.shortcuts - */ -export function ContextMenu({ modal, options, shortcuts }) { - return (
- {Object.entries(options).map(([label, callback]) => - - )} -
); -} \ No newline at end of file diff --git a/src/components/modals/context-menu/ContextMenu.module.css b/src/components/modals/context-menu/ContextMenu.module.css deleted file mode 100644 index 6e25281..0000000 --- a/src/components/modals/context-menu/ContextMenu.module.css +++ /dev/null @@ -1,33 +0,0 @@ -.Container { - padding: 0.5rem; - border-top-left-radius: 0 !important; - background-color: var(--background-color-b) !important; -} - -.Button { - display: flex; - gap: 0.75rem; - justify-content: space-between; - width: 100%; - padding: 0.25rem 0.5rem; - background: none; - border: none; - border-radius: 0.5rem; - outline: none; - font-size: 0.875rem; - text-align: start; - white-space: nowrap; - cursor: pointer; -} - -.Button:hover, .Button:focus-visible { - background-color: hsla(var(--background-color-a-hsl), 75%); -} - -.Label, .Shortcut { - margin: 0; -} - -.Shortcut { - color: var(--foreground-color-b); -} \ No newline at end of file diff --git a/src/components/modals/dialog-box/DialogBox.jsx b/src/components/modals/dialog-box/DialogBox.jsx index 28c30a1..3853b48 100644 --- a/src/components/modals/dialog-box/DialogBox.jsx +++ b/src/components/modals/dialog-box/DialogBox.jsx @@ -7,30 +7,18 @@ import utilStyles from "../../../styles/utils.module.css"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faXmark } from "@fortawesome/free-solid-svg-icons"; import { DIALOG_CONTENT_TYPES } from "../../../constants/modals.js"; +import { useScreenDimensions } from "../../../hooks/utils/screen.js"; export function DialogBox({ modal, params }) { const { app, title, children } = params; const nodeRef = useRef(null); - const [initialised, setInitialised] = useState(false); const [startPosition, setStartPosition] = useState(modal.position); - - const [screenWidth, setScreenWidth] = useState(100); - const [screenHeight, setScreenHeight] = useState(100); + const [screenWidth, screenHeight] = useScreenDimensions(); useEffect(() => { - const resizeObserver = new ResizeObserver((event) => { - setScreenWidth(event[0].contentBoxSize[0].inlineSize); - setScreenHeight(event[0].contentBoxSize[0].blockSize); - setInitialised(true); - }); - - resizeObserver.observe(document.getElementById("root")); - }, []); - - useEffect(() => { - if (!initialised) + if (screenWidth == null || screenHeight == null) return; if (modal.size.x > screenWidth || modal.size.y > screenHeight) { @@ -45,7 +33,7 @@ export function DialogBox({ modal, params }) { setStartPosition(modal.position); } } - }, [initialised, modal, screenHeight, screenWidth]); + }, [modal, screenHeight, screenWidth]); const onClick = (event) => { event.preventDefault(); diff --git a/src/components/task-bar/TaskBar.jsx b/src/components/task-bar/TaskBar.jsx index f020b51..3d5ae90 100644 --- a/src/components/task-bar/TaskBar.jsx +++ b/src/components/task-bar/TaskBar.jsx @@ -1,7 +1,7 @@ import { memo, useRef, useState } from "react"; import styles from "./TaskBar.module.css"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faSearch } from "@fortawesome/free-solid-svg-icons"; +import { faCog, faSearch } from "@fortawesome/free-solid-svg-icons"; import AppsManager from "../../features/applications/applications.js"; import { ReactSVG } from "react-svg"; import { HomeMenu } from "./menus/HomeMenu.jsx"; @@ -13,24 +13,36 @@ import { SearchMenu } from "./menus/SearchMenu.jsx"; import { Calendar } from "./indicators/Calendar.jsx"; import { useScrollWithShadow } from "../../hooks/utils/scrollWithShadows.js"; import { AppButton } from "./AppButton.jsx"; +import { useContextMenu } from "../../hooks/modals/contextMenu.js"; +import { Actions } from "../actions/Actions.jsx"; +import { useModals } from "../../hooks/modals/modals.js"; +import { ClickAction } from "../actions/actions/ClickAction.jsx"; +import { APPS, APP_NAMES } from "../../constants/applications.js"; +import { useWindowsManager } from "../../hooks/windows/windowsManagerContext.js"; +import { ModalsView } from "../modals/ModalsView.jsx"; +import { TASK_BAR_HEIGHT } from "../../constants/taskBar.js"; export const Taskbar = memo(() => { const [showHome, setShowHome] = useState(false); const [showSearch, setShowSearch] = useState(false); const [searchQuery, setSearchQuery] = useState(""); const ref = useRef(null); - const { boxShadow, onUpdate } = useScrollWithShadow({ - ref, - shadow: { - offset: 20, - blurRadius: 10, - spreadRadius: -10, - color: { - a: 25 - } - } - }); + const { boxShadow, onUpdate } = useScrollWithShadow({ ref, shadow: { + offset: 20, + blurRadius: 10, + spreadRadius: -10, + color: { a: 25 } + } }); const inputRef = useRef(null); + const [modalsManager, modals] = useModals(); + const windowsManager = useWindowsManager(); + const { onContextMenu } = useContextMenu({ modalsManager, Actions: (props) => + + { + windowsManager.open(APPS.SETTINGS); + }}/> + + }); const updateShowHome = (value) => { setShowHome(value); @@ -67,8 +79,17 @@ export const Taskbar = memo(() => { updateShowSearch(true); }; - return ( -
+ return (<> + +
{ + if (event.target.getAttribute("data-allow-context-menu")) + onContextMenu(event); + }} + >
{ updateShowHome(false); }}> @@ -103,8 +124,14 @@ export const Taskbar = memo(() => {
-
-
+
+
{AppsManager.APPLICATIONS.map((app) => )} @@ -118,5 +145,5 @@ export const Taskbar = memo(() => {
- ); + ); }, []); \ No newline at end of file diff --git a/src/components/utils/Button.jsx b/src/components/utils/Button.jsx index 47e53ff..3270d28 100644 --- a/src/components/utils/Button.jsx +++ b/src/components/utils/Button.jsx @@ -1,18 +1,39 @@ +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import styles from "./Button.module.css"; +import { faExternalLink } from "@fortawesome/free-solid-svg-icons"; /** * @param {object} props + * @param {string} props.className + * @param {string} props.href */ export function Button(props) { - const { className = "", children } = props; + let { className = "" } = props; + const { href, children } = props; - return ( - - ); + + ); + } else { + return (); + } } \ No newline at end of file diff --git a/src/components/utils/Button.module.css b/src/components/utils/Button.module.css index 5dd37c1..831170f 100644 --- a/src/components/utils/Button.module.css +++ b/src/components/utils/Button.module.css @@ -1,8 +1,25 @@ .Button { - color: var(--foreground-color-a); - background: none; + --text-color: var(--foreground-color-a); + --normal-color: var(--background-color-a); + --hover-color: var(--background-color-b); + + color: var(--text-color); + background-color: var(--normal-color); border: none; outline: none; + font-size: 0.875em; transition: background-color 100ms ease-in-out; cursor: pointer; +} + +.Button:hover, .Button:focus-visible { + background-color: var(--hover-color); +} + +.Button-link { + text-decoration: none; +} + +.Button-link > svg { + margin-left: 0.5rem; } \ No newline at end of file diff --git a/src/components/windows/WindowView.jsx b/src/components/windows/WindowView.jsx index 2e4e82d..27157f7 100644 --- a/src/components/windows/WindowView.jsx +++ b/src/components/windows/WindowView.jsx @@ -1,6 +1,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import styles from "./WindowView.module.css"; -import { faMinus, faWindowMaximize as fasWindowMaximize, faXmark } from "@fortawesome/free-solid-svg-icons"; +import { faExpand, faMinus, faWindowMaximize as fasWindowMaximize, faTimes, faXmark } from "@fortawesome/free-solid-svg-icons"; import { ReactSVG } from "react-svg"; import { useWindowsManager } from "../../hooks/windows/windowsManagerContext.js"; import Draggable from "react-draggable"; @@ -13,6 +13,9 @@ import { useModals } from "../../hooks/modals/modals.js"; import { ModalsView } from "../modals/ModalsView.jsx"; import { useContextMenu } from "../../hooks/modals/contextMenu.js"; import AppsManager from "../../features/applications/applications.js"; +import { ClickAction } from "../actions/actions/ClickAction.jsx"; +import { Actions } from "../actions/Actions.jsx"; +import { useScreenDimensions } from "../../hooks/utils/screen.js"; /** * @typedef {object} windowProps @@ -41,43 +44,30 @@ export const WindowView = memo(({ id, app, size, position, onInteract, options, const nodeRef = useRef(null); const [modalsManager, modals] = useModals(); - const [initialised, setInitialised] = useState(false); const [startSize, setStartSize] = useState(size); const [startPosition, setStartPosition] = useState(position); const [maximized, setMaximized] = useState(false); const [minimized, setMinimized] = useState(false); - const [screenWidth, setScreenWidth] = useState(100); - const [screenHeight, setScreenHeight] = useState(100); + const [screenWidth, screenHeight] = useScreenDimensions(); const [title, setTitle] = useState(app.name); const [iconUrl, setIconUrl] = useState(AppsManager.getAppIconUrl(app.id)); - const { onContextMenu } = useContextMenu({ - modalsManager, - options: { - "Maximize": () => { setMaximized(!maximized); }, - "Close": () => { close(); } - }, - shortcuts: { - "Maximize": ["F11"], - "Close": ["Control", "q"] - } + const { onContextMenu, ShortcutsListener } = useContextMenu({ modalsManager, Actions: (props) => + + { + setMaximized(!maximized); + }}/> + { + close(); + }}/> + }); useEffect(() => { - const resizeObserver = new ResizeObserver((event) => { - setScreenWidth(event[0].contentBoxSize[0].inlineSize); - setScreenHeight(event[0].contentBoxSize[0].blockSize); - setInitialised(true); - }); - - resizeObserver.observe(document.getElementById("root")); - }, []); - - useEffect(() => { - if (!initialised) + if (screenWidth == null || screenHeight == null) return; if (size.x > screenWidth || size.y > screenHeight) { @@ -94,7 +84,7 @@ export const WindowView = memo(({ id, app, size, position, onInteract, options, setStartPosition(position); } } - }, [initialised, position, size, screenHeight, screenWidth]); + }, [position, size, screenHeight, screenWidth]); const close = (event) => { event?.preventDefault(); @@ -119,6 +109,7 @@ export const WindowView = memo(({ id, app, size, position, onInteract, options, classNames.push(styles.Minimized); return (<> + >} props.options - * @param {Object>} props.shortcuts - * @returns {{ onContextMenu: onContextMenuType }} + * @param {import("../../components/actions/Actions.jsx").actionsType} props.Actions + * @returns {{ + * onContextMenu: onContextMenuType, + * ShortcutsListener: import("../../components/actions/Actions.jsx").actionsType + * }} */ -export function useContextMenu({ modalsManager, options, shortcuts }) { +export function useContextMenu({ modalsManager, Actions }) { // Open a new modal when context menu is triggered const onContextMenu = useCallback((event, params = {}) => { event.preventDefault(); @@ -34,15 +35,21 @@ export function useContextMenu({ modalsManager, options, shortcuts }) { positionY -= containerRect.y / 2; } - const newModal = new Modal(ContextMenu) + const newModal = new Modal(Actions) .setPosition(new Vector2(positionX, positionY)) - .setProps({ options, shortcuts, params }); + .setProps({ + triggerParams: params, + className: STYLES.CONTEXT_MENU, + onAnyTrigger: () => { + newModal.close(); + } + }); modalsManager.open(newModal); return newModal; - }, [modalsManager, options, shortcuts]); + }, [Actions, modalsManager]); - useShortcuts({ options, shortcuts, useCategories: false }); + const ShortcutsListener = () => <>; - return { onContextMenu }; + return { onContextMenu, ShortcutsListener }; } \ No newline at end of file diff --git a/src/hooks/utils/mouse.js b/src/hooks/utils/mouse.js index d26e91d..7addb49 100644 --- a/src/hooks/utils/mouse.js +++ b/src/hooks/utils/mouse.js @@ -1,7 +1,7 @@ import { useEffect } from "react"; /** - * @param {Object} params + * @param {object} params * @param {Function} params.onMouseDown * @param {Function} params.onMouseUp * @param {Function} params.onClick diff --git a/src/hooks/utils/screen.js b/src/hooks/utils/screen.js new file mode 100644 index 0000000..1524028 --- /dev/null +++ b/src/hooks/utils/screen.js @@ -0,0 +1,20 @@ +import { useEffect, useState } from "react"; + +/** + * @returns {[number, number]} + */ +export function useScreenDimensions() { + const [screenWidth, setScreenWidth] = useState(null); + const [screenHeight, setScreenHeight] = useState(null); + + useEffect(() => { + const resizeObserver = new ResizeObserver((event) => { + setScreenWidth(event[0].contentBoxSize[0].inlineSize); + setScreenHeight(event[0].contentBoxSize[0].blockSize); + }); + + resizeObserver.observe(document.getElementById("root")); + }, []); + + return [screenWidth, screenHeight]; +} \ No newline at end of file