diff --git a/lib/components/ImageButton.tsx b/lib/components/ImageButton.tsx new file mode 100644 index 0000000..be3b80e --- /dev/null +++ b/lib/components/ImageButton.tsx @@ -0,0 +1,230 @@ +/** + * @file + * @copyright 2024 Aylong (https://github.com/AyIong) + * @license MIT + */ + +import { Placement } from '@popperjs/core'; +import { ReactNode } from 'react'; + +import { BooleanLike, classes } from '../common/react'; +import styles from '../styles/components/ImageButton.module.scss'; +import { BoxProps, computeBoxProps } from './Box'; +import { DmIcon } from './DmIcon'; +import { Icon } from './Icon'; +import { Image } from './Image'; +import { Stack } from './Stack'; +import { Tooltip } from './Tooltip'; + +type Props = Partial<{ + /** Asset cache. Example: `asset={['assetname32x32', thing.key]}` */ + asset: string[]; + /** Classic way to put images. Example: `base64={thing.image}` */ + base64: string; + /** + * Special container for buttons. + * You can put any other component here. + * Has some special stylings! + * Example: `buttons={}` + */ + buttons: ReactNode; + /** + * Enables alternate layout for `buttons` container. + * Without fluid, buttons will be on top and with `pointer-events: none`, useful for text info. + * With fluid, buttons will be in "hamburger" style. + */ + buttonsAlt: boolean; + /** Content under image. Or on the right if fluid. */ + children: ReactNode; + /** Applies a CSS class to the element. */ + className: string; + /** Color of the button. See [Button](#button) but without `transparent`. */ + color: string; + /** Makes button disabled and dark red if true. Also disables onClick. */ + disabled: BooleanLike; + /** Optional. Adds a "stub" when loading DmIcon. */ + dmFallback: ReactNode; + /** Parameter `icon` of component `DmIcon`. */ + dmIcon: string | null; + /** Parameter `icon_state` of component `DmIcon`. */ + dmIconState: string | null; + /** + * Changes the layout of the button, making it fill the entire horizontally available space. + * Allows the use of `title` + */ + fluid: boolean; + /** Parameter responsible for the size of the image, component and standard "stubs". */ + imageSize: number; + /** Prop `src` of Image component. Example: `imageSrc={resolveAsset(thing.image}` */ + imageSrc: string; + /** Called when button is clicked with LMB. */ + onClick: (e: any) => void; + /** Called when button is clicked with RMB. */ + onRightClick: (e: any) => void; + /** Makes button selected and green if true. */ + selected: BooleanLike; + /** Requires `fluid` for work. Bold text with divider betwen content. */ + title: string; + /** A fancy, boxy tooltip, which appears when hovering over the button */ + tooltip: ReactNode; + /** Position of the tooltip. See [`Popper`](#Popper) for valid options. */ + tooltipPosition: Placement; +}> & + BoxProps; + +export function ImageButton(props: Props) { + const { + asset, + base64, + buttons, + buttonsAlt, + children, + className, + color, + disabled, + dmFallback, + dmIcon, + dmIconState, + fluid, + imageSize = 64, + imageSrc, + onClick, + onRightClick, + selected, + title, + tooltip, + tooltipPosition, + ...rest + } = props; + + function getFallback(iconName: string, iconSpin: boolean) { + return ( + + + + + + ); + } + + let buttonContent = ( +
{ + if (!disabled && onClick) { + onClick(event); + } + }} + onContextMenu={(event) => { + event.preventDefault(); + if (!disabled && onRightClick) { + onRightClick(event); + } + }} + style={{ width: !fluid ? `calc(${imageSize}px + 0.5em + 2px)` : 'auto' }} + > +
+ {base64 || asset || imageSrc ? ( + + ) : dmIcon && dmIconState ? ( + + ) : ( + getFallback('question', false) + )} +
+ {fluid ? ( +
+ {title && ( + + {title} + + )} + {children && ( + {children} + )} +
+ ) : ( + children && ( + + {children} + + ) + )} +
+ ); + + if (tooltip) { + buttonContent = ( + + {buttonContent} + + ); + } + + return ( +
+ {buttonContent} + {buttons && ( +
+ {buttons} +
+ )} +
+ ); +} diff --git a/lib/components/index.ts b/lib/components/index.ts index 0bdfaa9..a06d765 100644 --- a/lib/components/index.ts +++ b/lib/components/index.ts @@ -18,6 +18,7 @@ export { FitText } from './FitText'; export { Flex } from './Flex'; export { Icon } from './Icon'; export { Image } from './Image'; +export { ImageButton } from './ImageButton'; export { InfinitePlane } from './InfinitePlane'; export { Input } from './Input'; export { KeyListener } from './KeyListener'; diff --git a/lib/styles/components/ImageButton.module.scss b/lib/styles/components/ImageButton.module.scss new file mode 100644 index 0000000..cb48539 --- /dev/null +++ b/lib/styles/components/ImageButton.module.scss @@ -0,0 +1,258 @@ +/** + * @file + * @copyright 2024 Aylong (https://github.com/AyIong) + * @license MIT + */ + +@use '../base.scss'; +@use '../colors.scss'; +@use '../functions.scss' as *; + +$color-default: colors.bg(base.$color-bg-section) !default; +$color-disabled: #631d1d !default; +$color-selected: colors.bg(colors.$green) !default; +$color-divider: rgba(255, 255, 255, 0.1) !default; +$divider-thickness: base.em(2px) !default; +$bg-map: colors.$bg-map !default; + +@mixin button-style( + $color, + $border-color: rgba(lighten($color, 50%), 0.2), + $border-width: 1px 0 0 0, + $opacity: 0.2, + $hoverable: true, + $transition-duration: 0.2s +) { + $luminance: luminance($color); + $text-color: if($luminance > 0.3, rgba(0, 0, 0, 1), rgba(255, 255, 255, 1)); + + background-color: rgba($color, $opacity); + color: $text-color; + border: solid $border-color; + border-width: $border-width; + transition: + background-color $transition-duration, + border-color $transition-duration; + + @if $hoverable { + &:hover { + background-color: rgba(lighten($color, 50%), $opacity); + } + } +} + +@each $color-name, $color-value in $bg-map { + .color__#{$color-name} { + @include button-style($color-value, $border-width: 1px); + } + + .contentColor__#{$color-name} { + @include button-style($color-value, $border-color: lighten($color-value, 25%), $opacity: 1, $hoverable: false); + } + + .buttonsContainerColor__#{$color-name} { + @include button-style( + $color-value, + $border-width: 1px 1px 1px 0, + $opacity: 0.33, + $hoverable: false, + $transition-duration: 0 + ); + } +} + +.color__default { + @include button-style(lighten($color-default, 85%), $border-width: 1px); +} + +.disabled { + background-color: rgba($color-disabled, 0.25) !important; + border-color: rgba($color-disabled, 0.25) !important; +} + +.selected { + @include button-style($color-selected, $border-color: rgba($color-selected, 0.25), $border-width: 1px); +} + +.contentColor__default { + @include button-style( + lighten($color-default, 80%), + $border-color: lighten($color-default, 100%), + $opacity: 1, + $hoverable: false + ); +} + +.contentDisabled { + background-color: $color-disabled !important; + border-top: 1px solid lighten($color-disabled, 25%) !important; +} + +.contentSelected { + @include button-style($color-selected, $border-color: lighten($color-selected, 25%), $opacity: 1, $hoverable: false); +} + +.buttonsContainerColor__default { + @include button-style( + lighten($color-default, 85%), + $border-width: 1px 1px 1px 0, + $hoverable: false, + $transition-duration: 0 + ); +} + +.ImageButton { + display: inline-table; + position: relative; + text-align: center; + margin: 0.25em; + user-select: none; + -ms-user-select: none; + + .noAction { + pointer-events: none; + } + + .container { + display: flex; + flex-direction: column; + border-radius: 0.33em; + } + + .image { + position: relative; + align-self: center; + pointer-events: none; + overflow: hidden; + line-height: 0; + padding: 0.25em; + border-radius: 0.33em; + } + + .buttonsContainer { + display: flex; + position: absolute; + overflow: hidden; + left: 1px; + bottom: 1.8em; + max-width: 100%; + z-index: 1; + + &.buttonsAltContainer { + overflow: visible; + flex-direction: column; + pointer-events: none; + top: 1px; + bottom: inherit !important; + } + + &.buttonsEmpty { + bottom: 1px; + } + + & > * { + /* I know !important is bad, but here's no other way */ + margin: 0 !important; + padding: 0 0.2em !important; + border-radius: 0 !important; + } + } + + .content { + -ms-user-select: none; + user-select: none; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding: 0.25em 0.5em; + margin: -1px; + border-radius: 0 0 0.33em 0.33em; + z-index: 2; + } +} + +.fluid { + display: flex; + flex-direction: row; + position: relative; + text-align: center; + margin: 0 0 0.5em 0; + user-select: none; + -ms-user-select: none; + + &:last-child { + margin-bottom: 0; + } + + .info { + display: flex; + flex-direction: column; + justify-content: center; + flex: 1; + } + + .title { + font-weight: bold; + padding: 0.5em; + + &.divider { + margin: 0 0.5em; + border-bottom: $divider-thickness solid $color-divider; + } + } + + .contentFluid { + padding: 0.5em; + color: white; + } + + .container { + flex-direction: row; + flex: 1; + + &.hasButtons { + border-radius: 0.33em 0 0 0.33em; + border-width: 1px 0 1px 1px; + } + } + + .image { + padding: 0; + } + + .buttonsContainer { + position: relative; + left: inherit; + bottom: inherit; + border-radius: 0 0.33em 0.33em 0; + + &.buttonsEmpty { + bottom: inherit; + } + + &.buttonsAltContainer { + overflow: hidden; + pointer-events: auto; + top: inherit; + + & > * { + border-top: 1px solid rgba(255, 255, 255, 0.075); + + &:first-child { + border-top: 0; + } + } + } + + & > * { + display: inline-flex; + flex-direction: column; + justify-content: center; + text-align: center; + white-space: pre-wrap; + line-height: base.em(14px); + height: 100%; + border-left: 1px solid rgba(255, 255, 255, 0.075); + } + } +} diff --git a/lib/styles/components/ImageButton.module.scss.d.ts b/lib/styles/components/ImageButton.module.scss.d.ts new file mode 100644 index 0000000..40c157f --- /dev/null +++ b/lib/styles/components/ImageButton.module.scss.d.ts @@ -0,0 +1,105 @@ +declare const classNames: { + readonly ImageButton: "ImageButton"; + readonly buttonsAltContainer: "buttonsAltContainer"; + readonly buttonsContainer: "buttonsContainer"; + readonly buttonsContainerColor__average: "buttonsContainerColor__average"; + readonly buttonsContainerColor__bad: "buttonsContainerColor__bad"; + readonly buttonsContainerColor__black: "buttonsContainerColor__black"; + readonly buttonsContainerColor__blue: "buttonsContainerColor__blue"; + readonly buttonsContainerColor__brown: "buttonsContainerColor__brown"; + readonly buttonsContainerColor__caution: "buttonsContainerColor__caution"; + readonly buttonsContainerColor__danger: "buttonsContainerColor__danger"; + readonly buttonsContainerColor__default: "buttonsContainerColor__default"; + readonly buttonsContainerColor__good: "buttonsContainerColor__good"; + readonly buttonsContainerColor__green: "buttonsContainerColor__green"; + readonly buttonsContainerColor__grey: "buttonsContainerColor__grey"; + readonly "buttonsContainerColor__light-grey": "buttonsContainerColor__light-grey"; + readonly buttonsContainerColor__olive: "buttonsContainerColor__olive"; + readonly buttonsContainerColor__orange: "buttonsContainerColor__orange"; + readonly buttonsContainerColor__pink: "buttonsContainerColor__pink"; + readonly buttonsContainerColor__purple: "buttonsContainerColor__purple"; + readonly buttonsContainerColor__red: "buttonsContainerColor__red"; + readonly buttonsContainerColor__teal: "buttonsContainerColor__teal"; + readonly buttonsContainerColor__violet: "buttonsContainerColor__violet"; + readonly buttonsContainerColor__white: "buttonsContainerColor__white"; + readonly buttonsContainerColor__yellow: "buttonsContainerColor__yellow"; + readonly buttonsEmpty: "buttonsEmpty"; + readonly colorButtonsContainer__average: "buttonsContainerColor__average"; + readonly colorButtonsContainer__bad: "buttonsContainerColor__bad"; + readonly colorButtonsContainer__black: "buttonsContainerColor__black"; + readonly colorButtonsContainer__blue: "buttonsContainerColor__blue"; + readonly colorButtonsContainer__brown: "buttonsContainerColor__brown"; + readonly colorButtonsContainer__caution: "buttonsContainerColor__caution"; + readonly colorButtonsContainer__danger: "buttonsContainerColor__danger"; + readonly colorButtonsContainer__default: "buttonsContainerColor__default"; + readonly colorButtonsContainer__good: "buttonsContainerColor__good"; + readonly colorButtonsContainer__green: "buttonsContainerColor__green"; + readonly colorButtonsContainer__grey: "buttonsContainerColor__grey"; + readonly "colorButtonsContainer__light-grey": "buttonsContainerColor__light-grey"; + readonly colorButtonsContainer__olive: "buttonsContainerColor__olive"; + readonly colorButtonsContainer__orange: "buttonsContainerColor__orange"; + readonly colorButtonsContainer__pink: "buttonsContainerColor__pink"; + readonly colorButtonsContainer__purple: "buttonsContainerColor__purple"; + readonly colorButtonsContainer__red: "buttonsContainerColor__red"; + readonly colorButtonsContainer__teal: "buttonsContainerColor__teal"; + readonly colorButtonsContainer__violet: "buttonsContainerColor__violet"; + readonly colorButtonsContainer__white: "buttonsContainerColor__white"; + readonly colorButtonsContainer__yellow: "buttonsContainerColor__yellow"; + readonly colorContent__average: "contentColor__average"; + readonly colorContent__bad: "contentColor__bad"; + readonly colorContent__black: "contentColor__black"; + readonly colorContent__blue: "contentColor__blue"; + readonly colorContent__brown: "contentColor__brown"; + readonly colorContent__caution: "contentColor__caution"; + readonly colorContent__danger: "contentColor__danger"; + readonly colorContent__default: "contentColor__default"; + readonly colorContent__good: "contentColor__good"; + readonly colorContent__green: "contentColor__green"; + readonly colorContent__grey: "contentColor__grey"; + readonly "colorContent__light-grey": "contentColor__light-grey"; + readonly colorContent__olive: "contentColor__olive"; + readonly colorContent__orange: "contentColor__orange"; + readonly colorContent__pink: "contentColor__pink"; + readonly colorContent__purple: "contentColor__purple"; + readonly colorContent__red: "contentColor__red"; + readonly colorContent__teal: "contentColor__teal"; + readonly colorContent__violet: "contentColor__violet"; + readonly colorContent__white: "contentColor__white"; + readonly colorContent__yellow: "contentColor__yellow"; + readonly color__average: "color__average"; + readonly color__bad: "color__bad"; + readonly color__black: "color__black"; + readonly color__blue: "color__blue"; + readonly color__brown: "color__brown"; + readonly color__caution: "color__caution"; + readonly color__danger: "color__danger"; + readonly color__default: "color__default"; + readonly color__good: "color__good"; + readonly color__green: "color__green"; + readonly color__grey: "color__grey"; + readonly "color__light-grey": "color__light-grey"; + readonly color__olive: "color__olive"; + readonly color__orange: "color__orange"; + readonly color__pink: "color__pink"; + readonly color__purple: "color__purple"; + readonly color__red: "color__red"; + readonly color__teal: "color__teal"; + readonly color__violet: "color__violet"; + readonly color__white: "color__white"; + readonly color__yellow: "color__yellow"; + readonly container: "container"; + readonly content: "content"; + readonly contentDisabled: "contentDisabled"; + readonly contentFluid: "contentFluid"; + readonly contentSelected: "contentSelected"; + readonly disabled: "disabled"; + readonly divider: "divider"; + readonly fluid: "fluid"; + readonly hasButtons: "hasButtons"; + readonly image: "image"; + readonly info: "info"; + readonly noAction: "noAction"; + readonly selected: "selected"; + readonly title: "title"; +}; +export = classNames;