mirror of
https://github.com/CHOMPStation2/CHOMPStation2.git
synced 2025-12-15 20:52:41 +00:00
[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:
@@ -24,7 +24,6 @@
|
||||
@include meta.load-css('~tgui/styles/atomic/text.scss');
|
||||
|
||||
// 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/ColorBox.scss');
|
||||
@include meta.load-css('~tgui/styles/components/Dimmer.scss');
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -6,32 +6,23 @@
|
||||
|
||||
export { AnimatedNumber } from './AnimatedNumber';
|
||||
export { Autofocus } from './Autofocus';
|
||||
export { Blink } from './Blink';
|
||||
export { BlockQuote } from './BlockQuote';
|
||||
export { Box } from './Box';
|
||||
export { Button } from './Button';
|
||||
export { ByondUi } from './ByondUi';
|
||||
export { Chart } from './Chart';
|
||||
export { Collapsible } from './Collapsible';
|
||||
export { ColorBox } from './ColorBox';
|
||||
export { Dialog } from './Dialog';
|
||||
export { Dimmer } from './Dimmer';
|
||||
export { Divider } from './Divider';
|
||||
export { DmIcon } from './DmIcon';
|
||||
export { DraggableControl } from './DraggableControl';
|
||||
export { Dropdown } from './Dropdown';
|
||||
export { FitText } from './FitText';
|
||||
export { Flex } from './Flex';
|
||||
export { Grid } from './Grid';
|
||||
export { Icon } from './Icon';
|
||||
export { Image } from './Image';
|
||||
export { InfinitePlane } from './InfinitePlane';
|
||||
export { Input } from './Input';
|
||||
export { KeyListener } from './KeyListener';
|
||||
export { Knob } from './Knob';
|
||||
export { LabeledControls } from './LabeledControls';
|
||||
export { LabeledList } from './LabeledList';
|
||||
export { MenuBar } from './MenuBar';
|
||||
export { Modal } from './Modal';
|
||||
export { NanoMap } from './NanoMap';
|
||||
export { NoticeBox } from './NoticeBox';
|
||||
@@ -39,15 +30,10 @@ export { NumberInput } from './NumberInput';
|
||||
export { Popper } from './Popper';
|
||||
export { ProgressBar } from './ProgressBar';
|
||||
export { RestrictedInput } from './RestrictedInput';
|
||||
export { RoundGauge } from './RoundGauge';
|
||||
export { Section } from './Section';
|
||||
export { Slider } from './Slider';
|
||||
export { Stack } from './Stack';
|
||||
export { StyleableSection } from './StyleableSection';
|
||||
export { Table } from './Table';
|
||||
export { Tabs } from './Tabs';
|
||||
export { TextArea } from './TextArea';
|
||||
export { TimeDisplay } from './TimeDisplay';
|
||||
export { Tooltip } from './Tooltip';
|
||||
export { TrackOutsideClicks } from './TrackOutsideClicks';
|
||||
export { VirtualList } from './VirtualList';
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import { capitalize, decodeHtmlEntities } from 'common/string';
|
||||
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 {
|
||||
AppearanceChangerEars,
|
||||
AppearanceChangerGender,
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { capitalize } from 'common/string';
|
||||
|
||||
import { useBackend } from '../../backend';
|
||||
import { useBackend } from 'tgui/backend';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ByondUi,
|
||||
ColorBox,
|
||||
Flex,
|
||||
LabeledList,
|
||||
Section,
|
||||
} from '../../components';
|
||||
} from 'tgui/components';
|
||||
import { ByondUi } from 'tgui-core/components';
|
||||
|
||||
import { activeBodyRecord } from './types';
|
||||
|
||||
export const BodyDesignerSpecificRecord = (props: {
|
||||
|
||||
@@ -3,10 +3,10 @@ import { flow } from 'common/fp';
|
||||
import { BooleanLike, classes } from 'common/react';
|
||||
import { createSearch } from 'common/string';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useBackend } from '../backend';
|
||||
import { Button, ByondUi, Dropdown, Flex, Input, Section } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
import { useBackend } from 'tgui/backend';
|
||||
import { Button, Dropdown, Flex, Input, Section } from 'tgui/components';
|
||||
import { Window } from 'tgui/layouts';
|
||||
import { ByondUi } from 'tgui-core/components';
|
||||
|
||||
type activeCamera = { name: string; status: BooleanLike } | null;
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
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 { Data } from './types';
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useBackend } from '../backend';
|
||||
import { Button, ByondUi } from '../components';
|
||||
import { NtosWindow } from '../layouts';
|
||||
import { useBackend } from 'tgui/backend';
|
||||
import { Button } from 'tgui/components';
|
||||
import { NtosWindow } from 'tgui/layouts';
|
||||
import { ByondUi } from 'tgui-core/components';
|
||||
|
||||
import {
|
||||
camera,
|
||||
CameraConsoleContent,
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -16,6 +16,7 @@
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-popper": "^2.3.0",
|
||||
"tgui-core": "^1.2.0",
|
||||
"tgui-dev-server": "workspace:*",
|
||||
"tgui-polyfill": "workspace:*"
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -5,8 +5,9 @@
|
||||
*/
|
||||
|
||||
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';
|
||||
|
||||
export const meta = {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,6 @@
|
||||
@include meta.load-css('./atomic/text.scss');
|
||||
|
||||
// Components
|
||||
@include meta.load-css('./components/BlockQuote.scss');
|
||||
@include meta.load-css('./components/Button.scss');
|
||||
@include meta.load-css('./components/ColorBox.scss');
|
||||
@include meta.load-css('./components/Dialog.scss');
|
||||
@@ -32,13 +31,11 @@
|
||||
@include meta.load-css('./components/Input.scss');
|
||||
@include meta.load-css('./components/Knob.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/NanoMap.scss');
|
||||
@include meta.load-css('./components/NoticeBox.scss');
|
||||
@include meta.load-css('./components/NumberInput.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/Slider.scss');
|
||||
@include meta.load-css('./components/Stack.scss');
|
||||
|
||||
@@ -60,7 +60,7 @@ module.exports = (env = {}, argv) => {
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
test: /\.(s)?css$/,
|
||||
use: [
|
||||
{
|
||||
loader: ExtractCssPlugin.loader,
|
||||
|
||||
@@ -8236,6 +8236,16 @@ __metadata:
|
||||
languageName: unknown
|
||||
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":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "tgui-dev-server@workspace:packages/tgui-dev-server"
|
||||
@@ -8344,6 +8354,7 @@ __metadata:
|
||||
react: "npm:^18.2.0"
|
||||
react-dom: "npm:^18.2.0"
|
||||
react-popper: "npm:^2.3.0"
|
||||
tgui-core: "npm:^1.2.0"
|
||||
tgui-dev-server: "workspace:*"
|
||||
tgui-polyfill: "workspace:*"
|
||||
languageName: unknown
|
||||
|
||||
Reference in New Issue
Block a user