[MIRROR] Add tgui-core and use it for ByondUi (#9119)

Co-authored-by: Heroman3003 <31296024+Heroman3003@users.noreply.github.com>
Co-authored-by: CHOMPStation2 <chompsation2@gmail.com>
This commit is contained in:
CHOMPStation2
2024-10-02 03:38:14 -07:00
committed by GitHub
parent ef4ad9e18b
commit 99d245ca57
33 changed files with 35 additions and 1583 deletions

View File

@@ -24,7 +24,6 @@
@include meta.load-css('~tgui/styles/atomic/text.scss'); @include meta.load-css('~tgui/styles/atomic/text.scss');
// Components // Components
@include meta.load-css('~tgui/styles/components/BlockQuote.scss');
@include meta.load-css('~tgui/styles/components/Button.scss'); @include meta.load-css('~tgui/styles/components/Button.scss');
@include meta.load-css('~tgui/styles/components/ColorBox.scss'); @include meta.load-css('~tgui/styles/components/ColorBox.scss');
@include meta.load-css('~tgui/styles/components/Dimmer.scss'); @include meta.load-css('~tgui/styles/components/Dimmer.scss');

View File

@@ -1,69 +0,0 @@
import { Component } from 'react';
const DEFAULT_BLINKING_INTERVAL = 1000;
const DEFAULT_BLINKING_TIME = 1000;
export class Blink extends Component {
constructor(props) {
super(props);
this.state = {
hidden: false,
};
}
createTimer() {
const {
interval = DEFAULT_BLINKING_INTERVAL,
time = DEFAULT_BLINKING_TIME,
} = this.props;
clearInterval(this.interval);
clearTimeout(this.timer);
this.setState({
hidden: false,
});
this.interval = setInterval(() => {
this.setState({
hidden: true,
});
this.timer = setTimeout(() => {
this.setState({
hidden: false,
});
}, time);
}, interval + time);
}
componentDidMount() {
this.createTimer();
}
componentDidUpdate(prevProps) {
if (
prevProps.interval !== this.props.interval ||
prevProps.time !== this.props.time
) {
this.createTimer();
}
}
componentWillUnmount() {
clearInterval(this.interval);
clearTimeout(this.timer);
}
render() {
return (
<span
style={{
visibility: this.state.hidden ? 'hidden' : 'visible',
}}
>
{this.props.children}
</span>
);
}
}

View File

@@ -1,15 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { classes } from 'common/react';
import { Box, BoxProps } from './Box';
export function BlockQuote(props: BoxProps) {
const { className, ...rest } = props;
return <Box className={classes(['BlockQuote', className])} {...rest} />;
}

View File

@@ -1,127 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { shallowDiffers } from 'common/react';
import { debounce } from 'common/timer';
import { Component, createRef } from 'react';
import { createLogger } from '../logging';
import { computeBoxProps } from './Box';
const logger = createLogger('ByondUi');
// Stack of currently allocated BYOND UI element ids.
const byondUiStack = [];
const createByondUiElement = (elementId) => {
// Reserve an index in the stack
const index = byondUiStack.length;
byondUiStack.push(null);
// Get a unique id
const id = elementId || 'byondui_' + index;
logger.log(`allocated '${id}'`);
// Return a control structure
return {
render: (params) => {
logger.log(`rendering '${id}'`);
byondUiStack[index] = id;
Byond.winset(id, params);
},
unmount: () => {
logger.log(`unmounting '${id}'`);
byondUiStack[index] = null;
Byond.winset(id, {
parent: '',
});
},
};
};
window.addEventListener('beforeunload', () => {
// Cleanly unmount all visible UI elements
for (let index = 0; index < byondUiStack.length; index++) {
const id = byondUiStack[index];
if (typeof id === 'string') {
logger.log(`unmounting '${id}' (beforeunload)`);
byondUiStack[index] = null;
Byond.winset(id, {
parent: '',
});
}
}
});
/**
* Get the bounding box of the DOM element in display-pixels.
*/
const getBoundingBox = (element) => {
const pixelRatio = window.devicePixelRatio ?? 1;
const rect = element.getBoundingClientRect();
// prettier-ignore
return {
pos: [
rect.left * pixelRatio,
rect.top * pixelRatio,
],
size: [
(rect.right - rect.left) * pixelRatio,
(rect.bottom - rect.top) * pixelRatio,
],
};
};
export class ByondUi extends Component {
constructor(props) {
super(props);
this.containerRef = createRef();
this.byondUiElement = createByondUiElement(props.params?.id);
this.handleResize = debounce(() => {
this.forceUpdate();
}, 100);
}
shouldComponentUpdate(nextProps) {
const { params: prevParams = {}, ...prevRest } = this.props;
const { params: nextParams = {}, ...nextRest } = nextProps;
return (
shallowDiffers(prevParams, nextParams) ||
shallowDiffers(prevRest, nextRest)
);
}
componentDidMount() {
window.addEventListener('resize', this.handleResize);
this.componentDidUpdate();
this.handleResize();
}
componentDidUpdate() {
const { params = {} } = this.props;
const box = getBoundingBox(this.containerRef.current);
logger.debug('bounding box', box);
this.byondUiElement.render({
parent: Byond.windowId,
...params,
pos: box.pos[0] + ',' + box.pos[1],
size: box.size[0] + 'x' + box.size[1],
});
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
this.byondUiElement.unmount();
}
render() {
const { params, ...rest } = this.props;
return (
<div ref={this.containerRef} {...computeBoxProps(rest)}>
{/* Filler */}
<div style={{ minHeight: '22px' }} />
</div>
);
}
}

View File

@@ -1,72 +0,0 @@
import { ReactNode, useEffect, useState } from 'react';
import { resolveAsset } from '../assets';
import { fetchRetry } from '../http';
import { BoxProps } from './Box';
import { Image } from './Image';
enum Direction {
NORTH = 1,
SOUTH = 2,
EAST = 4,
WEST = 8,
NORTHEAST = NORTH | EAST,
NORTHWEST = NORTH | WEST,
SOUTHEAST = SOUTH | EAST,
SOUTHWEST = SOUTH | WEST,
}
type Props = {
/** Required: The path of the icon */
icon: string;
/** Required: The state of the icon */
icon_state: string;
} & Partial<{
/** Facing direction. See direction enum. Default is South */
direction: Direction;
/** Fallback icon. */
fallback: ReactNode;
/** Frame number. Default is 1 */
frame: number;
/** Movement state. Default is false */
movement: boolean;
}> &
BoxProps;
let refMap: Record<string, string> | undefined;
export function DmIcon(props: Props) {
const {
className,
direction = Direction.SOUTH,
fallback,
frame = 1,
icon_state,
icon,
movement = false,
...rest
} = props;
const [iconRef, setIconRef] = useState('');
const query = `${iconRef}?state=${icon_state}&dir=${direction}&movement=${movement}&frame=${frame}`;
useEffect(() => {
async function fetchRefMap() {
const response = await fetchRetry(resolveAsset('icon_ref_map.json'));
const data = await response.json();
refMap = data;
setIconRef(data[icon]);
}
if (!refMap) {
fetchRefMap();
} else {
setIconRef(refMap[icon]);
}
}, []);
if (!iconRef) return fallback;
return <Image fixErrors src={query} {...rest} />;
}

View File

@@ -1,52 +0,0 @@
import { Component, Fragment } from 'react';
import { Box } from './Box';
export class FakeTerminal extends Component {
constructor(props) {
super(props);
this.timer = null;
this.state = {
currentIndex: 0,
currentDisplay: [],
};
}
tick() {
const { props, state } = this;
if (state.currentIndex <= props.allMessages.length) {
this.setState((prevState) => {
return {
currentIndex: prevState.currentIndex + 1,
};
});
const { currentDisplay } = state;
currentDisplay.push(props.allMessages[state.currentIndex]);
} else {
clearTimeout(this.timer);
setTimeout(props.onFinished, props.finishedTimeout);
}
}
componentDidMount() {
const { linesPerSecond = 2.5 } = this.props;
this.timer = setInterval(() => this.tick(), 1000 / linesPerSecond);
}
componentWillUnmount() {
clearTimeout(this.timer);
}
render() {
return (
<Box m={1}>
{this.state.currentDisplay.map((value) => (
<Fragment key={value}>
{value}
<br />
</Fragment>
))}
</Box>
);
}
}

View File

@@ -1,99 +0,0 @@
import {
Component,
createRef,
HTMLAttributes,
PropsWithChildren,
RefObject,
} from 'react';
const DEFAULT_ACCEPTABLE_DIFFERENCE = 5;
type Props = {
acceptableDifference?: number;
maxWidth: number;
maxFontSize: number;
native?: HTMLAttributes<HTMLDivElement>;
} & PropsWithChildren;
type State = {
fontSize: number;
};
export class FitText extends Component<Props, State> {
ref: RefObject<HTMLDivElement> = createRef();
state: State = {
fontSize: 0,
};
constructor(props: Props) {
super(props);
this.resize = this.resize.bind(this);
window.addEventListener('resize', this.resize);
}
componentDidUpdate(prevProps) {
if (prevProps.children !== this.props.children) {
this.resize();
}
}
componentWillUnmount() {
window.removeEventListener('resize', this.resize);
}
resize() {
const element = this.ref.current;
if (!element) {
return;
}
const maxWidth = this.props.maxWidth;
let start = 0;
let end = this.props.maxFontSize;
for (let _ = 0; _ < 10; _++) {
const middle = Math.round((start + end) / 2);
element.style.fontSize = `${middle}px`;
const difference = element.offsetWidth - maxWidth;
if (difference > 0) {
end = middle;
} else if (
difference <
(this.props.acceptableDifference ?? DEFAULT_ACCEPTABLE_DIFFERENCE)
) {
start = middle;
} else {
break;
}
}
this.setState({
fontSize: Math.round((start + end) / 2),
});
}
componentDidMount() {
this.resize();
}
render() {
return (
<span
ref={this.ref}
style={{
fontSize: `${this.state.fontSize}px`,
...(typeof this.props.native?.style === 'object'
? this.props.native.style
: {}),
}}
>
{this.props.children}
</span>
);
}
}

View File

@@ -1,44 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { PropsWithChildren } from 'react';
import { logger } from '../logging';
import { BoxProps } from './Box';
import { Table } from './Table';
/** @deprecated Do not use. Use stack instead. */
export function Grid(props: PropsWithChildren<BoxProps>) {
const { children, ...rest } = props;
logger.error('Grid component is deprecated. Use a Stack instead.');
return (
<Table {...rest}>
<Table.Row>{children}</Table.Row>
</Table>
);
}
type Props = Partial<{
/** Width of the column in percentage. */
size: number;
}> &
BoxProps;
/** @deprecated Do not use. Use stack instead. */
export function GridColumn(props: Props) {
const { size = 1, style, ...rest } = props;
return (
<Table.Cell
style={{
width: size + '%',
...style,
}}
{...rest}
/>
);
}
Grid.Column = GridColumn;

View File

@@ -1,192 +0,0 @@
import { Component } from 'react';
import { computeBoxProps } from './Box';
import { Button } from './Button';
import { ProgressBar } from './ProgressBar';
import { Stack } from './Stack';
const ZOOM_MIN_VAL = 0.5;
const ZOOM_MAX_VAL = 1.5;
const ZOOM_INCREMENT = 0.1;
export class InfinitePlane extends Component {
constructor(props) {
super(props);
this.state = {
mouseDown: false,
left: 0,
top: 0,
lastLeft: 0,
lastTop: 0,
zoom: 1,
};
this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleZoomIncrease = this.handleZoomIncrease.bind(this);
this.handleZoomDecrease = this.handleZoomDecrease.bind(this);
this.onMouseUp = this.onMouseUp.bind(this);
this.doOffsetMouse = this.doOffsetMouse.bind(this);
}
componentDidMount() {
window.addEventListener('mouseup', this.onMouseUp);
window.addEventListener('mousedown', this.doOffsetMouse);
window.addEventListener('mousemove', this.doOffsetMouse);
window.addEventListener('mouseup', this.doOffsetMouse);
}
componentWillUnmount() {
window.removeEventListener('mouseup', this.onMouseUp);
window.removeEventListener('mousedown', this.doOffsetMouse);
window.removeEventListener('mousemove', this.doOffsetMouse);
window.removeEventListener('mouseup', this.doOffsetMouse);
}
doOffsetMouse(event) {
const { zoom } = this.state;
event.screenZoomX = event.screenX * Math.pow(zoom, -1);
event.screenZoomY = event.screenY * Math.pow(zoom, -1);
}
handleMouseDown(event) {
this.setState((state) => {
return {
mouseDown: true,
lastLeft: event.clientX - state.left,
lastTop: event.clientY - state.top,
};
});
}
onMouseUp() {
this.setState({
mouseDown: false,
});
}
handleZoomIncrease(event) {
const { onZoomChange } = this.props;
const { zoom } = this.state;
const newZoomValue = Math.min(zoom + ZOOM_INCREMENT, ZOOM_MAX_VAL);
this.setState({
zoom: newZoomValue,
});
if (onZoomChange) {
onZoomChange(newZoomValue);
}
}
handleZoomDecrease(event) {
const { onZoomChange } = this.props;
const { zoom } = this.state;
const newZoomValue = Math.max(zoom - ZOOM_INCREMENT, ZOOM_MIN_VAL);
this.setState({
zoom: newZoomValue,
});
if (onZoomChange) {
onZoomChange(newZoomValue);
}
}
handleMouseMove(event) {
const { onBackgroundMoved, initialLeft = 0, initialTop = 0 } = this.props;
if (this.state.mouseDown) {
let newX, newY;
this.setState((state) => {
newX = event.clientX - state.lastLeft;
newY = event.clientY - state.lastTop;
return {
left: newX,
top: newY,
};
});
if (onBackgroundMoved) {
onBackgroundMoved(newX + initialLeft, newY + initialTop);
}
}
}
render() {
const {
children,
backgroundImage,
imageWidth,
initialLeft = 0,
initialTop = 0,
...rest
} = this.props;
const { left, top, zoom } = this.state;
const finalLeft = initialLeft + left;
const finalTop = initialTop + top;
return (
<div
ref={this.ref}
{...computeBoxProps({
...rest,
style: {
...rest.style,
overflow: 'hidden',
position: 'relative',
},
})}
>
<div
onMouseDown={this.handleMouseDown}
onMouseMove={this.handleMouseMove}
style={{
position: 'fixed',
height: '100%',
width: '100%',
backgroundImage: `url("${backgroundImage}")`,
backgroundPosition: `${finalLeft}px ${finalTop}px`,
backgroundRepeat: 'repeat',
backgroundSize: `${zoom * imageWidth}px`,
}}
/>
<div
onMouseDown={this.handleMouseDown}
onMouseMove={this.handleMouseMove}
style={{
position: 'fixed',
transform: `translate(${finalLeft}px, ${finalTop}px) scale(${zoom})`,
transformOrigin: 'top left',
height: '100%',
width: '100%',
}}
>
{children}
</div>
<Stack position="absolute" width="100%">
<Stack.Item>
<Button icon="minus" onClick={this.handleZoomDecrease} />
</Stack.Item>
<Stack.Item grow={1}>
<ProgressBar
minValue={ZOOM_MIN_VAL}
value={zoom}
maxValue={ZOOM_MAX_VAL}
>
{zoom}x
</ProgressBar>
</Stack.Item>
<Stack.Item>
<Button icon="plus" onClick={this.handleZoomIncrease} />
</Stack.Item>
</Stack>
</div>
);
}
}

View File

@@ -1,40 +0,0 @@
import { Component } from 'react';
import { KeyEvent } from '../events';
import { listenForKeyEvents } from '../hotkeys';
type KeyListenerProps = Partial<{
onKey: (key: KeyEvent) => void;
onKeyDown: (key: KeyEvent) => void;
onKeyUp: (key: KeyEvent) => void;
}>;
export class KeyListener extends Component<KeyListenerProps> {
dispose: () => void;
constructor(props) {
super(props);
this.dispose = listenForKeyEvents((key) => {
if (this.props.onKey) {
this.props.onKey(key);
}
if (key.isDown() && this.props.onKeyDown) {
this.props.onKeyDown(key);
}
if (key.isUp() && this.props.onKeyUp) {
this.props.onKeyUp(key);
}
});
}
componentWillUnmount() {
this.dispose();
}
render() {
return null;
}
}

View File

@@ -1,238 +0,0 @@
/**
* @file
* @copyright 2022 Aleksej Komarov
* @license MIT
*/
import { classes } from 'common/react';
import { Component, createRef, ReactNode, RefObject } from 'react';
import { logger } from '../logging';
import { Box } from './Box';
import { Icon } from './Icon';
type MenuProps = {
children: any;
width: string;
menuRef: RefObject<HTMLElement>;
onOutsideClick: () => void;
};
class Menu extends Component<MenuProps> {
private readonly handleClick: (event) => void;
constructor(props) {
super(props);
this.handleClick = (event) => {
if (!this.props.menuRef.current) {
logger.log(`Menu.handleClick(): No ref`);
return;
}
if (this.props.menuRef.current.contains(event.target)) {
logger.log(`Menu.handleClick(): Inside`);
} else {
logger.log(`Menu.handleClick(): Outside`);
this.props.onOutsideClick();
}
};
}
// eslint-disable-next-line react/no-deprecated
componentWillMount() {
window.addEventListener('click', this.handleClick);
}
componentWillUnmount() {
window.removeEventListener('click', this.handleClick);
}
render() {
const { width, children } = this.props;
return (
<div
className={'MenuBar__menu'}
style={{
width: width,
}}
>
{children}
</div>
);
}
}
type MenuBarDropdownProps = {
open: boolean;
openWidth: string;
children: any;
disabled?: boolean;
display: any;
onMouseOver: () => void;
onClick: () => void;
onOutsideClick: () => void;
className?: string;
};
class MenuBarButton extends Component<MenuBarDropdownProps> {
private readonly menuRef: RefObject<HTMLDivElement>;
constructor(props) {
super(props);
this.menuRef = createRef();
}
render() {
const { props } = this;
const {
open,
openWidth,
children,
disabled,
display,
onMouseOver,
onClick,
onOutsideClick,
...boxProps
} = props;
const { className, ...rest } = boxProps;
return (
<div ref={this.menuRef}>
<Box
className={classes([
'MenuBar__MenuBarButton',
'MenuBar__font',
'MenuBar__hover',
className,
])}
{...rest}
onClick={disabled ? () => null : onClick}
onMouseOver={onMouseOver}
>
<span className="MenuBar__MenuBarButton-text">{display}</span>
</Box>
{open && (
<Menu
width={openWidth}
menuRef={this.menuRef}
onOutsideClick={onOutsideClick}
>
{children}
</Menu>
)}
</div>
);
}
}
type MenuBarItemProps = {
entry: string;
children: any;
openWidth: string;
display: ReactNode;
setOpenMenuBar: (entry: string | null) => void;
openMenuBar: string | null;
setOpenOnHover: (flag: boolean) => void;
openOnHover: boolean;
disabled?: boolean;
className?: string;
};
export const Dropdown = (props: MenuBarItemProps) => {
const {
entry,
children,
openWidth,
display,
setOpenMenuBar,
openMenuBar,
setOpenOnHover,
openOnHover,
disabled,
className,
} = props;
return (
<MenuBarButton
openWidth={openWidth}
display={display}
disabled={disabled}
open={openMenuBar === entry}
className={className}
onClick={() => {
const open = openMenuBar === entry ? null : entry;
setOpenMenuBar(open);
setOpenOnHover(!openOnHover);
}}
onOutsideClick={() => {
setOpenMenuBar(null);
setOpenOnHover(false);
}}
onMouseOver={() => {
if (openOnHover) {
setOpenMenuBar(entry);
}
}}
>
{children}
</MenuBarButton>
);
};
const MenuItemToggle = (props) => {
const { value, displayText, onClick, checked } = props;
return (
<Box
className={classes([
'MenuBar__font',
'MenuBar__MenuItem',
'MenuBar__MenuItemToggle',
'MenuBar__hover',
])}
onClick={() => onClick(value)}
>
<div className="MenuBar__MenuItemToggle__check">
{checked && <Icon size={1.3} name="check" />}
</div>
{displayText}
</Box>
);
};
Dropdown.MenuItemToggle = MenuItemToggle;
const MenuItem = (props) => {
const { value, displayText, onClick } = props;
return (
<Box
className={classes([
'MenuBar__font',
'MenuBar__MenuItem',
'MenuBar__hover',
])}
onClick={() => onClick(value)}
>
{displayText}
</Box>
);
};
Dropdown.MenuItem = MenuItem;
const Separator = () => {
return <div className="MenuBar__Separator" />;
};
Dropdown.Separator = Separator;
type MenuBarProps = {
children: any;
};
export const MenuBar = (props: MenuBarProps) => {
const { children } = props;
return <Box className="MenuBar">{children}</Box>;
};
MenuBar.Dropdown = Dropdown;

View File

@@ -1,133 +0,0 @@
/**
* @file
* @copyright 2020 bobbahbrown (https://github.com/bobbahbrown)
* @license MIT
*/
import { clamp01, keyOfMatchingRange, scale } from 'common/math';
import { classes } from 'common/react';
import { AnimatedNumber } from './AnimatedNumber';
import { Box, computeBoxClassName, computeBoxProps } from './Box';
export const RoundGauge = (props) => {
const {
value,
minValue = 1,
maxValue = 1,
ranges,
alertAfter,
alertBefore,
format,
size = 1,
className,
style,
...rest
} = props;
const scaledValue = scale(value, minValue, maxValue);
const clampedValue = clamp01(scaledValue);
const scaledRanges = ranges ? {} : { primary: [0, 1] };
if (ranges) {
Object.keys(ranges).forEach((x) => {
const range = ranges[x];
scaledRanges[x] = [
scale(range[0], minValue, maxValue),
scale(range[1], minValue, maxValue),
];
});
}
const shouldShowAlert = () => {
// If both after and before alert props are set, attempt to interpret both
// in a helpful way.
if (alertAfter && alertBefore && alertAfter < alertBefore) {
// If alertAfter is before alertBefore, only display an alert if
// we're between them.
if (alertAfter < value && alertBefore > value) {
return true;
}
} else if (alertAfter < value || alertBefore > value) {
// Otherwise, we have distint ranges, or only one or neither are set.
// Either way, being on the active side of either is sufficient.
return true;
}
return false;
};
// prettier-ignore
const alertColor = shouldShowAlert()
&& keyOfMatchingRange(clampedValue, scaledRanges);
return (
<Box inline>
<div
className={classes([
'RoundGauge',
className,
computeBoxClassName(rest),
])}
{...computeBoxProps({
style: {
fontSize: size + 'em',
...style,
},
...rest,
})}
>
<svg viewBox="0 0 100 50">
{(alertAfter || alertBefore) && (
<g
className={classes([
'RoundGauge__alert',
alertColor ? `active RoundGauge__alert--${alertColor}` : '',
])}
>
<path d="M48.211,14.578C48.55,13.9 49.242,13.472 50,13.472C50.758,13.472 51.45,13.9 51.789,14.578C54.793,20.587 60.795,32.589 63.553,38.106C63.863,38.726 63.83,39.462 63.465,40.051C63.101,40.641 62.457,41 61.764,41C55.996,41 44.004,41 38.236,41C37.543,41 36.899,40.641 36.535,40.051C36.17,39.462 36.137,38.726 36.447,38.106C39.205,32.589 45.207,20.587 48.211,14.578ZM50,34.417C51.426,34.417 52.583,35.574 52.583,37C52.583,38.426 51.426,39.583 50,39.583C48.574,39.583 47.417,38.426 47.417,37C47.417,35.574 48.574,34.417 50,34.417ZM50,32.75C50,32.75 53,31.805 53,22.25C53,20.594 51.656,19.25 50,19.25C48.344,19.25 47,20.594 47,22.25C47,31.805 50,32.75 50,32.75Z" />
</g>
)}
<g>
<circle className="RoundGauge__ringTrack" cx="50" cy="50" r="45" />
</g>
<g>
{Object.keys(scaledRanges).map((x, i) => {
const col_ranges = scaledRanges[x];
return (
<circle
className={`RoundGauge__ringFill RoundGauge--color--${x}`}
key={i}
style={{
strokeDashoffset: Math.max(
(2.0 - (col_ranges[1] - col_ranges[0])) * Math.PI * 50,
0,
),
}}
transform={`rotate(${180 + 180 * col_ranges[0]} 50 50)`}
cx="50"
cy="50"
r="45"
/>
);
})}
</g>
<g
className="RoundGauge__needle"
transform={`rotate(${clampedValue * 180 - 90} 50 50)`}
>
<polygon
className="RoundGauge__needleLine"
points="46,50 50,0 54,50"
/>
<circle
className="RoundGauge__needleMiddle"
cx="50"
cy="50"
r="8"
/>
</g>
</svg>
</div>
<AnimatedNumber value={value} format={format} size={size} />
</Box>
);
};

View File

@@ -1,30 +0,0 @@
import { PropsWithChildren, ReactNode } from 'react';
import { Box } from './Box';
type Props = Partial<{
style: Record<string, any>;
titleStyle: Record<string, any>;
textStyle: Record<string, any>;
title: ReactNode;
titleSubtext: string;
}> &
PropsWithChildren;
// The cost of flexibility and prettiness.
export const StyleableSection = (props: Props) => {
return (
<Box style={props.style}>
{/* Yes, this box (line above) is missing the "Section" class. This is very intentional, as the layout looks *ugly* with it.*/}
<Box className="Section__title" style={props.titleStyle}>
<Box className="Section__titleText" style={props.textStyle}>
{props.title}
</Box>
<div className="Section__buttons">{props.titleSubtext}</div>
</Box>
<Box className="Section__rest">
<Box className="Section__content">{props.children}</Box>
</Box>
</Box>
);
};

View File

@@ -1,64 +0,0 @@
import { Component } from 'react';
import { formatTime } from '../format';
// AnimatedNumber Copypaste
const isSafeNumber = (value) => {
return (
typeof value === 'number' && Number.isFinite(value) && !Number.isNaN(value)
);
};
export class TimeDisplay extends Component {
constructor(props) {
super(props);
this.timer = null;
this.last_seen_value = undefined;
this.state = {
value: 0,
};
// Set initial state with value provided in props
if (isSafeNumber(props.value)) {
this.state.value = Number(props.value);
this.last_seen_value = Number(props.value);
}
}
componentDidUpdate() {
if (this.props.auto !== undefined) {
clearInterval(this.timer);
this.timer = setInterval(() => this.tick(), 1000); // every 1 s
}
}
tick() {
let current = Number(this.state.value);
if (this.props.value !== this.last_seen_value) {
this.last_seen_value = this.props.value;
current = this.props.value;
}
const mod = this.props.auto === 'up' ? 10 : -10; // Time down by default.
const value = Math.max(0, current + mod); // one sec tick
this.setState({ value });
}
componentDidMount() {
if (this.props.auto !== undefined) {
this.timer = setInterval(() => this.tick(), 1000); // every 1 s
}
}
componentWillUnmount() {
clearInterval(this.timer);
}
render() {
const val = this.state.value;
// Directly display weird stuff
if (!isSafeNumber(val)) {
return this.state.value || null;
}
return formatTime(val);
}
}

View File

@@ -1,35 +0,0 @@
import { Component, createRef, PropsWithChildren } from 'react';
type Props = {
onOutsideClick: () => void;
} & PropsWithChildren;
export class TrackOutsideClicks extends Component<Props> {
ref = createRef<HTMLDivElement>();
constructor(props) {
super(props);
this.handleOutsideClick = this.handleOutsideClick.bind(this);
document.addEventListener('click', this.handleOutsideClick);
}
componentWillUnmount() {
document.removeEventListener('click', this.handleOutsideClick);
}
handleOutsideClick(event: MouseEvent) {
if (!(event.target instanceof Node)) {
return;
}
if (this.ref.current && !this.ref.current.contains(event.target)) {
this.props.onOutsideClick();
}
}
render() {
return <div ref={this.ref}>{this.props.children}</div>;
}
}

View File

@@ -1,69 +0,0 @@
import {
PropsWithChildren,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
/**
* A vertical list that renders items to fill space up to the extents of the
* current window, and then defers rendering of other items until they come
* into view.
*/
export const VirtualList = (props: PropsWithChildren) => {
const { children } = props;
const containerRef = useRef(null as HTMLDivElement | null);
const [visibleElements, setVisibleElements] = useState(1);
const [padding, setPadding] = useState(0);
const adjustExtents = useCallback(() => {
const { current } = containerRef;
if (
!children ||
!Array.isArray(children) ||
!current ||
visibleElements >= children.length
) {
return;
}
const unusedArea =
document.body.offsetHeight - current.getBoundingClientRect().bottom;
const averageItemHeight = Math.ceil(current.offsetHeight / visibleElements);
if (unusedArea > 0) {
const newVisibleElements = Math.min(
children.length,
visibleElements +
Math.max(1, Math.ceil(unusedArea / averageItemHeight)),
);
setVisibleElements(newVisibleElements);
setPadding((children.length - newVisibleElements) * averageItemHeight);
}
}, [containerRef, visibleElements, children]);
useEffect(() => {
adjustExtents();
const interval = setInterval(adjustExtents, 100);
return () => clearInterval(interval);
}, [adjustExtents]);
return (
<div className={'VirtualList'}>
<div className={'VirtualList__Container'} ref={containerRef}>
{Array.isArray(children) ? children.slice(0, visibleElements) : null}
</div>
<div
className={'VirtualList__Padding'}
style={{ paddingBottom: `${padding}px` }}
/>
</div>
);
};

View File

@@ -6,32 +6,23 @@
export { AnimatedNumber } from './AnimatedNumber'; export { AnimatedNumber } from './AnimatedNumber';
export { Autofocus } from './Autofocus'; export { Autofocus } from './Autofocus';
export { Blink } from './Blink';
export { BlockQuote } from './BlockQuote';
export { Box } from './Box'; export { Box } from './Box';
export { Button } from './Button'; export { Button } from './Button';
export { ByondUi } from './ByondUi';
export { Chart } from './Chart'; export { Chart } from './Chart';
export { Collapsible } from './Collapsible'; export { Collapsible } from './Collapsible';
export { ColorBox } from './ColorBox'; export { ColorBox } from './ColorBox';
export { Dialog } from './Dialog'; export { Dialog } from './Dialog';
export { Dimmer } from './Dimmer'; export { Dimmer } from './Dimmer';
export { Divider } from './Divider'; export { Divider } from './Divider';
export { DmIcon } from './DmIcon';
export { DraggableControl } from './DraggableControl'; export { DraggableControl } from './DraggableControl';
export { Dropdown } from './Dropdown'; export { Dropdown } from './Dropdown';
export { FitText } from './FitText';
export { Flex } from './Flex'; export { Flex } from './Flex';
export { Grid } from './Grid';
export { Icon } from './Icon'; export { Icon } from './Icon';
export { Image } from './Image'; export { Image } from './Image';
export { InfinitePlane } from './InfinitePlane';
export { Input } from './Input'; export { Input } from './Input';
export { KeyListener } from './KeyListener';
export { Knob } from './Knob'; export { Knob } from './Knob';
export { LabeledControls } from './LabeledControls'; export { LabeledControls } from './LabeledControls';
export { LabeledList } from './LabeledList'; export { LabeledList } from './LabeledList';
export { MenuBar } from './MenuBar';
export { Modal } from './Modal'; export { Modal } from './Modal';
export { NanoMap } from './NanoMap'; export { NanoMap } from './NanoMap';
export { NoticeBox } from './NoticeBox'; export { NoticeBox } from './NoticeBox';
@@ -39,15 +30,10 @@ export { NumberInput } from './NumberInput';
export { Popper } from './Popper'; export { Popper } from './Popper';
export { ProgressBar } from './ProgressBar'; export { ProgressBar } from './ProgressBar';
export { RestrictedInput } from './RestrictedInput'; export { RestrictedInput } from './RestrictedInput';
export { RoundGauge } from './RoundGauge';
export { Section } from './Section'; export { Section } from './Section';
export { Slider } from './Slider'; export { Slider } from './Slider';
export { Stack } from './Stack'; export { Stack } from './Stack';
export { StyleableSection } from './StyleableSection';
export { Table } from './Table'; export { Table } from './Table';
export { Tabs } from './Tabs'; export { Tabs } from './Tabs';
export { TextArea } from './TextArea'; export { TextArea } from './TextArea';
export { TimeDisplay } from './TimeDisplay';
export { Tooltip } from './Tooltip'; export { Tooltip } from './Tooltip';
export { TrackOutsideClicks } from './TrackOutsideClicks';
export { VirtualList } from './VirtualList';

View File

@@ -1,16 +1,10 @@
import { capitalize, decodeHtmlEntities } from 'common/string'; import { capitalize, decodeHtmlEntities } from 'common/string';
import { useState } from 'react'; import { useState } from 'react';
import { useBackend } from 'tgui/backend';
import { Box, Flex, LabeledList, Section, Tabs } from 'tgui/components';
import { Window } from 'tgui/layouts';
import { ByondUi } from 'tgui-core/components';
import { useBackend } from '../../backend';
import {
Box,
ByondUi,
Flex,
LabeledList,
Section,
Tabs,
} from '../../components';
import { Window } from '../../layouts';
import { import {
AppearanceChangerEars, AppearanceChangerEars,
AppearanceChangerGender, AppearanceChangerGender,

View File

@@ -1,15 +1,15 @@
import { capitalize } from 'common/string'; import { capitalize } from 'common/string';
import { useBackend } from 'tgui/backend';
import { useBackend } from '../../backend';
import { import {
Box, Box,
Button, Button,
ByondUi,
ColorBox, ColorBox,
Flex, Flex,
LabeledList, LabeledList,
Section, Section,
} from '../../components'; } from 'tgui/components';
import { ByondUi } from 'tgui-core/components';
import { activeBodyRecord } from './types'; import { activeBodyRecord } from './types';
export const BodyDesignerSpecificRecord = (props: { export const BodyDesignerSpecificRecord = (props: {

View File

@@ -3,10 +3,10 @@ import { flow } from 'common/fp';
import { BooleanLike, classes } from 'common/react'; import { BooleanLike, classes } from 'common/react';
import { createSearch } from 'common/string'; import { createSearch } from 'common/string';
import { useState } from 'react'; import { useState } from 'react';
import { useBackend } from 'tgui/backend';
import { useBackend } from '../backend'; import { Button, Dropdown, Flex, Input, Section } from 'tgui/components';
import { Button, ByondUi, Dropdown, Flex, Input, Section } from '../components'; import { Window } from 'tgui/layouts';
import { Window } from '../layouts'; import { ByondUi } from 'tgui-core/components';
type activeCamera = { name: string; status: BooleanLike } | null; type activeCamera = { name: string; status: BooleanLike } | null;

View File

@@ -1,7 +1,8 @@
import { decodeHtmlEntities } from 'common/string'; import { decodeHtmlEntities } from 'common/string';
import { useBackend } from 'tgui/backend';
import { Box, Button, Flex, Icon, Section } from 'tgui/components';
import { ByondUi } from 'tgui-core/components';
import { useBackend } from '../../backend';
import { Box, Button, ByondUi, Flex, Icon, Section } from '../../components';
import { HOMETAB } from './constants'; import { HOMETAB } from './constants';
import { Data } from './types'; import { Data } from './types';

View File

@@ -1,6 +1,8 @@
import { useBackend } from '../backend'; import { useBackend } from 'tgui/backend';
import { Button, ByondUi } from '../components'; import { Button } from 'tgui/components';
import { NtosWindow } from '../layouts'; import { NtosWindow } from 'tgui/layouts';
import { ByondUi } from 'tgui-core/components';
import { import {
camera, camera,
CameraConsoleContent, CameraConsoleContent,

View File

@@ -1,35 +0,0 @@
import { useBackend } from '../backend';
import { ByondUi } from '../components';
import { Window } from '../layouts';
type Data = { mapRef: string };
export const StationBlueprints = (props) => {
return (
<Window width={870} height={708}>
<StationBlueprintsContent />
</Window>
);
};
export const StationBlueprintsContent = (props) => {
const { data } = useBackend<Data>();
const { mapRef /* areas, turfs */ } = data;
return (
<>
<div className="CameraConsole__left">
<Window.Content scrollable>Honk!</Window.Content>
</div>
<div className="CameraConsole__right">
<ByondUi
className="CameraConsole__map"
params={{
id: mapRef,
type: 'map',
}}
/>
</div>
</>
);
};

View File

@@ -16,6 +16,7 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-popper": "^2.3.0", "react-popper": "^2.3.0",
"tgui-core": "^1.2.0",
"tgui-dev-server": "workspace:*", "tgui-dev-server": "workspace:*",
"tgui-polyfill": "workspace:*" "tgui-polyfill": "workspace:*"
} }

View File

@@ -1,20 +0,0 @@
/**
* @file
* @copyright 2021 Aleksej Komarov
* @license MIT
*/
import { Blink, Section } from '../components';
export const meta = {
title: 'Blink',
render: () => <Story />,
};
const Story = (props) => {
return (
<Section>
<Blink>Blink</Blink>
</Section>
);
};

View File

@@ -1,23 +0,0 @@
/**
* @file
* @copyright 2021 Aleksej Komarov
* @license MIT
*/
import { BlockQuote, Section } from '../components';
import { BoxWithSampleText } from './common';
export const meta = {
title: 'BlockQuote',
render: () => <Story />,
};
const Story = (props) => {
return (
<Section>
<BlockQuote>
<BoxWithSampleText />
</BlockQuote>
</Section>
);
};

View File

@@ -5,8 +5,9 @@
*/ */
import { useState } from 'react'; import { useState } from 'react';
import { ByondUi } from 'tgui-core/components';
import { Box, Button, ByondUi, Section } from '../components'; import { Box, Button, Section } from '../components';
import { logger } from '../logging'; import { logger } from '../logging';
export const meta = { export const meta = {

View File

@@ -1,20 +0,0 @@
/**
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/
@use '../base.scss';
@use '../colors.scss';
$color-default: colors.fg(colors.$label) !default;
.BlockQuote {
color: $color-default;
border-left: base.em(2px) solid $color-default;
padding-left: 0.5em;
margin-bottom: 0.5em;
&:last-child {
margin-bottom: 0;
}
}

View File

@@ -1,75 +0,0 @@
/**
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/
@use '../base.scss';
$separator-color: base.$color-bg-section;
$background-color: base.$color-bg !default;
$dropdown-z-index: 5;
.MenuBar {
display: flex;
}
.MenuBar__font {
font-family: Verdana, sans-serif;
font-size: base.em(12px);
line-height: base.em(17px);
}
.MenuBar__hover {
&:hover {
background-color: lighten($background-color, 30%);
transition: background-color 0ms;
}
}
.MenuBar__MenuBarButton {
padding: 0.2rem 0.5rem 0.2rem 0.5rem;
}
.MenuBar__menu {
position: absolute;
z-index: $dropdown-z-index;
background-color: $background-color;
padding: 0.3rem 0.3rem 0.3rem 0.3rem;
box-shadow: 4px 6px 5px -2px rgba(0, 0, 0, 0.55);
}
.MenuBar__MenuItem {
z-index: $dropdown-z-index;
transition: background-color 100ms ease-out;
background-color: $background-color;
white-space: nowrap;
padding: 0.3rem 2rem 0.3rem 3rem;
}
.MenuBar__MenuItemToggle {
padding: 0.3rem 2rem 0.3rem 0;
}
.MenuBar__MenuItemToggle__check {
display: inline-block;
vertical-align: middle;
min-width: 3rem;
margin-left: 0.3rem;
}
.MenuBar__over {
top: auto;
bottom: 100%;
}
.MenuBar__MenuBarButton-text {
text-overflow: clip;
white-space: nowrap;
height: base.em(17px);
}
.MenuBar__Separator {
display: block;
margin: 0.3rem 0.3rem 0.3rem 2.3rem;
border-top: 1px solid $separator-color;
}

View File

@@ -1,88 +0,0 @@
/**
* Copyright (c) 2020 bobbahbrown (https://github.com/bobbahbrown)
* SPDX-License-Identifier: MIT
*/
@use '../base.scss';
@use '../colors.scss';
@use '../functions.scss' as *;
$fg-map: colors.$fg-map !default;
$ring-color: #6a96c9 !default;
.RoundGauge {
font-size: 1rem;
width: 2.6em;
height: 1.3em;
margin: 0 auto;
margin-bottom: 0.2em;
}
$pi: 3.1416;
.RoundGauge__ringTrack {
fill: transparent;
stroke: rgba(255, 255, 255, 0.1);
stroke-width: 10;
stroke-dasharray: 50 * $pi;
stroke-dashoffset: 50 * $pi;
}
.RoundGauge__ringFill {
fill: transparent;
stroke: $ring-color;
stroke-width: 10;
stroke-dasharray: 100 * $pi;
transition: stroke 50ms ease-out;
}
.RoundGauge__needle,
.RoundGauge__ringFill {
transition: transform 50ms ease-in-out;
}
.RoundGauge__needleLine,
.RoundGauge__needleMiddle {
fill: colors.$bad;
}
.RoundGauge__alert {
fill-rule: evenodd;
clip-rule: evenodd;
stroke-linejoin: round;
stroke-miterlimit: 2;
fill: rgba(255, 255, 255, 0.1);
}
.RoundGauge__alert.max {
fill: colors.$bad;
}
@each $color-name, $color-value in $fg-map {
.RoundGauge--color--#{$color-name}.RoundGauge__ringFill {
stroke: $color-value;
}
}
@each $color-name, $color-value in $fg-map {
.RoundGauge__alert--#{$color-name} {
fill: $color-value;
transition: opacity 0.6s cubic-bezier(0.25, 1, 0.5, 1);
animation: RoundGauge__alertAnim
1s
cubic-bezier(0.34, 1.56, 0.64, 1)
infinite;
}
}
@keyframes RoundGauge__alertAnim {
0% {
opacity: 0.1;
}
50% {
opacity: 1;
}
100% {
opacity: 0.1;
}
}

View File

@@ -20,7 +20,6 @@
@include meta.load-css('./atomic/text.scss'); @include meta.load-css('./atomic/text.scss');
// Components // Components
@include meta.load-css('./components/BlockQuote.scss');
@include meta.load-css('./components/Button.scss'); @include meta.load-css('./components/Button.scss');
@include meta.load-css('./components/ColorBox.scss'); @include meta.load-css('./components/ColorBox.scss');
@include meta.load-css('./components/Dialog.scss'); @include meta.load-css('./components/Dialog.scss');
@@ -32,13 +31,11 @@
@include meta.load-css('./components/Input.scss'); @include meta.load-css('./components/Input.scss');
@include meta.load-css('./components/Knob.scss'); @include meta.load-css('./components/Knob.scss');
@include meta.load-css('./components/LabeledList.scss'); @include meta.load-css('./components/LabeledList.scss');
@include meta.load-css('./components/MenuBar.scss');
@include meta.load-css('./components/Modal.scss'); @include meta.load-css('./components/Modal.scss');
@include meta.load-css('./components/NanoMap.scss'); @include meta.load-css('./components/NanoMap.scss');
@include meta.load-css('./components/NoticeBox.scss'); @include meta.load-css('./components/NoticeBox.scss');
@include meta.load-css('./components/NumberInput.scss'); @include meta.load-css('./components/NumberInput.scss');
@include meta.load-css('./components/ProgressBar.scss'); @include meta.load-css('./components/ProgressBar.scss');
@include meta.load-css('./components/RoundGauge.scss');
@include meta.load-css('./components/Section.scss'); @include meta.load-css('./components/Section.scss');
@include meta.load-css('./components/Slider.scss'); @include meta.load-css('./components/Slider.scss');
@include meta.load-css('./components/Stack.scss'); @include meta.load-css('./components/Stack.scss');

View File

@@ -60,7 +60,7 @@ module.exports = (env = {}, argv) => {
], ],
}, },
{ {
test: /\.scss$/, test: /\.(s)?css$/,
use: [ use: [
{ {
loader: ExtractCssPlugin.loader, loader: ExtractCssPlugin.loader,

View File

@@ -8236,6 +8236,16 @@ __metadata:
languageName: unknown languageName: unknown
linkType: soft linkType: soft
"tgui-core@npm:^1.2.0":
version: 1.2.0
resolution: "tgui-core@npm:1.2.0"
peerDependencies:
react: ^18.2.0
react-dom: ^18.2.0
checksum: 10c0/1eead0edbe0df5c49bfa88f0d2caa5df743057be75e9c526d0afd838b8def072c67fe60435c66cba52551c4ef70749d60d68094fa103542187008998002714f7
languageName: node
linkType: hard
"tgui-dev-server@workspace:*, tgui-dev-server@workspace:packages/tgui-dev-server": "tgui-dev-server@workspace:*, tgui-dev-server@workspace:packages/tgui-dev-server":
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "tgui-dev-server@workspace:packages/tgui-dev-server" resolution: "tgui-dev-server@workspace:packages/tgui-dev-server"
@@ -8344,6 +8354,7 @@ __metadata:
react: "npm:^18.2.0" react: "npm:^18.2.0"
react-dom: "npm:^18.2.0" react-dom: "npm:^18.2.0"
react-popper: "npm:^2.3.0" react-popper: "npm:^2.3.0"
tgui-core: "npm:^1.2.0"
tgui-dev-server: "workspace:*" tgui-dev-server: "workspace:*"
tgui-polyfill: "workspace:*" tgui-polyfill: "workspace:*"
languageName: unknown languageName: unknown