Skip to content

Commit

Permalink
Little ImageButton tweaks (#53)
Browse files Browse the repository at this point in the history
  • Loading branch information
AyIong authored Jan 4, 2025
1 parent 9bc0a9d commit a3df855
Show file tree
Hide file tree
Showing 4 changed files with 240 additions and 15 deletions.
2 changes: 1 addition & 1 deletion lib/components/DmIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { ReactNode } from 'react';
import type { BoxProps } from './Box';
import { Image } from './Image';

enum Direction {
export enum Direction {
NORTH = 1,
SOUTH = 2,
EAST = 4,
Expand Down
44 changes: 35 additions & 9 deletions lib/components/ImageButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type { ReactNode } from 'react';
import { type BooleanLike, classes } from '../common/react';
import { computeBoxProps } from '../common/ui';
import type { BoxProps } from './Box';
import { DmIcon } from './DmIcon';
import { type Direction, DmIcon } from './DmIcon';
import { Icon } from './Icon';
import { Image } from './Image';
import { Stack } from './Stack';
Expand All @@ -28,11 +28,11 @@ type Props = Partial<{
*/
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.
* Same as buttons, but. Have disabled pointer-events on content inside if non-fluid.
* Fluid version have humburger layout.
* Can be used with buttons prop.
*/
buttonsAlt: boolean;
buttonsAlt: ReactNode;
/** Content under image. Or on the right if fluid. */
children: ReactNode;
/** Applies a CSS class to the element. */
Expand All @@ -51,14 +51,16 @@ type Props = Partial<{
dmIcon: string | null;
/** Parameter `icon_state` of component `DmIcon`. */
dmIconState: string | null;
/** Parameter `direction` of component `DmIcon`. */
dmDirection: Direction;
/**
* 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}` */
/** Prop `src` of Image component. Example: `imageSrc={resolveAsset(thing.image)}` */
imageSrc: string;
/** Called when button is clicked with LMB. */
onClick: (e: any) => void;
Expand All @@ -75,6 +77,12 @@ type Props = Partial<{
}> &
BoxProps;

/**
* Stylized button, with the ability to easily and simply insert any picture into it.
* - Without image, will be default question icon.
* - If an image is specified but for some reason cannot be displayed, there will be a spinner fallback until it is loaded.
* - Component has no **hover** effects, if `onClick` or `onRightClick` is not specified.
*/
export function ImageButton(props: Props) {
const {
asset,
Expand Down Expand Up @@ -119,7 +127,8 @@ export function ImageButton(props: Props) {
<div
className={classes([
'container',
buttons && 'hasButtons',
(buttons as boolean) ||
(fluid && (buttonsAlt as boolean) && 'hasButtons'),
!onClick && !onRightClick && 'noAction',
selected && 'ImageButton--selected',
disabled && 'ImageButton--disabled',
Expand Down Expand Up @@ -216,19 +225,36 @@ export function ImageButton(props: Props) {
<div
className={classes([
'buttonsContainer',
buttonsAlt && 'buttonsAltContainer',
!children && 'buttonsEmpty',
fluid && color && typeof color === 'string'
? `ImageButton--buttonsContainerColor__${color}`
: fluid && 'ImageButton--buttonsContainerColor__default',
])}
style={{
width: buttonsAlt ? `calc(${imageSize}px + 0.5em)` : 'auto',
width: 'auto',
}}
>
{buttons}
</div>
)}
{buttonsAlt && (
<div
className={classes([
'buttonsContainer',
'buttonsAltContainer',
!children && 'buttonsEmpty',
fluid && color && typeof color === 'string'
? `ImageButton--buttonsContainerColor__${color}`
: fluid && 'ImageButton--buttonsContainerColor__default',
])}
style={{
width: `calc(${imageSize}px + ${fluid ? 0 : 0.5}em)`,
maxWidth: !fluid ? `calc(${imageSize}px + 0.5em)` : 'auto',
}}
>
{buttonsAlt}
</div>
)}
</div>
);
}
5 changes: 3 additions & 2 deletions lib/styles/components/ImageButton.scss
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ $bg-map: colors.$bg-map !default;
}

.ImageButton {
/* Better replace via inline-flex after Byond 516 will be stable */
display: inline-table;
position: relative;
text-align: center;
Expand Down Expand Up @@ -178,7 +179,7 @@ $bg-map: colors.$bg-map !default;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: 0.25em 0.5em;
padding: 0.25em 0.33em;
margin: -1px;
border-radius: 0 0 0.33em 0.33em;
z-index: 2;
Expand All @@ -190,7 +191,7 @@ $bg-map: colors.$bg-map !default;
flex-direction: row;
position: relative;
text-align: center;
margin: 0 0 0.5em 0;
margin: 0 0 0.33em 0;
user-select: none;
-ms-user-select: none;

Expand Down
204 changes: 201 additions & 3 deletions stories/ImageButton.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import type { Meta, StoryObj } from '@storybook/react';
import type { ComponentProps } from 'react';
import { ImageButton } from '../lib/components/ImageButton';
import { type ComponentProps, useState } from 'react';
import { Button, Icon, ImageButton } from '../lib/components';

type StoryProps = ComponentProps<typeof ImageButton>;
const soulFish = (
<span style={{ color: 'rgba(255, 255, 255, 0.5' }}>SoulFish</span>
);
const soulFishImage =
'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgBAMAAACBVGfHAAAAIVBMVEXAwMAAAP8AAMwAZv9mMwCZZjMAAAD/mTP/zGb/ADMHBwd3bPqIAAAAAXRSTlMAQObYZgAAAI5JREFUKJFjYBhkQAiNz2iEJiAMEmBxQBIAaXENQOgwBgqwhEJVsKWlJRkbCsAF2NLKy9OSjQ0FVWEC5Rlt5cnGxsKmIVADKjo62oWBAoYwMyo6Z6AKlHd0lAMFlK1cYXrKywuNgQIL4AIMDEpAFauA9iIcthjkMgYXuACTlLKhAornFFctQvUtkyLDgAIAaJcdwdTNoTsAAAAASUVORK5CYII=';

type StoryProps = ComponentProps<typeof ImageButton>;
export default {
component: ImageButton,
title: 'Components/ImageButton',
Expand All @@ -16,3 +21,196 @@ export const Default: Story = {
children: 'ImageButton',
},
};

export const DefaultFluid: Story = {
args: {
fluid: true,
title: 'ImageButton',
children: 'It will take up the entire available width',
},
};

export const FilledDefault: Story = {
render: (args) => {
const [disabled, setDisabled] = useState(false);
const [selected, setSelected] = useState(false);

return (
<ImageButton
{...args}
base64={soulFishImage}
disabled={disabled}
selected={selected}
buttons={
<Button
icon={'power-off'}
color={disabled ? 'bad' : 'transparent'}
onClick={() => setDisabled(!disabled)}
/>
}
buttonsAlt={soulFish}
tooltip={'Also, you can Right Click on it'}
tooltipPosition={'bottom'}
onClick={() => setSelected(!selected)}
onRightClick={() => setDisabled(!disabled)}
>
Hover me
</ImageButton>
);
},
};

export const FilledFluid: Story = {
render: (args) => {
const [disabled, setDisabled] = useState(false);
const [selected, setSelected] = useState(false);

return (
<ImageButton
{...args}
fluid
title={'ImageButton'}
base64={soulFishImage}
disabled={disabled}
selected={selected}
buttonsAlt={
<Button
color={disabled ? 'bad' : 'transparent'}
onClick={() => setDisabled(!disabled)}
>
<Icon name={'power-off'} size={1.66} mb={1} />
<br />
{disabled ? 'Enable' : 'Disable'}
</Button>
}
tooltip={'Also, you can Right Click on it'}
tooltipPosition={'bottom'}
onClick={() => setSelected(!selected)}
onRightClick={() => setDisabled(!disabled)}
>
You can put any component you want inside "buttons" or "buttonsAlt"
props
</ImageButton>
);
},
};

export const Mixed: Story = {
render: (args) => {
const [disabled, setDisabled] = useState(false);
const [selected, setSelected] = useState(false);
const [compact, setCompact] = useState(false);

const info = (
<>
You can mix layouts without making new constants for each one.
<br />
If you are MAD enough.
</>
);

const controls = (
<>
<Button
icon={'power-off'}
color={disabled ? 'bad' : 'transparent'}
onClick={() => setDisabled(!disabled)}
/>
<Button
icon={compact ? 'expand' : 'minimize'}
color={'transparent'}
onClick={() => setCompact(!compact)}
/>
</>
);

return (
<ImageButton
{...args}
fluid={!compact}
title={'ImageButton'}
base64={soulFishImage}
imageSize={compact ? 96 : 64}
disabled={disabled}
selected={selected}
buttons={compact && controls}
buttonsAlt={compact ? soulFish : controls}
tooltip={compact && info}
tooltipPosition={'bottom'}
onClick={() => setSelected(!selected)}
onRightClick={() => setDisabled(!disabled)}
>
{!compact ? info : 'ImageButton'}
</ImageButton>
);
},
};

export const Colors: Story = {
render: (args) => {
const [disabled, setDisabled] = useState(false);
const [selected, setSelected] = useState(false);
const [compact, setCompact] = useState(false);

const COLORS = [
'red',
'orange',
'yellow',
'olive',
'green',
'teal',
'blue',
'violet',
'purple',
'pink',
'brown',
'grey',
'light-grey',
'label',
'good',
'average',
'bad',
'black',
'white',
];

const controls = (
<>
<Button
icon={'power-off'}
color={disabled ? 'bad' : 'transparent'}
onClick={() => setDisabled(!disabled)}
/>
<Button
icon={compact ? 'expand' : 'minimize'}
color={'transparent'}
onClick={() => setCompact(!compact)}
/>
</>
);

return (
<>
{COLORS.map((color) => (
<ImageButton
{...args}
key={color}
color={color}
fluid={!compact}
title={!compact ? color : ''}
base64={soulFishImage}
imageSize={compact ? 96 : 48}
disabled={disabled}
selected={selected}
buttons={compact && controls}
buttonsAlt={compact ? soulFish : controls}
onClick={() => setSelected(!selected)}
onRightClick={() => setDisabled(!disabled)}
>
{compact && color}
</ImageButton>
))}
</>
);
},
};

0 comments on commit a3df855

Please sign in to comment.