diff --git a/code/modules/atmospherics/machinery/portable/canister.dm b/code/modules/atmospherics/machinery/portable/canister.dm
index 876f4c9d782..f386d5f9bef 100644
--- a/code/modules/atmospherics/machinery/portable/canister.dm
+++ b/code/modules/atmospherics/machinery/portable/canister.dm
@@ -5,8 +5,8 @@
desc = "A canister for the storage of gas."
icon_state = "yellow"
density = TRUE
- ui_x = 420
- ui_y = 405
+ ui_x = 346
+ ui_y = 268
var/valve_open = FALSE
var/obj/machinery/atmospherics/components/binary/passive_gate/pump
diff --git a/code/modules/power/smes.dm b/code/modules/power/smes.dm
index e2c6745d71c..fc253fd92aa 100644
--- a/code/modules/power/smes.dm
+++ b/code/modules/power/smes.dm
@@ -22,7 +22,7 @@
use_power = NO_POWER_USE
circuit = /obj/item/circuitboard/machine/smes
ui_x = 340
- ui_y = 440
+ ui_y = 350
var/capacity = 5e6 // maximum charge
var/charge = 0 // actual charge
@@ -327,23 +327,21 @@
/obj/machinery/power/smes/ui_data()
var/list/data = list(
- "capacityPercent" = round(100*charge/capacity, 0.1),
"capacity" = capacity,
+ "capacityPercent" = round(100*charge/capacity, 0.1),
"charge" = charge,
-
"inputAttempt" = input_attempt,
"inputting" = inputting,
"inputLevel" = input_level,
"inputLevel_text" = DisplayPower(input_level),
"inputLevelMax" = input_level_max,
- "inputAvailable" = DisplayPower(input_available),
-
+ "inputAvailable" = input_available,
"outputAttempt" = output_attempt,
"outputting" = outputting,
"outputLevel" = output_level,
"outputLevel_text" = DisplayPower(output_level),
"outputLevelMax" = output_level_max,
- "outputUsed" = DisplayPower(output_used)
+ "outputUsed" = output_used,
)
return data
diff --git a/tgui/.eslintrc.yml b/tgui/.eslintrc.yml
index 4bff333295d..e5ebafddcfc 100644
--- a/tgui/.eslintrc.yml
+++ b/tgui/.eslintrc.yml
@@ -625,7 +625,7 @@ rules:
## Prevent invalid characters from appearing in markup
react/no-unescaped-entities: error
## Prevent usage of unknown DOM property (fixable)
- react/no-unknown-property: error
+ # react/no-unknown-property: error
## Prevent usage of unsafe lifecycle methods
react/no-unsafe: error
## Prevent definitions of unused prop types
@@ -705,7 +705,7 @@ rules:
## Validate JSX has key prop when in array or iterator
react/jsx-key: error
## Validate JSX maximum depth
- react/jsx-max-depth: [error, { max: 6 }] ## Generous
+ react/jsx-max-depth: [error, { max: 10 }] ## Generous
## Limit maximum of props on a single line in JSX (fixable)
# react/jsx-max-props-per-line: error
## Prevent usage of .bind() and arrow functions in JSX props
diff --git a/tgui/README.md b/tgui/README.md
index 5fd6434c1ea..a7ef35602ed 100644
--- a/tgui/README.md
+++ b/tgui/README.md
@@ -15,7 +15,7 @@ People come to tgui from different backgrounds and with different
learning styles. Whether you prefer a more theoretical or a practical
approach, we hope you’ll find this section helpful.
-### Practical tutorial
+### Practical Tutorial
If you are completely new to frontend and prefer to **learn by doing**,
start with our [practical tutorial](docs/tutorial-and-examples.md).
@@ -123,7 +123,26 @@ Note that in Windows, you have to go through Advanced System Settings,
System Properties and then open Environment Variables window to do the
same thing. You may need to reboot after this.
-## Project structure
+## Developer Tools
+
+When developing with `tgui-dev-server`, you will have access to certain
+development only features.
+
+**Debug Logs.**
+When running server via `bin/tgui --dev --debug`, server will print debug
+logs and time spent on rendering. Use this information to optimize your
+code, and try to keep re-renders below 16ms.
+
+**Kitchen Sink.**
+Press `Ctrl+Alt+=` to open the KitchenSink interface. This interface is a
+playground to test various tgui components.
+
+**Layout Debugger.**
+Press `Ctrl+Alt+-` to toggle the *layout debugger*. It will show outlines of
+all tgui elements, which makes it easy to understand how everything comes
+together, and can reveal certain layout bugs which are not normally visible.
+
+## Project Structure
- `/packages` - Each folder here represents a self-contained Node module.
- `/packages/common` - Helper functions
@@ -138,17 +157,17 @@ interfaces, otherwise they simply won't load.
window elements, like the titlebar, buttons, resize handlers. Calls
`routes.js` to decide which component to render.
- `/packages/tgui/styles/main.scss` - CSS entry point.
-- `/packages/tgui/styles/atomic.scss` - Atomic CSS classes.
+- `/packages/tgui/styles/atomic` - Atomic CSS classes.
These are very simple, tiny, reusable CSS classes which you can use and
combine to change appearance of your elements. Keep them small.
-- `/packages/tgui/styles/components.scss` - CSS classes which are used
+- `/packages/tgui/styles/components` - CSS classes which are used
in UI components, and most of the stylesheets referenced here are located
in `/packages/tgui/components`. These stylesheets closely follow the
[BEM](https://en.bem.info/methodology/) methodology.
- `/packages/tgui/styles/functions.scss` - Useful SASS functions.
Stuff like `lighten`, `darken`, `luminance` are defined here.
-## Component reference
+## Component Reference
> Notice: This documentation might be out of date, so always check the source
> code to see the most up-to-date information.
@@ -162,17 +181,14 @@ it is used a lot in this framework.
There are a few important semantics you need to know about:
-- `content` prop is a synonym to a `children` prop.
+- Some elements support a `content` prop, which is a synonym to a
+`children` prop.
- `content` is better used when your element is a self-closing tag
(like ``), and when content is small and simple
enough to fit in a prop. Keep in mind, that this prop is **not** native
- to React, and is a feature of this component system.
- - `children` is better used when your element is a full tag (like
- ``), and when content is long and complex. This is
- a native React prop (unlike `content`), and contains all elements you
- defined between the opening and the closing tag of an element.
- - You should never use both on a same element.
+ to React, and is only available on these components: `Button`, `Tooltip`.
- You should never use `children` explicitly as a prop on an element.
+ Instead open a full tag, and place children or text inside the tag.
- Inferno supports both camelcase (`onClick`) and lowercase (`onclick`)
event names.
- Camel case names are what's called "synthetic" events, and are the
@@ -183,8 +199,8 @@ event names.
- Lower case names are native browser events and should be used sparingly,
for example when you need an explicit IE8 support. **DO NOT** use
lowercase event handlers unless you really know what you are doing.
- - [Button](#button) component straight up does not support lowercase event
- handlers. Use the camel case `onClick` instead.
+ - [Button](#button) component does not support lowercase `onclick` event.
+ Use the camel case `onClick` instead.
### `AnimatedNumber`
@@ -430,6 +446,8 @@ Props:
Dims surrounding area to emphasize content placed inside.
+Content is automatically centered inside the dimmer.
+
Props:
- See inherited props: [Box](#box)
@@ -498,6 +516,8 @@ Props:
(1 unit - 0.5em). Does not directly relate to a flex css property
(adds a modifier class under the hood), and only integer numbers are
supported.
+- `inline: boolean` - Makes flexbox container inline, with similar behavior
+to an `inline` property on a `Box`.
- `direction: string` - This establishes the main-axis, thus defining the
direction flex items are placed in the flex container.
- `row` (default) - left to right.
@@ -549,14 +569,22 @@ item should take up. This number is unit-less and is relative to other
siblings.
- `shrink: number` - This defines the ability for a flex item to shrink
if necessary. Inverse of `grow`.
-- `basis: string` - This defines the default size of an element before the
-remaining space is distributed. It can be a length (e.g. `20%`, `5rem`, etc.),
+- `basis: string` - This defines the default size of an element before any
+flex-related calculations are done. Has to be a length (e.g. `20%`, `5rem`),
an `auto` or `content` keyword.
+ - **Important:** IE11 flex is buggy, and auto width/height calculations
+ can sometimes end up in a circular dependency. This usually happens, when
+ working with tables inside flex (they have wacky internal widths and such).
+ Setting basis to `0` breaks the loop and fixes all of the problems.
- `align: string` - This allows the default alignment (or the one specified by
align-items) to be overridden for individual flex items. See: [Flex](#flex).
### `Grid`
+> **Deprecated:** This component is no longer recommended due to the variety
+> of bugs that come with table-based layouts.
+> We recommend using [Flex](#flex) instead.
+
Helps you to divide horizontal space into two or more equal sections.
It is essentially a single-row `Table`, but with some extra features.
@@ -565,10 +593,14 @@ Example:
```jsx
-
+
+ Hello world!
+
-
+
+ Hello world!
+
```
@@ -627,6 +659,44 @@ when this happens. Useful for things like chat inputs.
the text by either unfocusing the input box, or by pressing the Enter key.
- `onInput: (e, value) => void` - An event, which fires on every keypress.
+### `Knob`
+
+A radial control, which allows dialing in precise values by dragging it
+up and down.
+
+Single click opens an input box to manually type in a number.
+
+Props:
+
+- See inherited props: [Box](#box)
+- `animated: boolean` - Animates the value if it was changed externally.
+- `bipolar: boolean` - Knob can be bipolar or unipolar.
+- `size: number` - Relative size of the knob. `1` is normal size, `2` is two
+times bigger. Fractional numbers are supported.
+- `color: string` - Color of the outer ring around the knob.
+- `value: number` - Value itself, controls the position of the cursor.
+- `unit: string` - Unit to display to the right of value.
+- `minValue: number` - Lowest possible value.
+- `maxValue: number` - Highest possible value.
+- `fillValue: number` - If set, this value will be used to set the fill
+percentage of the outer ring independently of the main value.
+- `ranges: { color: [from, to] }` - Applies a `color` to the outer ring around
+the knob based on whether the value lands in the range between `from` and `to`.
+See an example of this prop in [ProgressBar](#progressbar).
+- `step: number` (default: 1) - Adjust value by this amount when
+dragging the input.
+- `stepPixelSize: number` (default: 1) - Screen distance mouse needs
+to travel to adjust value by one `step`.
+- `format: value => value` - Format value using this function before
+displaying it.
+- `suppressFlicker: number` - A number in milliseconds, for which the input
+will hold off from updating while events propagate through the backend.
+Default is about 250ms, increase it if you still see flickering.
+- `onChange: (e, value) => void` - An event, which fires when you release
+the input, or successfully enter a number.
+- `onDrag: (e, value) => void` - An event, which fires about every 500ms
+when you drag the input up and down, on release and on manual editing.
+
### `LabeledList`
LabeledList is a continuous, vertical list of text and other content, where
@@ -795,6 +865,41 @@ means deeper level of nesting. Must be an integer number.
- `buttons: any` - Buttons to render aside the section title.
- `content/children: any` - Content of this section.
+### `Slider`
+
+A horizontal, [ProgressBar](#progressbar)-like control, which allows dialing
+in precise values by dragging it left and right.
+
+Single click opens an input box to manually type in a number.
+
+Props:
+
+- See inherited props: [Box](#box)
+- `animated: boolean` - Animates the value if it was changed externally.
+- `color: string` - Color of the slider.
+- `value: number` - Value itself, controls the position of the cursor.
+- `unit: string` - Unit to display to the right of value.
+- `minValue: number` - Lowest possible value.
+- `maxValue: number` - Highest possible value.
+- `fillValue: number` - If set, this value will be used to set the fill
+percentage of the progress bar filler independently of the main value.
+- `ranges: { color: [from, to] }` - Applies a `color` to the slider
+based on whether the value lands in the range between `from` and `to`.
+See an example of this prop in [ProgressBar](#progressbar).
+- `step: number` (default: 1) - Adjust value by this amount when
+dragging the input.
+- `stepPixelSize: number` (default: 1) - Screen distance mouse needs
+to travel to adjust value by one `step`.
+- `format: value => value` - Format value using this function before
+displaying it.
+- `suppressFlicker: number` - A number in milliseconds, for which the input
+will hold off from updating while events propagate through the backend.
+Default is about 250ms, increase it if you still see flickering.
+- `onChange: (e, value) => void` - An event, which fires when you release
+the input, or successfully enter a number.
+- `onDrag: (e, value) => void` - An event, which fires about every 500ms
+when you drag the input up and down, on release and on manual editing.
+
### `Table`
A straight forward mapping to a standard html table, which is slightly
diff --git a/tgui/packages/common/math.js b/tgui/packages/common/math.js
index a33b9aa214c..f2918c5c111 100644
--- a/tgui/packages/common/math.js
+++ b/tgui/packages/common/math.js
@@ -1,10 +1,22 @@
/**
* Limits a number to the range between 'min' and 'max'.
*/
-export const clamp = (value, min = 0, max = 1) => {
+export const clamp = (value, min, max) => {
return Math.max(min, Math.min(value, max));
};
+/**
+ * Limits a number between 0 and 1.
+ */
+export const clamp01 = value => clamp(value, 0, 1);
+
+/**
+ * Scales a number to fit into the range between min and max.
+ */
+export const scale = (value, min, max) => {
+ return (value - min) / (max - min);
+};
+
/**
* Returns a rounded number.
* TODO: Replace this native rounding function with a more robust one.
@@ -15,5 +27,31 @@ export const round = value => Math.round(value);
* Returns a string representing a number in fixed point notation.
*/
export const toFixed = (value, fractionDigits = 0) => {
- return Number(value).toFixed(fractionDigits);
+ return Number(value).toFixed(Math.max(fractionDigits, 0));
+};
+
+/**
+ * Checks whether a value is within the provided range.
+ *
+ * Range is an array of two numbers, for example: [0, 15].
+ */
+export const inRange = (value, range) => {
+ return range
+ && value >= range[0]
+ && value <= range[1];
+};
+
+/**
+ * Walks over the object with ranges, comparing value against every range,
+ * and returns the key of the first matching range.
+ *
+ * Range is an array of two numbers, for example: [0, 15].
+ */
+export const keyOfMatchingRange = (value, ranges) => {
+ for (let rangeName of Object.keys(ranges)) {
+ const range = ranges[rangeName];
+ if (inRange(value, range)) {
+ return rangeName;
+ }
+ }
};
diff --git a/tgui/packages/tgui/components/Box.js b/tgui/packages/tgui/components/Box.js
index b0f56e67e85..4433e10107c 100644
--- a/tgui/packages/tgui/components/Box.js
+++ b/tgui/packages/tgui/components/Box.js
@@ -123,43 +123,53 @@ export const computeBoxProps = props => {
}
}
// Concatenate styles
- Object.assign(computedStyles, props.style);
let style = '';
for (let attrName of Object.keys(computedStyles)) {
const attrValue = computedStyles[attrName];
style += attrName + ':' + attrValue + ';';
}
+ if (props.style) {
+ for (let attrName of Object.keys(props.style)) {
+ const attrValue = props.style[attrName];
+ style += attrName + ':' + attrValue + ';';
+ }
+ }
if (style.length > 0) {
computedProps.style = style;
}
return computedProps;
};
+export const computeBoxClassName = props => {
+ const color = props.textColor || props.color;
+ const backgroundColor = props.backgroundColor;
+ return classes([
+ isColorClass(color) && 'color-' + color,
+ isColorClass(backgroundColor) && 'color-bg-' + backgroundColor,
+ ]);
+};
+
export const Box = props => {
const {
as = 'div',
className,
- content,
children,
...rest
} = props;
- const color = props.textColor || props.color;
- const backgroundColor = props.backgroundColor;
// Render props
if (typeof children === 'function') {
return children(computeBoxProps(props));
}
+ const computedClassName = typeof className === 'string'
+ ? className + ' ' + computeBoxClassName(rest)
+ : computeBoxClassName(rest);
const computedProps = computeBoxProps(rest);
// Render a wrapper element
return createVNode(
VNodeFlags.HtmlElement,
as,
- classes([
- className,
- isColorClass(color) && 'color-' + color,
- isColorClass(backgroundColor) && 'color-bg-' + backgroundColor,
- ]),
- content || children,
+ computedClassName,
+ children,
ChildFlags.UnknownChildren,
computedProps);
};
diff --git a/tgui/packages/tgui/components/Button.js b/tgui/packages/tgui/components/Button.js
index 8d67c15d748..3b7c3a3776e 100644
--- a/tgui/packages/tgui/components/Button.js
+++ b/tgui/packages/tgui/components/Button.js
@@ -41,7 +41,7 @@ export const Button = props => {
// IE8: Use a lowercase "onclick" because synthetic events are fucked.
// IE8: Use an "unselectable" prop because "user-select" doesn't work.
return (
- {
])}
color={content ? null : 'transparent'}
backgroundColor={color}
- content={content || '.'}
- {...rest} />
+ {...rest}>
+ {content || '.'}
+
);
};
diff --git a/tgui/packages/tgui/components/DraggableControl.js b/tgui/packages/tgui/components/DraggableControl.js
new file mode 100644
index 00000000000..c540401b379
--- /dev/null
+++ b/tgui/packages/tgui/components/DraggableControl.js
@@ -0,0 +1,274 @@
+import { clamp } from 'common/math';
+import { pureComponentHooks } from 'common/react';
+import { Component, createRef } from 'inferno';
+import { AnimatedNumber } from './AnimatedNumber';
+
+/**
+ * Reduces screen offset to a single number based on the matrix provided.
+ */
+const getScalarScreenOffset = (e, matrix) => {
+ return e.screenX * matrix[0] + e.screenY * matrix[1];
+};
+
+export class DraggableControl extends Component {
+ constructor(props) {
+ super(props);
+ this.inputRef = createRef();
+ this.state = {
+ value: props.value,
+ dragging: false,
+ editing: false,
+ internalValue: null,
+ origin: null,
+ suppressingFlicker: false,
+ };
+
+ // Suppresses flickering while the value propagates through the backend
+ this.flickerTimer = null;
+ this.suppressFlicker = () => {
+ const { suppressFlicker } = this.props;
+ if (suppressFlicker > 0) {
+ this.setState({
+ suppressingFlicker: true,
+ });
+ clearTimeout(this.flickerTimer);
+ this.flickerTimer = setTimeout(() => this.setState({
+ suppressingFlicker: false,
+ }), suppressFlicker);
+ }
+ };
+
+ this.handleDragStart = e => {
+ const {
+ value,
+ dragMatrix,
+ } = this.props;
+ const { editing } = this.state;
+ if (editing) {
+ return;
+ }
+ document.body.style['pointer-events'] = 'none';
+ this.ref = e.target;
+ this.setState({
+ dragging: false,
+ origin: getScalarScreenOffset(e, dragMatrix),
+ value,
+ internalValue: value,
+ });
+ this.timer = setTimeout(() => {
+ this.setState({
+ dragging: true,
+ });
+ }, 250);
+ this.dragInterval = setInterval(() => {
+ const { dragging, value } = this.state;
+ const { onDrag } = this.props;
+ if (dragging && onDrag) {
+ onDrag(e, value);
+ }
+ }, 500);
+ document.addEventListener('mousemove', this.handleDragMove);
+ document.addEventListener('mouseup', this.handleDragEnd);
+ };
+
+ this.handleDragMove = e => {
+ const {
+ minValue,
+ maxValue,
+ step,
+ stepPixelSize,
+ dragMatrix,
+ } = this.props;
+ this.setState(prevState => {
+ const state = { ...prevState };
+ const offset = getScalarScreenOffset(e, dragMatrix) - state.origin;
+ if (prevState.dragging) {
+ const stepOffset = Number.isFinite(minValue)
+ ? minValue % step
+ : 0;
+ // Translate mouse movement to value
+ // Give it some headroom (by increasing clamp range by 1 step)
+ state.internalValue = clamp(
+ state.internalValue
+ + offset * step / stepPixelSize,
+ minValue - step,
+ maxValue + step);
+ // Clamp the final value
+ state.value = clamp(
+ state.internalValue
+ - state.internalValue % step
+ + stepOffset,
+ minValue,
+ maxValue);
+ state.origin = getScalarScreenOffset(e, dragMatrix);
+ }
+ else if (Math.abs(offset) > 4) {
+ state.dragging = true;
+ }
+ return state;
+ });
+ };
+
+ this.handleDragEnd = e => {
+ const {
+ onChange,
+ onDrag,
+ } = this.props;
+ const {
+ dragging,
+ value,
+ internalValue,
+ } = this.state;
+ document.body.style['pointer-events'] = 'auto';
+ clearTimeout(this.timer);
+ clearInterval(this.dragInterval);
+ this.setState({
+ dragging: false,
+ editing: !dragging,
+ origin: null,
+ });
+ document.removeEventListener('mousemove', this.handleDragMove);
+ document.removeEventListener('mouseup', this.handleDragEnd);
+ if (dragging) {
+ this.suppressFlicker();
+ if (onChange) {
+ onChange(e, value);
+ }
+ if (onDrag) {
+ onDrag(e, value);
+ }
+ }
+ else if (this.inputRef) {
+ const input = this.inputRef.current;
+ input.value = internalValue;
+ // IE8: Dies when trying to focus a hidden element
+ // (Error: Object does not support this action)
+ try {
+ input.focus();
+ input.select();
+ }
+ catch {}
+ }
+ };
+ }
+
+ render() {
+ const {
+ dragging,
+ editing,
+ value: intermediateValue,
+ suppressingFlicker,
+ } = this.state;
+ const {
+ animated,
+ value,
+ unit,
+ minValue,
+ maxValue,
+ format,
+ onChange,
+ onDrag,
+ children,
+ // Input props
+ height,
+ lineHeight,
+ fontSize,
+ } = this.props;
+ let displayValue = value;
+ if (dragging || suppressingFlicker) {
+ displayValue = intermediateValue;
+ }
+ // Setup a display element
+ // Shows a formatted number based on what we are currently doing
+ // with the draggable surface.
+ const renderDisplayElement = value => (
+ value + (unit ? ' ' + unit : '')
+ );
+ const displayElement = (
+ animated && !dragging && !suppressingFlicker && (
+
+ {renderDisplayElement}
+
+ ) || (
+ renderDisplayElement(format
+ ? format(displayValue)
+ : displayValue)
+ )
+ );
+ // Setup an input element
+ // Handles direct input via the keyboard
+ const inputElement = (
+ {
+ if (!editing) {
+ return;
+ }
+ const value = clamp(e.target.value, minValue, maxValue);
+ this.setState({
+ editing: false,
+ value,
+ });
+ this.suppressFlicker();
+ if (onChange) {
+ onChange(e, value);
+ }
+ if (onDrag) {
+ onDrag(e, value);
+ }
+ }}
+ onKeyDown={e => {
+ if (e.keyCode === 13) {
+ const value = clamp(e.target.value, minValue, maxValue);
+ this.setState({
+ editing: false,
+ value,
+ });
+ this.suppressFlicker();
+ if (onChange) {
+ onChange(e, value);
+ }
+ if (onDrag) {
+ onDrag(e, value);
+ }
+ return;
+ }
+ if (e.keyCode === 27) {
+ this.setState({
+ editing: false,
+ });
+ return;
+ }
+ }} />
+ );
+ // Return a part of the state for higher-level components to use.
+ return children({
+ dragging,
+ editing,
+ value,
+ displayValue,
+ displayElement,
+ inputElement,
+ handleDragStart: this.handleDragStart,
+ });
+ }
+}
+
+DraggableControl.defaultHooks = pureComponentHooks;
+DraggableControl.defaultProps = {
+ minValue: -Infinity,
+ maxValue: +Infinity,
+ step: 1,
+ stepPixelSize: 1,
+ suppressFlicker: 50,
+ dragMatrix: [1, 0],
+};
diff --git a/tgui/packages/tgui/components/Flex.js b/tgui/packages/tgui/components/Flex.js
index 4b5cb71115c..420f39d079b 100644
--- a/tgui/packages/tgui/components/Flex.js
+++ b/tgui/packages/tgui/components/Flex.js
@@ -8,12 +8,14 @@ export const computeFlexProps = props => {
wrap,
align,
justify,
+ inline,
spacing = 0,
...rest
} = props;
return {
className: classes([
'Flex',
+ inline && 'Flex--inline',
spacing > 0 && 'Flex--spacing--' + spacing,
className,
]),
diff --git a/tgui/packages/tgui/components/Knob.js b/tgui/packages/tgui/components/Knob.js
new file mode 100644
index 00000000000..c13548aa489
--- /dev/null
+++ b/tgui/packages/tgui/components/Knob.js
@@ -0,0 +1,128 @@
+import { keyOfMatchingRange, scale } from 'common/math';
+import { classes } from 'common/react';
+import { computeBoxClassName, computeBoxProps } from './Box';
+import { DraggableControl } from './DraggableControl';
+
+export const Knob = props => {
+ const {
+ // Draggable props (passthrough)
+ animated,
+ format,
+ maxValue,
+ minValue,
+ onChange,
+ onDrag,
+ step,
+ stepPixelSize,
+ suppressFlicker,
+ unit,
+ value,
+ // Own props
+ className,
+ style,
+ fillValue,
+ color,
+ ranges = {},
+ size,
+ bipolar,
+ children,
+ ...rest
+ } = props;
+ return (
+
+ {control => {
+ const {
+ dragging,
+ editing,
+ value,
+ displayValue,
+ displayElement,
+ inputElement,
+ handleDragStart,
+ } = control;
+ const scaledFillValue = scale(
+ fillValue ?? displayValue,
+ minValue,
+ maxValue);
+ const scaledDisplayValue = scale(
+ displayValue,
+ minValue,
+ maxValue);
+ const effectiveColor = color
+ || keyOfMatchingRange(fillValue ?? value, ranges)
+ || 'default';
+ const rotation = (scaledDisplayValue - 0.5) * 270;
+ return (
+
+
+
+
+
+
+ {dragging && (
+
+ {displayElement}
+
+ )}
+
+
+ {inputElement}
+
+ );
+ }}
+
+ );
+};
diff --git a/tgui/packages/tgui/components/NumberInput.js b/tgui/packages/tgui/components/NumberInput.js
index 52f6a878652..c9f9846e2f3 100644
--- a/tgui/packages/tgui/components/NumberInput.js
+++ b/tgui/packages/tgui/components/NumberInput.js
@@ -77,13 +77,15 @@ export class NumberInput extends Component {
// Give it some headroom (by increasing clamp range by 1 step)
state.internalValue = clamp(
state.internalValue + offset * step / stepPixelSize,
- minValue - step, maxValue + step);
+ minValue - step,
+ maxValue + step);
// Clamp the final value
state.value = clamp(
state.internalValue
- state.internalValue % step
+ stepOffset,
- minValue, maxValue);
+ minValue,
+ maxValue);
state.origin = e.screenY;
}
else if (Math.abs(offset) > 4) {
diff --git a/tgui/packages/tgui/components/ProgressBar.js b/tgui/packages/tgui/components/ProgressBar.js
index e58bfa38693..db3ada7023a 100644
--- a/tgui/packages/tgui/components/ProgressBar.js
+++ b/tgui/packages/tgui/components/ProgressBar.js
@@ -1,47 +1,41 @@
+import { clamp, keyOfMatchingRange, toFixed } from 'common/math';
import { classes, pureComponentHooks } from 'common/react';
-import { clamp, toFixed } from 'common/math';
+import { computeBoxClassName, computeBoxProps } from './Box';
export const ProgressBar = props => {
const {
+ className,
value,
minValue = 0,
maxValue = 1,
+ color,
ranges = {},
- content,
children,
+ ...rest
} = props;
const scaledValue = (value - minValue) / (maxValue - minValue);
- const hasContent = content !== undefined || children !== undefined;
- let { color } = props;
- // Cycle through ranges in key order to determine progressbar color.
- if (!color) {
- for (let rangeName of Object.keys(ranges)) {
- const range = ranges[rangeName];
- if (range && value >= range[0] && value <= range[1]) {
- color = rangeName;
- break;
- }
- }
- }
- // Default color
- if (!color) {
- color = 'default';
- }
+ const hasContent = children !== undefined;
+ const effectiveColor = color
+ || keyOfMatchingRange(value, ranges)
+ || 'default';
return (