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={Send }`
+ */
+ 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;