diff --git a/src/renderer/components/+catalog/catalog.tsx b/src/renderer/components/+catalog/catalog.tsx index 76339e79a3..aaa4e6e7e7 100644 --- a/src/renderer/components/+catalog/catalog.tsx +++ b/src/renderer/components/+catalog/catalog.tsx @@ -28,7 +28,7 @@ import { action, makeObservable, observable, reaction, runInAction, when } from import { CatalogEntityItem, CatalogEntityStore } from "./catalog-entity.store"; import { navigate } from "../../navigation"; import { MenuItem, MenuActions } from "../menu"; -import type { CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../api/catalog-entity"; +import { CatalogEntityContextMenu, CatalogEntityContextMenuContext, catalogEntityRunContext } from "../../api/catalog-entity"; import { HotbarStore } from "../../../common/hotbar-store"; import { ConfirmDialog } from "../confirm-dialog"; import { catalogCategoryRegistry, CatalogEntity } from "../../../common/catalog"; @@ -41,7 +41,7 @@ import { makeCss } from "../../../common/utils/makeCss"; import { CatalogEntityDetails } from "./catalog-entity-details"; import { catalogURL, CatalogViewRouteParam } from "../../../common/routes"; import { CatalogMenu } from "./catalog-menu"; -import { HotbarIcon } from "../hotbar/hotbar-icon"; +import { EntityIcon } from "../entity-icon"; export const previousActiveTab = createAppStorage("catalog-previous-active-tab", ""); @@ -172,13 +172,14 @@ export class Catalog extends React.Component { renderIcon(item: CatalogEntityItem) { return ( - item.onRun(catalogEntityRunContext)} + hoverWidth="1.5px" size={24} /> ); diff --git a/src/renderer/components/avatar/avatar.tsx b/src/renderer/components/avatar/avatar.tsx index 4164b65261..c2028bbb99 100644 --- a/src/renderer/components/avatar/avatar.tsx +++ b/src/renderer/components/avatar/avatar.tsx @@ -25,7 +25,7 @@ import GraphemeSplitter from "grapheme-splitter"; import { Avatar as MaterialAvatar, AvatarTypeMap } from "@material-ui/core"; import { iter } from "../../utils"; -interface Props extends DOMAttributes, Partial { +export interface AvatarProps extends DOMAttributes, Partial { title: string; colorHash?: string; width?: number; @@ -33,6 +33,7 @@ interface Props extends DOMAttributes, Partial { src?: string; className?: string; background?: string; + style?: React.CSSProperties; } function getNameParts(name: string): string[] { @@ -69,8 +70,8 @@ function getIconString(title: string) { ].filter(Boolean).join(""); } -export function Avatar(props: Props) { - const { title, width = 32, height = 32, colorHash, children, background, ...settings } = props; +export function Avatar(props: AvatarProps) { + const { title, width = 32, height = 32, colorHash, children, style, background, ...settings } = props; const getBackgroundColor = () => { if (background) { @@ -89,7 +90,8 @@ export function Avatar(props: Props) { backgroundColor: getBackgroundColor(), width, height, - textTransform: "uppercase" + textTransform: "uppercase", + ...style, }; }; diff --git a/src/renderer/components/entity-icon/entity-icon.scss b/src/renderer/components/entity-icon/entity-icon.scss new file mode 100644 index 0000000000..6b657ecec8 --- /dev/null +++ b/src/renderer/components/entity-icon/entity-icon.scss @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +.EntityIcon { + &.active { + box-shadow: 0 0 0px var(--hover-width) var(--clusterMenuBackground), 0 0 0px calc(2*var(--hover-width)) var(--textColorAccent); + } + + &:hover { + &:not(.active) { + box-shadow: 0 0 0px var(--hover-width) var(--clusterMenuBackground), 0 0 0px calc(2*var(--hover-width)) #ffffff50; + } + } +} diff --git a/src/renderer/components/entity-icon/entity-icon.tsx b/src/renderer/components/entity-icon/entity-icon.tsx new file mode 100644 index 0000000000..965ca0a837 --- /dev/null +++ b/src/renderer/components/entity-icon/entity-icon.tsx @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import "./entity-icon.scss"; + +import React, { DOMAttributes } from "react"; +import { cssNames, IClassName } from "../../utils"; +import { Avatar } from "../avatar/avatar"; +import { Icon } from "../icon"; + +export interface EntityIconProps extends DOMAttributes { + title: string; + size?: number; + source: string; + src?: string; + material?: string; + background?: string; + active?: boolean; + className?: IClassName; + hoverWidth?: string; +} + +export function EntityIcon({ active, size = 40, hoverWidth = "3px", material, className, ...props }: EntityIconProps) { + return ( + + {material && } + + ); +} diff --git a/src/renderer/components/entity-icon/index.ts b/src/renderer/components/entity-icon/index.ts new file mode 100644 index 0000000000..1521120bd3 --- /dev/null +++ b/src/renderer/components/entity-icon/index.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +export * from "./entity-icon"; diff --git a/src/renderer/components/hotbar/hotbar-entity-icon.tsx b/src/renderer/components/hotbar/hotbar-entity-icon.tsx index fc6628f3eb..ee6cb48c37 100644 --- a/src/renderer/components/hotbar/hotbar-entity-icon.tsx +++ b/src/renderer/components/hotbar/hotbar-entity-icon.tsx @@ -31,6 +31,7 @@ import { cssNames, IClassName } from "../../utils"; import { Icon } from "../icon"; import { HotbarIcon } from "./hotbar-icon"; import { HotbarStore } from "../../../common/hotbar-store"; +import { catalogEntityRunContext } from "../../api/catalog-entity"; interface Props extends DOMAttributes { entity: CatalogEntity; @@ -44,20 +45,16 @@ interface Props extends DOMAttributes { @observer export class HotbarEntityIcon extends React.Component { - @observable private contextMenu: CatalogEntityContextMenuContext; + @observable private contextMenu: CatalogEntityContextMenuContext = { + menuItems: [], + navigate: (url: string) => navigate(url), + }; constructor(props: Props) { super(props); makeObservable(this); } - componentDidMount() { - this.contextMenu = { - menuItems: [], - navigate: (url: string) => navigate(url), - }; - } - get kindIcon() { const className = "badge"; const category = catalogCategoryRegistry.getCategoryForEntity(this.props.entity); @@ -92,20 +89,17 @@ export class HotbarEntityIcon extends React.Component { } render() { - if (!this.contextMenu) { - return null; - } - const { entity, errorClass, add, remove, - index, children, ...elemProps + index, children, className, ...elemProps } = this.props; - const className = cssNames("HotbarEntityIcon", this.props.className, { + const classNames = cssNames("HotbarEntityIcon", className, { interactive: true, active: this.isActive(entity), disabled: !entity }); + console.log(entity); const isPersisted = this.isPersisted(entity); const onOpen = async () => { const menuItems: CatalogEntityContextMenu[] = []; @@ -136,11 +130,12 @@ export class HotbarEntityIcon extends React.Component { src={entity.spec.icon?.src} material={entity.spec.icon?.material} background={entity.spec.icon?.background} - className={className} + className={classNames} active={isActive} onMenuOpen={onOpen} menuItems={this.contextMenu.menuItems} tooltip={`${entity.metadata.name} (${entity.metadata.source})`} + onClick={() => entity.onRun(catalogEntityRunContext)} {...elemProps} > { this.ledIcon } diff --git a/src/renderer/components/hotbar/hotbar-icon.scss b/src/renderer/components/hotbar/hotbar-icon.scss index 2f9ad6a696..cec2c18bf3 100644 --- a/src/renderer/components/hotbar/hotbar-icon.scss +++ b/src/renderer/components/hotbar/hotbar-icon.scss @@ -64,23 +64,6 @@ box-shadow: none; } - div.MuiAvatar-root { - width: var(--size); - height: var(--size); - - &.active { - box-shadow: 0 0 0px 3px var(--clusterMenuBackground), 0 0 0px 6px var(--textColorAccent); - } - - &.interactive { - &:hover { - &:not(.active) { - box-shadow: 0 0 0px 3px var(--clusterMenuBackground), 0 0 0px 6px #ffffff50; - } - } - } - } - .led { position: absolute; left: 3px; diff --git a/src/renderer/components/hotbar/hotbar-icon.tsx b/src/renderer/components/hotbar/hotbar-icon.tsx index b8447baf80..5f085cf281 100644 --- a/src/renderer/components/hotbar/hotbar-icon.tsx +++ b/src/renderer/components/hotbar/hotbar-icon.tsx @@ -28,9 +28,8 @@ import { cssNames, IClassName } from "../../utils"; import { ConfirmDialog } from "../confirm-dialog"; import { Menu, MenuItem } from "../menu"; import { observer } from "mobx-react"; -import { Avatar } from "../avatar/avatar"; -import { Icon } from "../icon"; -import { Tooltip } from "../tooltip"; +import { EntityIcon } from "../entity-icon"; +import { Tooltip } from "@material-ui/core"; export interface HotbarIconProps extends DOMAttributes { uid: string; @@ -74,34 +73,27 @@ export const HotbarIcon = observer(({menuItems = [], size = 40, tooltip, ...prop setMenuOpen(!menuOpen); }; - const renderIcon = () => { - return ( - { - if (!disabled) { - onClick?.(event); - } - }} - > - {material && } - - ); - }; - return ( -
0 })}> - {tooltip && {tooltip}} -
- {renderIcon()} - {children} -
+
+ +
+ { + if (!disabled) { + onClick?.(event); + } + }} + {...rest} + /> + {children} +
+
{ > {item && ( - {(provided, snapshot) => { - const style = { - zIndex: defaultHotbarCells - index, - position: "absolute", - ...provided.draggableProps.style, - } as React.CSSProperties; - - return ( -
- {entity ? ( - entity.onRun(catalogEntityRunContext)} - className={cssNames({ isDragging: snapshot.isDragging })} - remove={this.removeItem} - add={this.addItem} - size={40} - /> - ) : ( - this.removeItem(item.entity.uid) - } - ]} - disabled - size={40} - /> - )} -
- ); - }} + {(provided, snapshot) => ( +
+ {entity ? ( + + ) : ( + this.removeItem(item.entity.uid) + } + ]} + disabled + size={40} + /> + )} +
+ )}
)} {provided.placeholder} diff --git a/src/renderer/utils/cssNames.ts b/src/renderer/utils/cssNames.ts index 7bf4cc4032..a6deaa0fcf 100755 --- a/src/renderer/utils/cssNames.ts +++ b/src/renderer/utils/cssNames.ts @@ -21,25 +21,24 @@ // Helper for combining css classes inside components -export type IClassName = string | string[] | IClassNameMap; -export type IClassNameMap = { - [className: string]: boolean | any; -}; +export type IClassName = string | string[] | Record | undefined | null; export function cssNames(...args: IClassName[]): string { - const map: IClassNameMap = {}; + const names: string[] = []; - args.forEach(className => { - if (typeof className === "string" || Array.isArray(className)) { - [].concat(className).forEach(name => map[name] = true); + for (const arg of args) { + if (typeof arg === "string") { + names.push(arg.trim()); + } else if (Array.isArray(arg)) { + names.push(...arg.map(name => name.trim())); + } else if (arg && typeof arg === "object") { + for (const [name, isActive] of Object.entries(arg)) { + if (isActive) { + names.push(name.trim()); + } + } } - else { - Object.assign(map, className); - } - }); + } - return Object.entries(map) - .filter(([, isActive]) => !!isActive) - .map(([className]) => className.trim()) - .join(" "); + return names.filter(Boolean).join(" "); }