tgui: Knobs, Sliders and Performance Improvements (#50483)

* Optimize Box

* Some tweaks to vending machines

* Layout debugger, initial work on sliders

* Slider, Knob, docs, refactoring, etc

* Rebuild tgui

* Knobby Canister

* Remove imports
This commit is contained in:
Aleksej Komarov
2020-04-17 03:06:24 +08:00
committed by GitHub
parent 08427fa56c
commit 947b74cdec
52 changed files with 1583 additions and 472 deletions
@@ -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
+4 -6
View File
@@ -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
+2 -2
View File
@@ -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
+123 -18
View File
@@ -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 youll 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 `<Button content="Hello" />`), 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
`<Button>Hello</Button>`), 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
<Grid>
<Grid.Column>
<Section title="Section 1" content="Hello world!" />
<Section title="Section 1">
Hello world!
</Section>
</Grid.Column>
<Grid.Column size={2}>
<Section title="Section 2" content="Hello world!" />
<Section title="Section 2">
Hello world!
</Section>
</Grid.Column>
</Grid>
```
@@ -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
+40 -2
View File
@@ -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;
}
}
};
+20 -10
View File
@@ -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);
};
+1 -1
View File
@@ -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 (
<Box as="span"
<Box
className={classes([
'Button',
fluid && 'Button--fluid',
+3 -2
View File
@@ -11,8 +11,9 @@ export const ColorBox = props => {
])}
color={content ? null : 'transparent'}
backgroundColor={color}
content={content || '.'}
{...rest} />
{...rest}>
{content || '.'}
</Box>
);
};
@@ -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 && (
<AnimatedNumber
value={displayValue}
format={format}>
{renderDisplayElement}
</AnimatedNumber>
) || (
renderDisplayElement(format
? format(displayValue)
: displayValue)
)
);
// Setup an input element
// Handles direct input via the keyboard
const inputElement = (
<input
ref={this.inputRef}
className="NumberInput__input"
style={{
display: !editing ? 'none' : undefined,
height: height,
'line-height': lineHeight,
'font-size': fontSize,
}}
onBlur={e => {
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],
};
+2
View File
@@ -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,
]),
+128
View File
@@ -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 (
<DraggableControl
dragMatrix={[0, -1]}
{...{
animated,
format,
maxValue,
minValue,
onChange,
onDrag,
step,
stepPixelSize,
suppressFlicker,
unit,
value,
}}>
{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 (
<div
className={classes([
'Knob',
'Knob--color--' + effectiveColor,
bipolar && 'Knob--bipolar',
className,
computeBoxClassName(rest),
])}
style={{
'font-size': size + 'rem',
...style,
}}
{...computeBoxProps(rest)}
onMouseDown={handleDragStart}>
<div className="Knob__circle">
<div
className="Knob__cursorBox"
style={{
transform: `rotate(${rotation}deg)`,
}}>
<div className="Knob__cursor" />
</div>
</div>
{dragging && (
<div className="Knob__popupValue">
{displayElement}
</div>
)}
<svg
className="Knob__ring Knob__ringTrackPivot"
viewBox="0 0 100 100">
<circle
className="Knob__ringTrack"
cx="50"
cy="50"
r="50" />
</svg>
<svg
className="Knob__ring Knob__ringFillPivot"
viewBox="0 0 100 100">
<circle
className="Knob__ringFill"
style={{
'stroke-dashoffset': (
((bipolar ? 2.75 : 2.00) - scaledFillValue * 1.5)
* Math.PI * 50
),
}}
cx="50"
cy="50"
r="50" />
</svg>
{inputElement}
</div>
);
}}
</DraggableControl>
);
};
+4 -2
View File
@@ -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) {
+18 -24
View File
@@ -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 (
<div
className={classes([
'ProgressBar',
'ProgressBar--color--' + color,
])}>
'ProgressBar--color--' + effectiveColor,
className,
computeBoxClassName(rest),
])}
{...computeBoxProps(rest)}>
<div
className="ProgressBar__fill"
style={{
'width': (clamp(scaledValue, 0, 1) * 100) + '%',
width: (clamp(scaledValue, 0, 1) * 100) + '%',
}} />
<div className="ProgressBar__content">
{hasContent && content}
{hasContent && children}
{!hasContent && toFixed(scaledValue * 100) + '%'}
{hasContent
? children
: toFixed(scaledValue * 100) + '%'}
</div>
</div>
);
+122
View File
@@ -0,0 +1,122 @@
import { clamp01, keyOfMatchingRange, scale } from 'common/math';
import { classes } from 'common/react';
import { computeBoxClassName, computeBoxProps } from './Box';
import { DraggableControl } from './DraggableControl';
export const Slider = props => {
const {
// Draggable props (passthrough)
animated,
format,
maxValue,
minValue,
onChange,
onDrag,
step,
stepPixelSize,
suppressFlicker,
unit,
value,
// Own props
className,
fillValue,
color,
ranges = {},
children,
...rest
} = props;
const hasContent = children !== undefined;
return (
<DraggableControl
dragMatrix={[1, 0]}
{...{
animated,
format,
maxValue,
minValue,
onChange,
onDrag,
step,
stepPixelSize,
suppressFlicker,
unit,
value,
}}>
{control => {
const {
dragging,
editing,
value,
displayValue,
displayElement,
inputElement,
handleDragStart,
} = control;
const hasFillValue = fillValue !== undefined
&& fillValue !== null;
const scaledValue = scale(
value,
minValue,
maxValue);
const scaledFillValue = scale(
fillValue ?? displayValue,
minValue,
maxValue);
const scaledDisplayValue = scale(
displayValue,
minValue,
maxValue);
const effectiveColor = color
|| keyOfMatchingRange(fillValue ?? value, ranges)
|| 'default';
return (
<div
className={classes([
'Slider',
'ProgressBar',
'ProgressBar--color--' + effectiveColor,
className,
computeBoxClassName(rest),
])}
{...computeBoxProps(rest)}
onMouseDown={handleDragStart}>
<div
className={classes([
'ProgressBar__fill',
hasFillValue && 'ProgressBar__fill--animated',
])}
style={{
width: clamp01(scaledFillValue) * 100 + '%',
opacity: 0.4,
}} />
<div
className="ProgressBar__fill"
style={{
width: clamp01(Math.min(scaledFillValue, scaledDisplayValue))
* 100 + '%',
}} />
<div
className="Slider__cursorOffset"
style={{
width: clamp01(scaledDisplayValue) * 100 + '%',
}}>
<div className="Slider__cursor" />
<div className="Slider__pointer" />
{dragging && (
<div className="Slider__popupValue">
{displayElement}
</div>
)}
</div>
<div className="ProgressBar__content">
{hasContent
? children
: displayElement}
</div>
{inputElement}
</div>
);
}}
</DraggableControl>
);
};
+28 -17
View File
@@ -1,55 +1,66 @@
import { classes, pureComponentHooks } from 'common/react';
import { Box } from './Box';
import { Box, computeBoxClassName, computeBoxProps } from './Box';
export const Table = props => {
const { collapsing, className, content, children, ...rest } = props;
const {
className,
collapsing,
children,
...rest
} = props;
return (
<Box
as="table"
<table
className={classes([
'Table',
collapsing && 'Table--collapsing',
className,
computeBoxClassName(rest),
])}
{...rest}>
{...computeBoxProps(rest)}>
<tbody>
{content}
{children}
</tbody>
</Box>
</table>
);
};
Table.defaultHooks = pureComponentHooks;
export const TableRow = props => {
const { className, header, ...rest } = props;
const {
className,
header,
...rest
} = props;
return (
<Box
as="tr"
<tr
className={classes([
'Table__row',
header && 'Table__row--header',
className,
computeBoxClassName(props),
])}
{...rest} />
{...computeBoxProps(rest)} />
);
};
TableRow.defaultHooks = pureComponentHooks;
export const TableCell = props => {
const { className, collapsing, header, ...rest } = props;
const {
className,
collapsing,
header,
...rest
} = props;
return (
<Box
as="td"
<td
className={classes([
'Table__cell',
collapsing && 'Table__cell--collapsing',
header && 'Table__cell--header',
className,
computeBoxClassName(props),
])}
{...rest} />
{...computeBoxProps(rest)} />
);
};
+2
View File
@@ -13,11 +13,13 @@ export { Flex } from './Flex';
export { Grid } from './Grid';
export { Icon } from './Icon';
export { Input } from './Input';
export { Knob } from './Knob';
export { LabeledList } from './LabeledList';
export { NoticeBox } from './NoticeBox';
export { NumberInput } from './NumberInput';
export { ProgressBar } from './ProgressBar';
export { Section } from './Section';
export { Slider } from './Slider';
export { Table } from './Table';
export { Tabs } from './Tabs';
export { TitleBar } from './TitleBar';
+56
View File
@@ -0,0 +1,56 @@
import { clamp, toFixed } from 'common/math';
const SI_SYMBOLS = [
'f', // femto
'p', // pico
'n', // nano
'μ', // micro
'm', // milli
// NOTE: This is a space for a reason. When we right align si numbers,
// in monospace mode, we want to units and numbers stay in their respective
// columns. If rendering in HTML mode, this space will collapse into
// a single space anyway.
' ',
'k', // kilo
'M', // mega
'G', // giga
'T', // tera
'P', // peta
'E', // exa
'Z', // zetta
'Y', // yotta
];
const SI_BASE_INDEX = SI_SYMBOLS.indexOf(' ');
/**
* Formats a number to a human readable form, by reducing it to SI units.
* TODO: This is quite a shit code and shit math, needs optimization.
*/
const formatSiUnit = (value, minBase1000 = -SI_BASE_INDEX, unit = '') => {
const realBase10 = Math.floor(Math.log10(value));
const base10 = Math.floor(Math.max(minBase1000 * 3, realBase10));
const realBase1000 = Math.floor(realBase10 / 3);
const base1000 = Math.floor(base10 / 3);
const symbolIndex = clamp(
SI_BASE_INDEX + base1000,
0,
SI_SYMBOLS.length);
const symbol = SI_SYMBOLS[symbolIndex];
const scaledNumber = value / Math.pow(1000, base1000);
const scaledPrecision = realBase1000 > minBase1000
? (2 + base1000 * 3 - base10)
: 0;
// TODO: Make numbers bigger than precision value show
// up to 2 decimal numbers.
const finalString = (
toFixed(scaledNumber, scaledPrecision)
+ ' ' + symbol + unit
);
return finalString.trim();
};
export const formatPower = (value, minBase1000 = 0) => {
return formatSiUnit(value, minBase1000, 'W');
};
+7
View File
@@ -251,6 +251,13 @@ export const hotKeyReducer = (state, action) => {
showKitchenSink: !state.showKitchenSink,
};
}
// Toggle layout debugger
if (ctrlKey && altKey && keyCode === KEY_MINUS) {
return {
...state,
debugLayout: !state.debugLayout,
};
}
return state;
}
return state;
@@ -16,9 +16,9 @@ export const Achievement = props => {
<td style={{ 'vertical-align': 'top' }}>
<h1>{name}</h1>
{desc}
<Box
color={value ? 'good' : 'bad'}
content={value ? 'Unlocked' : 'Locked'} />
<Box color={value ? 'good' : 'bad'}>
{value ? 'Unlocked' : 'Locked'}
</Box>
</td>
</tr>
);
@@ -39,9 +39,9 @@ export const Score = props => {
<td style={{ 'vertical-align': 'top' }}>
<h1>{name}</h1>
{desc}
<Box
color={value > 0 ? 'good' : 'bad'}
content={value > 0 ? `Earned ${value} times` : 'Locked'} />
<Box color={value > 0 ? 'good' : 'bad'}>
{value > 0 ? `Earned ${value} times` : 'Locked'}
</Box>
</td>
</tr>
);
@@ -105,7 +105,7 @@ export const BlackmarketUplink = props => {
</Table.Cell>
<Table.Cell collapsing textAlign="right">
<Button
content={'Buy'}
content="Buy"
disabled={!item.amount || item.cost > data.money}
onClick={() => act('select', {
item: item.id,
+7 -4
View File
@@ -43,11 +43,14 @@ export const BorgPanel = props => {
<LabeledList.Item label="Charge">
{!cell.missing ? (
<ProgressBar
value={cellPercent}
content={cell.charge + ' / ' + cell.maxcharge} />
value={cellPercent}>
{cell.charge + ' / ' + cell.maxcharge}
</ProgressBar>
) : (
<span className="color-bad">No cell installed</span>
) }
<span className="color-bad">
No cell installed
</span>
)}
<br />
<Button
icon="pencil-alt"
+81 -76
View File
@@ -1,13 +1,26 @@
import { Fragment } from 'inferno';
import { useBackend } from '../backend';
import { AnimatedNumber, Box, Button, LabeledList, NoticeBox, ProgressBar, Section } from '../components';
import { AnimatedNumber, Box, Button, Flex, Knob, LabeledList, NoticeBox, Section } from '../components';
export const Canister = props => {
const { act, data } = useBackend(props);
const {
portConnected,
tankPressure,
releasePressure,
defaultReleasePressure,
minReleasePressure,
maxReleasePressure,
valveOpen,
isPrototype,
hasHoldingTank,
holdingTank,
restricted,
} = data;
return (
<Fragment>
<NoticeBox>
The regulator {data.hasHoldingTank ? 'is' : 'is not'} connected
The regulator {hasHoldingTank ? 'is' : 'is not'} connected
to a tank.
</NoticeBox>
<Section
@@ -18,99 +31,91 @@ export const Canister = props => {
content="Relabel"
onClick={() => act('relabel')} />
)}>
<LabeledList>
<LabeledList.Item label="Pressure">
<AnimatedNumber value={data.tankPressure} /> kPa
</LabeledList.Item>
<LabeledList.Item
label="Port"
color={data.portConnected ? 'good' : 'average'}
content={data.portConnected ? 'Connected' : 'Not Connected'} />
{!!data.isPrototype && (
<LabeledList.Item label="Access">
<Button
icon={data.restricted ? 'lock' : 'unlock'}
color="caution"
content={data.restricted
? 'Restricted to Engineering'
: 'Public'}
onClick={() => act('restricted')} />
</LabeledList.Item>
)}
</LabeledList>
</Section>
<Section title="Valve">
<LabeledList>
<LabeledList.Item label="Release Pressure">
<ProgressBar
value={data.releasePressure
/ (data.maxReleasePressure - data.minReleasePressure)}>
<AnimatedNumber value={data.releasePressure} /> kPa
</ProgressBar>
</LabeledList.Item>
<LabeledList.Item label="Pressure Regulator">
<Button
icon="undo"
disabled={data.releasePressure === data.defaultReleasePressure}
content="Reset"
onClick={() => act('pressure', {
pressure: 'reset',
<Flex mx={-1}>
<Flex.Item
mx={1}
align="center"
textAlign="center">
<Knob
size={2}
color={!!valveOpen && 'yellow'}
value={releasePressure}
unit="kPa"
minValue={minReleasePressure}
maxValue={maxReleasePressure}
step={5}
stepPixelSize={2}
onDrag={(e, value) => act('pressure', {
pressure: value,
})} />
</Flex.Item>
<Flex.Item
mx={1}
my={-0.5}
align="center"
textAlign="center">
<Box my={0.5} color="label">
Valve
</Box>
<Box my={0.5} width="60px">
<AnimatedNumber value={releasePressure} /> kPa
</Box>
<Button
icon="minus"
disabled={data.releasePressure <= data.minReleasePressure}
content="Min"
onClick={() => act('pressure', {
pressure: 'min',
})} />
<Button
icon="pencil-alt"
content="Set"
onClick={() => act('pressure', {
pressure: 'input',
})} />
<Button
icon="plus"
disabled={data.releasePressure >= data.maxReleasePressure}
content="Max"
onClick={() => act('pressure', {
pressure: 'max',
})} />
</LabeledList.Item>
<LabeledList.Item label="Valve">
<Button
icon={data.valveOpen ? 'unlock' : 'lock'}
color={data.valveOpen
? (data.hasHoldingTank ? 'caution' : 'danger')
my={0.5}
color={valveOpen
? (hasHoldingTank ? 'caution' : 'danger')
: null}
content={data.valveOpen ? 'Open' : 'Closed'}
content={valveOpen ? 'Open' : 'Closed'}
onClick={() => act('valve')} />
</LabeledList.Item>
</LabeledList>
</Flex.Item>
<Flex.Item
mx={1}
grow={1}
basis={0}>
<LabeledList>
<LabeledList.Item label="Pressure">
<AnimatedNumber value={tankPressure} /> kPa
</LabeledList.Item>
<LabeledList.Item
label="Port"
color={portConnected ? 'good' : 'average'}>
{portConnected ? 'Connected' : 'Not Connected'}
</LabeledList.Item>
{!!isPrototype && (
<LabeledList.Item label="Access">
<Button
icon={restricted ? 'lock' : 'unlock'}
color="caution"
content={restricted
? 'Engineering'
: 'Public'}
onClick={() => act('restricted')} />
</LabeledList.Item>
)}
</LabeledList>
</Flex.Item>
</Flex>
</Section>
<Section
title="Holding Tank"
buttons={!!data.hasHoldingTank && (
buttons={!!hasHoldingTank && (
<Button
icon="eject"
color={data.valveOpen && 'danger'}
color={valveOpen && 'danger'}
content="Eject"
onClick={() => act('eject')} />
)}>
{!!data.hasHoldingTank && (
{!!hasHoldingTank && (
<LabeledList>
<LabeledList.Item label="Label">
{data.holdingTank.name}
{holdingTank.name}
</LabeledList.Item>
<LabeledList.Item label="Pressure">
<AnimatedNumber value={data.holdingTank.tankPressure} /> kPa
<AnimatedNumber value={holdingTank.tankPressure} /> kPa
</LabeledList.Item>
</LabeledList>
)}
{!data.hasHoldingTank && (
{!hasHoldingTank && (
<Box color="average">
No Holding Tank
</Box>
+10 -11
View File
@@ -1,9 +1,6 @@
import { Fragment } from 'inferno';
import { Component, createRef } from 'inferno';
import { useBackend } from '../backend';
import { Box, Button } from '../components';
import { Component, createRef } from 'inferno';
import { pureComponentHooks } from 'common/react';
class PaintCanvas extends Component {
constructor(props) {
@@ -45,8 +42,7 @@ class PaintCanvas extends Component {
clickwrapper(event) {
const x_size = this.props.value.length;
if (!x_size)
{
if (!x_size) {
return;
}
const y_size = this.props.value[0].length;
@@ -78,6 +74,7 @@ class PaintCanvas extends Component {
);
}
}
export const Canvas = props => {
const { act, data } = useBackend(props);
return (
@@ -86,11 +83,13 @@ export const Canvas = props => {
value={data.grid}
onCanvasClick={(x, y) => act("paint", { x, y })} />
<Box>
{!data.finalized
&& <Button.Confirm
onClick={() => act("finalize")}
content="Finalize" />}
{!data.finalized && (
<Button.Confirm
onClick={() => act("finalize")}
content="Finalize" />
)}
{data.name}
</Box>
</Box>);
</Box>
);
};
+5 -2
View File
@@ -60,7 +60,9 @@ export const Cargo = props => {
disabled={!(data.away && data.docked)}
onClick={() => act(ref, 'loan')} />
) : (
<Box color="bad">Loaned to Centcom</Box>
<Box color="bad">
Loaned to Centcom
</Box>
)}
</LabeledList.Item>
) : ''}
@@ -158,7 +160,8 @@ const Catalog = props => {
)}
</td>
<td className="LabeledList__cell LabeledList__buttons">
<Button fluid
<Button
fluid
content={(data.self_paid
? Math.round(pack.cost * 1.1)
: pack.cost) + ' credits'}
+11 -6
View File
@@ -37,8 +37,9 @@ export const ChemDispenser = props => {
<LabeledList>
<LabeledList.Item label="Energy">
<ProgressBar
value={data.energy / data.maxEnergy}
content={toFixed(data.energy) + ' units'} />
value={data.energy / data.maxEnergy}>
{toFixed(data.energy) + ' units'}
</ProgressBar>
</LabeledList.Item>
</LabeledList>
</Section>
@@ -79,7 +80,8 @@ export const ChemDispenser = props => {
)}>
<Box mr={-1}>
{recipes.map(recipe => (
<Button key={recipe.name}
<Button
key={recipe.name}
icon="tint"
width="129.5px"
lineHeight="21px"
@@ -99,7 +101,8 @@ export const ChemDispenser = props => {
title="Dispense"
buttons={(
beakerTransferAmounts.map(amount => (
<Button key={amount}
<Button
key={amount}
icon="plus"
selected={amount === data.amount}
content={amount}
@@ -110,7 +113,8 @@ export const ChemDispenser = props => {
)}>
<Box mr={-1}>
{data.chemicals.map(chemical => (
<Button key={chemical.id}
<Button
key={chemical.id}
icon="tint"
width="129.5px"
lineHeight="21px"
@@ -125,7 +129,8 @@ export const ChemDispenser = props => {
title="Beaker"
buttons={(
beakerTransferAmounts.map(amount => (
<Button key={amount}
<Button
key={amount}
icon="minus"
disabled={recording}
content={amount}
+5 -4
View File
@@ -191,12 +191,13 @@ const PackagingControlsItem = props => {
minValue={1}
maxValue={10}
onChange={onChangeAmount} />
<Button ml={1}
<Button
ml={1}
content="Create"
onClick={onCreate} />
<Box inline ml={1}
color="label"
content={sideNote} />
<Box inline ml={1} color="label">
{sideNote}
</Box>
</LabeledList.Item>
);
};
+16 -17
View File
@@ -27,15 +27,16 @@ export const Cryo = props => {
<Fragment>
<Section title="Occupant">
<LabeledList>
<LabeledList.Item
label="Occupant"
content={data.occupant.name ? data.occupant.name : "No Occupant"} />
<LabeledList.Item label="Occupant">
{data.occupant.name || 'No Occupant'}
</LabeledList.Item>
{!!data.hasOccupant && (
<Fragment>
<LabeledList.Item
label="State"
content={data.occupant.stat}
color={data.occupant.statstate} />
color={data.occupant.statstate}>
{data.occupant.stat}
</LabeledList.Item>
<LabeledList.Item
label="Temperature"
color={data.occupant.temperaturestatus}>
@@ -44,7 +45,7 @@ export const Cryo = props => {
<LabeledList.Item label="Health">
<ProgressBar
value={data.occupant.health / data.occupant.maxHealth}
color={(data.occupant.health > 0) ? "good" : "average"}>
color={data.occupant.health > 0 ? 'good' : 'average'}>
<AnimatedNumber value={data.occupant.health} />
</ProgressBar>
</LabeledList.Item>
@@ -64,17 +65,15 @@ export const Cryo = props => {
</Section>
<Section title="Cell">
<LabeledList>
<LabeledList.Item
label="Power"
content={(
<Button
icon={data.isOperating ? "power-off" : "times"}
disabled={data.isOpen}
onClick={() => act('power')}
color={data.isOperating && ("green")}>
{data.isOperating ? "On" : "Off"}
</Button>
)} />
<LabeledList.Item label="Power">
<Button
icon={data.isOperating ? "power-off" : "times"}
disabled={data.isOpen}
onClick={() => act('power')}
color={data.isOperating && 'green'}>
{data.isOperating ? "On" : "Off"}
</Button>
</LabeledList.Item>
<LabeledList.Item label="Temperature">
<AnimatedNumber value={data.cellTemperature} /> K
</LabeledList.Item>
+24 -26
View File
@@ -1,6 +1,6 @@
import { Fragment } from 'inferno';
import { useBackend } from '../backend';
import { AnimatedNumber, Box, Button, LabeledList, ProgressBar, Section, Tabs } from '../components';
import { Button, Section } from '../components';
export const DecalPainter = props => {
const { act, data } = useBackend(props);
@@ -10,17 +10,15 @@ export const DecalPainter = props => {
return (
<Fragment>
<Section title="Decal Type">
{decal_list.map(decal => {
return (
<Button
key={decal.decal}
content={decal.name}
selected={decal.decal === data.decal_style}
onClick={() => act('select decal', {
decals: decal.decal,
})} />
);
})}
{decal_list.map(decal => (
<Button
key={decal.decal}
content={decal.name}
selected={decal.decal === data.decal_style}
onClick={() => act('select decal', {
decals: decal.decal,
})} />
))}
</Section>
<Section title="Decal Color">
{color_list.map(color => {
@@ -28,35 +26,35 @@ export const DecalPainter = props => {
<Button
key={color.colors}
content={color.colors === "red"
? "Red"
: color.colors === "white"
? "White"
? "Red"
: color.colors === "white"
? "White"
: "Yellow"}
selected={color.colors === data.decal_color}
onClick={() => act('select color', {
colors: color.colors,
colors: color.colors,
})} />
);
})}
})}
</Section>
<Section title="Decal Direction">
{dir_list.map(dir => {
{dir_list.map(dir => {
return (
<Button
key={dir.dirs}
content={dir.dirs === 1
? "North"
: dir.dirs === 2
? "South"
: dir.dirs === 4
? "East"
content={dir.dirs === 1
? "North"
: dir.dirs === 2
? "South"
: dir.dirs === 4
? "East"
: "West"}
selected={dir.dirs === data.decal_direction}
onClick={() => act('selected direction', {
dirs: dir.dirs,
dirs: dir.dirs,
})} />
);
})}
})}
</Section>
</Fragment>
);
+9 -6
View File
@@ -22,18 +22,21 @@ export const DnaVault = props => {
<LabeledList>
<LabeledList.Item label="Human DNA">
<ProgressBar
value={dna / dna_max}
content={dna + ' / ' + dna_max + ' Samples'} />
value={dna / dna_max}>
{dna + ' / ' + dna_max + ' Samples'}
</ProgressBar>
</LabeledList.Item>
<LabeledList.Item label="Plant DNA">
<ProgressBar
value={plants / plants_max}
content={plants + ' / ' + plants_max + ' Samples'} />
value={plants / plants_max}>
{plants + ' / ' + plants_max + ' Samples'}
</ProgressBar>
</LabeledList.Item>
<LabeledList.Item label="Animal DNA">
<ProgressBar
value={animals / animals}
content={animals + ' / ' + animals_max + ' Samples'} />
value={animals / animals}>
{animals + ' / ' + animals_max + ' Samples'}
</ProgressBar>
</LabeledList.Item>
</LabeledList>
</Section>
+37 -24
View File
@@ -22,8 +22,7 @@ export const Gateway = props => {
);
}
if (current_target)
{
if (current_target) {
return (
<Section title={current_target.name} textAlign="center">
<Icon name="rainbow" size={4} color="green" />
@@ -37,12 +36,15 @@ export const Gateway = props => {
}
if (!destinations.length) {
return (<Section>No gateway nodes detected.</Section>);
return (
<Section>
No gateway nodes detected.
</Section>
);
}
const GatewayDest = dest => {
if (dest.availible)
{
const renderGatewayDest = dest => {
if (dest.availible) {
return (
<Section
key={dest.ref}
@@ -50,29 +52,40 @@ export const Gateway = props => {
textAlign="center">
<Button
fluid
onClick={() => act("activate", { "destination": dest.ref })}>
onClick={() => act('activate', {
destination: dest.ref,
})}>
Activate
</Button>
</Section>);
}
else
{
return (
<Section
textAlign="center"
key={dest.ref}
title={dest.name}>
<Box m={1} textColor="bad">{dest.reason}</Box>
{!!dest.timeout && (<ProgressBar
value={dest.timeout}
content="Calibrating..." />)}
</Section>);
</Section>
);
}
return (
<Section
textAlign="center"
key={dest.ref}
title={dest.name}>
<Box m={1} textColor="bad">
{dest.reason}
</Box>
{!!dest.timeout && (
<ProgressBar
value={dest.timeout}>
Calibrating...
</ProgressBar>
)}
</Section>
);
};
return (
<Fragment>
{!gateway_status && (<NoticeBox>Gateway Unpowered</NoticeBox>)}
{destinations.map(GatewayDest)}
</Fragment>);
{!gateway_status && (
<NoticeBox>
Gateway Unpowered
</NoticeBox>
)}
{destinations.map(renderGatewayDest)}
</Fragment>
);
};
+112 -26
View File
@@ -1,6 +1,7 @@
import { Component } from 'inferno';
import { useBackend } from '../backend';
import { BlockQuote, Box, Button, ByondUi, Collapsible, Input, LabeledList, NumberInput, ProgressBar, Section, Tabs, Tooltip } from '../components';
import { BlockQuote, Box, Button, ByondUi, Collapsible, Input, LabeledList, NumberInput, ProgressBar, Section, Tabs, Tooltip, Slider, Icon, Knob } from '../components';
import { DraggableControl } from '../components/DraggableControl';
const COLORS_ARBITRARY = [
'red',
@@ -47,7 +48,7 @@ const PAGES = [
component: () => KitchenSinkTooltip,
},
{
title: 'Input',
title: 'Input / Slider',
component: () => KitchenSinkInput,
},
{
@@ -123,8 +124,9 @@ const KitchenSinkButton = props => {
<Box inline
mx="7px"
key={color}
color={color}
content={color} />
color={color}>
{color}
</Box>
))}
</Box>
</Box>
@@ -134,14 +136,30 @@ const KitchenSinkButton = props => {
const KitchenSinkBox = props => {
return (
<Box>
<Box bold content="bold" />
<Box italic content="italic" />
<Box opacity={0.5} content="opacity 0.5" />
<Box opacity={0.25} content="opacity 0.25" />
<Box m={2} content="m: 2" />
<Box textAlign="left" content="left" />
<Box textAlign="center" content="center" />
<Box textAlign="right" content="right" />
<Box bold>
bold
</Box>
<Box italic>
italic
</Box>
<Box opacity={0.5}>
opacity 0.5
</Box>
<Box opacity={0.25}>
opacity 0.25
</Box>
<Box m={2}>
m: 2
</Box>
<Box textAlign="left">
left
</Box>
<Box textAlign="center">
center
</Box>
<Box textAlign="right">
right
</Box>
</Box>
);
};
@@ -166,8 +184,9 @@ class KitchenSinkProgressBar extends Component {
}}
minValue={-1}
maxValue={1}
value={progress}
content={`value: ${Number(progress).toFixed(1)}`} />
value={progress}>
Value: {Number(progress).toFixed(1)}
</ProgressBar>
<Box mt={1}>
<Button
content="-0.1"
@@ -199,7 +218,8 @@ class KitchenSinkTabs extends Component {
return (
<Box>
{'Vertical: '}
<Button inline
<Button
inline
content={String(vertical)}
onClick={() => this.setState(prevState => ({
vertical: !prevState.vertical,
@@ -273,7 +293,21 @@ class KitchenSinkInput extends Component {
return (
<Box>
<LabeledList>
<LabeledList.Item label="NumberInput">
<LabeledList.Item label="Input (onChange)">
<Input
value={text}
onChange={(e, value) => this.setState({
text: value,
})} />
</LabeledList.Item>
<LabeledList.Item label="Input (onInput)">
<Input
value={text}
onInput={(e, value) => this.setState({
text: value,
})} />
</LabeledList.Item>
<LabeledList.Item label="NumberInput (onChange)">
<NumberInput
animated
width={10}
@@ -285,6 +319,8 @@ class KitchenSinkInput extends Component {
onChange={(e, value) => this.setState({
number: value,
})} />
</LabeledList.Item>
<LabeledList.Item label="NumberInput (onDrag)">
<NumberInput
animated
width={10}
@@ -297,17 +333,67 @@ class KitchenSinkInput extends Component {
number: value,
})} />
</LabeledList.Item>
<LabeledList.Item label="Input">
<Input
value={text}
onChange={(e, value) => this.setState({
text: value,
<LabeledList.Item label="Slider (onDrag)">
<Slider
step={1}
stepPixelSize={5}
value={number}
minValue={-100}
maxValue={100}
onDrag={(e, value) => this.setState({
number: value,
})} />
<Input
value={text}
onInput={(e, value) => this.setState({
text: value,
</LabeledList.Item>
<LabeledList.Item label="Knob (onDrag)">
<Knob
inline
size={1}
step={1}
stepPixelSize={2}
value={number}
minValue={-100}
maxValue={100}
onDrag={(e, value) => this.setState({
number: value,
})} />
<Knob
ml={1}
inline
bipolar
size={1}
step={1}
stepPixelSize={2}
value={number}
minValue={-100}
maxValue={100}
onDrag={(e, value) => this.setState({
number: value,
})} />
</LabeledList.Item>
<LabeledList.Item label="Rotating Icon">
<Box inline position="relative">
<DraggableControl
value={number}
minValue={-100}
maxValue={100}
dragMatrix={[0, -1]}
step={1}
stepPixelSize={5}
onDrag={(e, value) => this.setState({
number: value,
})}>
{control => (
<Box onMouseDown={control.handleDragStart}>
<Icon
size={4}
color="yellow"
name="times"
rotation={control.displayValue * 4} />
{control.inputElement}
</Box>
)}
</DraggableControl>
</Box>
</LabeledList.Item>
</LabeledList>
</Box>
@@ -338,7 +424,7 @@ const BoxOfSampleText = props => {
<Box mt={1} bold>
The wide electrification of the southern
provinces will give a powerful impetus to the
growth of soviet agriculture.
growth of agriculture.
</Box>
</Box>
);
@@ -57,19 +57,17 @@ export const OperatingComputer = props => {
value={patient.health}
minValue={patient.minHealth}
maxValue={patient.maxHealth}
color={patient.health >= 0 ? 'good' : 'average'}
content={(
<AnimatedNumber value={patient.health} />
)} />
color={patient.health >= 0 ? 'good' : 'average'}>
<AnimatedNumber value={patient.health} />
</ProgressBar>
</LabeledList.Item>
{damageTypes.map(type => (
<LabeledList.Item key={type.type} label={type.label}>
<ProgressBar
value={patient[type.type] / patient.maxHealth}
color="bad"
content={(
<AnimatedNumber value={patient[type.type]} />
)} />
color="bad">
<AnimatedNumber value={patient[type.type]} />
</ProgressBar>
</LabeledList.Item>
))}
</LabeledList>
@@ -57,16 +57,18 @@ export class PowerMonitor extends Component {
value={supply}
minValue={0}
maxValue={maxValue}
color="teal"
content={toFixed(supply / 1000) + ' kW'} />
color="teal">
{toFixed(supply / 1000) + ' kW'}
</ProgressBar>
</LabeledList.Item>
<LabeledList.Item label="Draw">
<ProgressBar
value={demand}
minValue={0}
maxValue={maxValue}
color="pink"
content={toFixed(demand / 1000) + ' kW'} />
color="pink">
{toFixed(demand / 1000) + ' kW'}
</ProgressBar>
</LabeledList.Item>
</LabeledList>
</Section>
@@ -93,10 +93,12 @@ export const RapidPipeDispenser = props => {
</LabeledList.Item>
<LabeledList.Item
label="Color">
<Box inline
<Box
inline
width="64px"
color={PAINT_COLORS[selected_color]}
content={selected_color} />
color={PAINT_COLORS[selected_color]}>
{selected_color}
</Box>
{Object.keys(PAINT_COLORS)
.map(colorName => (
<ColorBox
+129 -119
View File
@@ -1,36 +1,43 @@
import { Fragment } from 'inferno';
import { useBackend } from '../backend';
import { Box, Button, NumberInput, LabeledList, ProgressBar, Section } from '../components';
import { Box, Button, Flex, LabeledList, ProgressBar, Section, Slider } from '../components';
import { formatPower } from '../format';
// Common power multiplier
const POWER_MUL = 1e3;
export const Smes = props => {
const { act, data } = useBackend(props);
let inputState;
if (data.capacityPercent >= 100) {
inputState = 'good';
}
else if (data.inputting) {
inputState = 'average';
}
else {
inputState = 'bad';
}
let outputState;
if (data.outputting) {
outputState = 'good';
}
else if (data.charge > 0) {
outputState = 'average';
}
else {
outputState = 'bad';
}
const {
capacityPercent,
capacity,
charge,
inputAttempt,
inputting,
inputLevel,
inputLevelMax,
inputAvailable,
outputAttempt,
outputting,
outputLevel,
outputLevelMax,
outputUsed,
} = data;
const inputState = (
capacityPercent >= 100 && 'good'
|| inputting && 'average'
|| 'bad'
);
const outputState = (
outputting && 'good'
|| charge > 0 && 'average'
|| 'bad'
);
return (
<Fragment>
<Section title="Stored Energy">
<ProgressBar
value={data.capacityPercent * 0.01}
value={capacityPercent * 0.01}
ranges={{
good: [0.5, Infinity],
average: [0.15, 0.5],
@@ -43,64 +50,65 @@ export const Smes = props => {
label="Charge Mode"
buttons={
<Button
icon={data.inputAttempt ? 'sync-alt' : 'times'}
selected={data.inputAttempt}
icon={inputAttempt ? 'sync-alt' : 'times'}
selected={inputAttempt}
onClick={() => act('tryinput')}>
{data.inputAttempt ? 'Auto' : 'Off'}
{inputAttempt ? 'Auto' : 'Off'}
</Button>
}>
<Box color={inputState}>
{data.capacityPercent >= 100
? 'Fully Charged'
: data.inputting
? 'Charging'
: 'Not Charging'}
{capacityPercent >= 100 && 'Fully Charged'
|| inputting && 'Charging'
|| 'Not Charging'}
</Box>
</LabeledList.Item>
<LabeledList.Item label="Target Input">
<ProgressBar
value={data.inputLevel/data.inputLevelMax}
content={data.inputLevel_text} />
</LabeledList.Item>
<LabeledList.Item label="Adjust Input">
<Button
icon="fast-backward"
disabled={data.inputLevel === 0}
onClick={() => act('input', {
target: 'min',
})} />
<Button
icon="backward"
disabled={data.inputLevel === 0}
onClick={() => act('input', {
adjust: -10000,
})} />
<NumberInput
value={Math.round(data.inputLevel/1000)}
unit="kW"
width="65px"
minValue={0}
maxValue={data.inputLevelMax/1000}
onChange={(e, value) => {
return act('input', {
target: value*1000,
});
}} />
<Button
icon="forward"
disabled={data.inputLevel === data.inputLevelMax}
onClick={() => act('input', {
adjust: 10000,
})} />
<Button
icon="fast-forward"
disabled={data.inputLevel === data.inputLevelMax}
onClick={() => act('input', {
target: 'max',
})} />
<Flex inline width="100%">
<Flex.Item>
<Button
icon="fast-backward"
disabled={inputLevel === 0}
onClick={() => act('input', {
target: 'min',
})} />
<Button
icon="backward"
disabled={inputLevel === 0}
onClick={() => act('input', {
adjust: -10000,
})} />
</Flex.Item>
<Flex.Item grow={1} mx={1}>
<Slider
value={inputLevel / POWER_MUL}
fillValue={inputAvailable / POWER_MUL}
minValue={0}
maxValue={inputLevelMax / POWER_MUL}
step={5}
stepPixelSize={4}
format={value => formatPower(value * POWER_MUL, 1)}
onDrag={(e, value) => act('input', {
target: value * POWER_MUL,
})} />
</Flex.Item>
<Flex.Item>
<Button
icon="forward"
disabled={inputLevel === inputLevelMax}
onClick={() => act('input', {
adjust: 10000,
})} />
<Button
icon="fast-forward"
disabled={inputLevel === inputLevelMax}
onClick={() => act('input', {
target: 'max',
})} />
</Flex.Item>
</Flex>
</LabeledList.Item>
<LabeledList.Item label="Available">
{data.inputAvailable}
{formatPower(inputAvailable)}
</LabeledList.Item>
</LabeledList>
</Section>
@@ -110,64 +118,66 @@ export const Smes = props => {
label="Output Mode"
buttons={
<Button
icon={data.outputAttempt ? 'power-off' : 'times'}
selected={data.outputAttempt}
icon={outputAttempt ? 'power-off' : 'times'}
selected={outputAttempt}
onClick={() => act('tryoutput')}>
{data.outputAttempt ? 'On' : 'Off'}
{outputAttempt ? 'On' : 'Off'}
</Button>
}>
<Box color={outputState}>
{data.outputting
{outputting
? 'Sending'
: data.charge > 0
: charge > 0
? 'Not Sending'
: 'No Charge'}
</Box>
</LabeledList.Item>
<LabeledList.Item label="Target Output">
<ProgressBar
value={data.outputLevel/data.outputLevelMax}
content={data.outputLevel_text} />
</LabeledList.Item>
<LabeledList.Item label="Adjust Output">
<Button
icon="fast-backward"
disabled={data.outputLevel === 0}
onClick={() => act('output', {
target: 'min',
})} />
<Button
icon="backward"
disabled={data.outputLevel === 0}
onClick={() => act('output', {
adjust: -10000,
})} />
<NumberInput
value={Math.round(data.outputLevel/1000)}
unit="kW"
width="65px"
minValue={0}
maxValue={data.outputLevelMax/1000}
onChange={(e, value) => {
return act('output', {
target: value*1000,
});
}} />
<Button
icon="forward"
disabled={data.outputLevel === data.outputLevelMax}
onClick={() => act('output', {
adjust: 10000,
})} />
<Button
icon="fast-forward"
disabled={data.outputLevel === data.outputLevelMax}
onClick={() => act('output', {
target: 'max',
})} />
<Flex inline width="100%">
<Flex.Item>
<Button
icon="fast-backward"
disabled={outputLevel === 0}
onClick={() => act('output', {
target: 'min',
})} />
<Button
icon="backward"
disabled={outputLevel === 0}
onClick={() => act('output', {
adjust: -10000,
})} />
</Flex.Item>
<Flex.Item grow={1} mx={1}>
<Slider
value={outputLevel / POWER_MUL}
minValue={0}
maxValue={outputLevelMax / POWER_MUL}
step={5}
stepPixelSize={4}
format={value => formatPower(value * POWER_MUL, 1)}
onDrag={(e, value) => act('output', {
target: value * POWER_MUL,
})} />
</Flex.Item>
<Flex.Item>
<Button
icon="forward"
disabled={outputLevel === outputLevelMax}
onClick={() => act('output', {
adjust: 10000,
})} />
<Button
icon="fast-forward"
disabled={outputLevel === outputLevelMax}
onClick={() => act('output', {
target: 'max',
})} />
</Flex.Item>
</Flex>
</LabeledList.Item>
<LabeledList.Item label="Outputting">
{data.outputUsed}
{formatPower(outputUsed)}
</LabeledList.Item>
</LabeledList>
</Section>
@@ -35,13 +35,14 @@ export const SmokeMachine = props => {
<Box mt={1}>
<LabeledList>
<LabeledList.Item label="Range">
{ [1, 2, 3, 4, 5].map(amount => (
<Button key={amount}
{[1, 2, 3, 4, 5].map(amount => (
<Button
key={amount}
selected={setting === amount}
icon="plus"
content={amount * 3}
disabled={maxSetting < amount}
onClick={() => act('setting', { amount: amount })} />
onClick={() => act('setting', { amount })} />
))}
</LabeledList.Item>
</LabeledList>
@@ -51,8 +51,9 @@ export const SolarControl = props => {
}}
minValue={0}
maxValue={1}
value={generated_ratio}
content={generated + ' W'} />
value={generated_ratio}>
{generated + ' W'}
</ProgressBar>
</LabeledList.Item>
</LabeledList>
</Grid.Column>
+3 -2
View File
@@ -30,12 +30,13 @@ export const SpaceHeater = props => {
{data.hasPowercell && (
<ProgressBar
value={data.powerLevel / 100}
content={data.powerLevel + '%'}
ranges={{
good: [0.6, Infinity],
average: [0.3, 0.6],
bad: [-Infinity, 0.3],
}} />
}}>
{data.powerLevel + '%'}
</ProgressBar>
) || 'None'}
</LabeledList.Item>
</LabeledList>
+3 -2
View File
@@ -9,12 +9,13 @@ export const Tank = props => {
<LabeledList.Item label="Pressure">
<ProgressBar
value={data.tankPressure / 1013}
content={data.tankPressure + ' kPa'}
ranges={{
good: [0.35, Infinity],
average: [0.15, 0.35],
bad: [-Infinity, 0.15],
}} />
}}>
{data.tankPressure + ' kPa'}
</ProgressBar>
</LabeledList.Item>
<LabeledList.Item label="Pressure Regulator">
<Button
+2 -2
View File
@@ -36,7 +36,7 @@ export const Teleporter = props => {
buttons={(
<Button
icon="tools"
content={"Set Target"}
content="Set Target"
onClick={() => act('settarget')} />
)}>
{target}
@@ -45,7 +45,7 @@ export const Teleporter = props => {
buttons={(
<Button
icon="tools"
content={"Calibrate Hub"}
content="Calibrate Hub"
onClick={() => act('calibrate')} />
)}>
{calibrating && (
@@ -18,13 +18,14 @@ export const VaultController = props => {
<LabeledList.Item label="Charge">
<ProgressBar
value={data.stored / data.max}
content={toFixed(data.stored/1000)
+ ' / ' + toFixed(data.max/1000) + ' kW'}
ranges={{
good: [1, Infinity],
average: [0.30, 1],
bad: [-Infinity, 0.30],
}} />
}}>
{toFixed(data.stored / 1000) + ' / '
+ toFixed(data.max / 1000) + ' kW'}
</ProgressBar>
</LabeledList.Item>
</LabeledList>
</Section>
+25 -22
View File
@@ -1,12 +1,10 @@
import { Fragment } from 'inferno';
import { act } from '../byond';
import { Section, Box, Button, Table } from '../components';
import { classes } from 'common/react';
import { Fragment } from 'inferno';
import { useBackend } from '../backend';
import { Box, Button, Section, Table } from '../components';
export const Vending = props => {
const { state } = props;
const { config, data } = state;
const { ref } = config;
const { act, data } = useBackend(props);
let inventory;
let custom = false;
if (data.vending_machine_input) {
@@ -59,7 +57,7 @@ export const Vending = props => {
);
return (
<Table.Row key={product.name}>
<Table.Cell>
<Table.Cell collapsing>
{product.base64 ? (
<img
src={`data:image/jpeg;base64,${product.img}`}
@@ -69,30 +67,36 @@ export const Vending = props => {
}} />
) : (
<span
className={classes(['vending32x32', product.path])}
className={classes([
'vending32x32',
product.path,
])}
style={{
'vertical-align': 'middle',
'horizontal-align': 'middle',
}} />
)}
<b>{product.name}</b>
</Table.Cell>
<Table.Cell>
<Box color={custom
? 'good'
: data.stock[product.name] <= 0
? 'bad'
: data.stock[product.name] <= (product.max_amount / 2)
? 'average'
: 'good'}>
<Table.Cell bold>
{product.name}
</Table.Cell>
<Table.Cell collapsing textAlign="center">
<Box
color={custom
? 'good'
: data.stock[product.name] <= 0
? 'bad'
: data.stock[product.name] <= (product.max_amount / 2)
? 'average'
: 'good'}>
{data.stock[product.name]} in stock
</Box>
</Table.Cell>
<Table.Cell>
<Table.Cell collapsing textAlign="center">
{custom && (
<Button
content={data.access ? 'FREE' : product.price + ' cr'}
onClick={() => act(ref, 'dispense', {
onClick={() => act('dispense', {
'item': product.name,
})} />
) || (
@@ -100,15 +104,14 @@ export const Vending = props => {
disabled={(
data.stock[product.namename] === 0
|| (
!free
&& (
!free && (
!data.user
|| product.price > data.user.cash
)
)
)}
content={free ? 'FREE' : product.price + ' cr'}
onClick={() => act(ref, 'vend', {
onClick={() => act('vend', {
'ref': product.ref,
})} />
)}
+6 -2
View File
@@ -36,7 +36,7 @@ export class Layout extends Component {
render() {
const { props } = this;
const { state, dispatch } = props;
const { config } = state;
const { config, debugLayout } = state;
const route = getRoute(state);
const { scrollable, resizable, theme } = route || {};
let contentElement;
@@ -86,7 +86,11 @@ export class Layout extends Component {
winset(config.window, 'is-visible', false);
runCommand(`uiclose ${config.ref}`);
}} />
<div className="Layout__rest">
<div
className={classes([
'Layout__rest',
debugLayout && 'debug-layout',
])}>
{contentElement}
{showDimmer && (
<div className="Layout__dimmer" />
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,12 @@
.debug-layout,
.debug-layout *:not(g):not(path) {
color: rgba(255, 255, 255, 0.9) !important;
background: transparent !important;
outline: 1px solid rgba(255, 255, 255, 0.5) !important;
box-shadow: none !important;
filter: none !important;
&:hover {
outline-color: rgba(255, 255, 255, 0.8) !important;
}
}
@@ -3,6 +3,10 @@
display: flex;
}
.Flex--inline {
display: inline-flex;
}
@for $i from 1 through 2 {
.Flex--spacing--#{$i} {
margin: 0 -3px * $i;
@@ -0,0 +1,159 @@
@use '../base.scss';
@use '../colors.scss';
@use '../functions.scss' as *;
$bg-map: colors.$bg-map !default;
$fg-map: colors.$fg-map !default;
$ring-color: #6a96c9 !default;
$knob-color: #333333 !default;
$inner-padding: 0.1em;
.Knob {
position: relative;
font-size: 1rem;
width: 2.6em;
height: 2.6em;
margin: 0 auto;
margin-bottom: -0.2em;
cursor: n-resize;
// Adjusts a baseline in a way, that makes knob middle-aligned
// when it flows with the text.
&:after {
content: '.';
color: transparent;
line-height: 2.5em;
}
}
.Knob__circle {
position: absolute;
top: $inner-padding;
bottom: $inner-padding;
left: $inner-padding;
right: $inner-padding;
margin: 0.3em;
background-color: $knob-color;
background-image: linear-gradient(to bottom,
rgba(255, 255, 255, 0.15) 0%,
rgba(255, 255, 255, 0) 100%);
border-radius: 50%;
box-shadow: 0 0.05em 0.5em 0 rgba(0, 0, 0, 0.5);
}
.Knob__cursorBox {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.Knob__cursor {
position: relative;
top: 0.05em;
margin: 0 auto;
width: 0.2em;
height: 0.8em;
background-color: rgba(255, 255, 255, 0.9);
}
.Knob__popupValue {
position: absolute;
top: -2rem;
right: 50%;
font-size: 1rem;
text-align: center;
padding: 0.25rem 0.5rem;
background-color: #000;
transform: translateX(50%);
white-space: nowrap;
}
.Knob__ring {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
padding: $inner-padding;
}
$pi: 3.1416;
.Knob__ringTrackPivot {
transform: rotateZ(135deg);
}
.Knob__ringTrack {
// transform-origin: 50% 50%;
fill: transparent;
stroke: rgba(255, 255, 255, 0.1);
stroke-width: 8;
stroke-linecap: round;
stroke-dasharray: 75 * $pi;
}
.Knob__ringFillPivot {
transform: rotateZ(135deg);
}
.Knob--bipolar .Knob__ringFillPivot {
transform: rotateZ(270deg);
}
.Knob__ringFill {
fill: transparent;
stroke: $ring-color;
stroke-width: 8;
stroke-linecap: round;
stroke-dasharray: 100 * $pi;
transition: stroke 50ms;
}
@each $color-name, $color-value in $fg-map {
.Knob--color--#{$color-name} {
.Knob__ringFill {
stroke: $color-value;
}
}
}
// .Slider__cursorOffset {
// position: absolute;
// top: 0;
// left: 0;
// bottom: 0;
// transition: none !important;
// }
// .Slider__cursor {
// position: absolute;
// top: 0;
// right: -1px;
// bottom: 0;
// width: 0;
// border-left: 2px solid #fff;
// }
// .Slider__pointer {
// position: absolute;
// right: -5px;
// bottom: -4px;
// width: 0;
// height: 0;
// border-left: 5px solid transparent;
// border-right: 5px solid transparent;
// border-bottom: 5px solid #fff;
// }
// .Slider__popupValue {
// position: absolute;
// right: 0;
// top: -22px;
// padding: 2px 4px;
// background-color: #000;
// transform: translateX(50%);
// white-space: nowrap;
// }
@@ -22,6 +22,9 @@ $bg-map: colors.$bg-map !default;
top: 0;
left: 0;
bottom: 0;
}
.ProgressBar__fill--animated {
transition: background-color 500ms, width 500ms;
}
@@ -0,0 +1,41 @@
.Slider {
cursor: e-resize;
}
.Slider__cursorOffset {
position: absolute;
top: 0;
left: 0;
bottom: 0;
transition: none !important;
}
.Slider__cursor {
position: absolute;
top: 0;
right: -1px;
bottom: 0;
width: 0;
border-left: 2px solid #fff;
}
.Slider__pointer {
position: absolute;
right: -5px;
bottom: -4px;
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-bottom: 5px solid #fff;
}
.Slider__popupValue {
position: absolute;
right: 0;
top: -22px;
padding: 2px 4px;
background-color: #000;
transform: translateX(50%);
white-space: nowrap;
}
+4 -1
View File
@@ -7,11 +7,12 @@
// Atomic classes
@include meta.load-css('./atomic/candystripe.scss');
@include meta.load-css('./atomic/color.scss');
@include meta.load-css('./atomic/debug-layout.scss');
@include meta.load-css('./atomic/display.scss');
@include meta.load-css('./atomic/margin.scss');
@include meta.load-css('./atomic/outline.scss');
@include meta.load-css('./atomic/position.scss');
@include meta.load-css('./atomic/text.scss');
@include meta.load-css('./atomic/outline.scss');
// Components
@include meta.load-css('./components/BlockQuote.scss');
@@ -24,6 +25,7 @@
@include meta.load-css('./components/FatalError.scss');
@include meta.load-css('./components/Flex.scss');
@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/Layout.scss');
@include meta.load-css('./components/NoticeBox.scss');
@@ -34,6 +36,7 @@
@include meta.load-css('./components/ProgressBar.scss');
@include meta.load-css('./components/Roulette.scss');
@include meta.load-css('./components/Section.scss');
@include meta.load-css('./components/Slider.scss');
@include meta.load-css('./components/Table.scss');
@include meta.load-css('./components/Tabs.scss');
@include meta.load-css('./components/TitleBar.scss');
+1 -1
View File
@@ -2,6 +2,7 @@ html, body {
box-sizing: border-box;
height: 100%;
margin: 0;
font-size: 12px;
}
html {
@@ -12,7 +13,6 @@ html {
body {
overflow: auto;
font-family: Verdana, Geneva, sans-serif;
font-size: 12px;
}
*, *:before, *:after {