mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-22 20:48:56 +01:00
tgui-next: Radio, ORM, Solar Tracker (#47537)
* Radio interface * Ore redemption machine interface * Remove candystripe because it looks like shit * Solar tracker interface * Add more channels, add full color support to Box * Cleanup README * conversion guide draft 1 * Update documentation * Implement an Input component, update component reference
This commit is contained in:
committed by
Rob Bailey
parent
44b76a4112
commit
8cf0a9db2c
@@ -111,7 +111,14 @@
|
||||
. = ..()
|
||||
ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open)
|
||||
if(!ui)
|
||||
ui = new(user, src, ui_key, "radio", name, 370, 220 + channels.len * 22, master_ui, state)
|
||||
var/ui_width = 360
|
||||
var/ui_height = 106
|
||||
if(subspace_transmission)
|
||||
if (channels.len > 0)
|
||||
ui_height += 6 + channels.len * 21
|
||||
else
|
||||
ui_height += 24
|
||||
ui = new(user, src, ui_key, "radio", name, ui_width, ui_height, master_ui, state)
|
||||
ui.open()
|
||||
|
||||
/obj/item/radio/ui_data(mob/user)
|
||||
|
||||
@@ -237,6 +237,7 @@
|
||||
data["disconnected"] = "mineral withdrawal is on hold"
|
||||
|
||||
data["diskDesigns"] = list()
|
||||
data["hasDisk"] = FALSE
|
||||
if(inserted_disk)
|
||||
data["hasDisk"] = TRUE
|
||||
if(inserted_disk.blueprints.len)
|
||||
|
||||
+41
-35
@@ -12,8 +12,6 @@
|
||||
active_power_usage = 0
|
||||
max_integrity = 150
|
||||
integrity_failure = 0.33
|
||||
ui_x = 500
|
||||
ui_y = 400
|
||||
|
||||
var/id = 0
|
||||
var/obscured = 0
|
||||
@@ -347,7 +345,7 @@
|
||||
datum/tgui/master_ui = null, datum/ui_state/state = GLOB.default_state)
|
||||
ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open)
|
||||
if(!ui)
|
||||
ui = new(user, src, ui_key, "solar_control", name, ui_x, ui_y, master_ui, state)
|
||||
ui = new(user, src, ui_key, "solar_control", name, 380, 230, master_ui, state)
|
||||
ui.open()
|
||||
|
||||
/obj/machinery/power/solar_control/ui_data()
|
||||
@@ -368,39 +366,47 @@
|
||||
/obj/machinery/power/solar_control/ui_act(action, params)
|
||||
if(..())
|
||||
return
|
||||
switch(action)
|
||||
if("angle")
|
||||
var/adjust = text2num(params["adjust"])
|
||||
if(adjust)
|
||||
currentdir = CLAMP((360 + adjust + currentdir) % 360, 0, 359)
|
||||
targetdir = currentdir
|
||||
set_panels(currentdir)
|
||||
. = TRUE
|
||||
if("rate")
|
||||
var/adjust = text2num(params["adjust"])
|
||||
if(adjust)
|
||||
trackrate = CLAMP(trackrate + adjust, -7200, 7200)
|
||||
if(trackrate)
|
||||
nexttime = world.time + 36000 / abs(trackrate)
|
||||
. = TRUE
|
||||
if("tracking")
|
||||
var/mode = text2num(params["mode"])
|
||||
track = mode
|
||||
if(mode == 2 && connected_tracker)
|
||||
connected_tracker.set_angle(SSsun.angle)
|
||||
set_panels(currentdir)
|
||||
else if(mode == 1)
|
||||
targetdir = currentdir
|
||||
if(trackrate)
|
||||
nexttime = world.time + 36000 / abs(trackrate)
|
||||
set_panels(targetdir)
|
||||
. = TRUE
|
||||
if("refresh")
|
||||
search_for_connected()
|
||||
if(connected_tracker && track == 2)
|
||||
connected_tracker.set_angle(SSsun.angle)
|
||||
if(action == "angle")
|
||||
var/adjust = text2num(params["adjust"])
|
||||
var/value = text2num(params["value"])
|
||||
if(adjust)
|
||||
value = currentdir + adjust
|
||||
if(value != null)
|
||||
currentdir = CLAMP((360 + value) % 360, 0, 359)
|
||||
targetdir = currentdir
|
||||
set_panels(currentdir)
|
||||
. = TRUE
|
||||
return TRUE
|
||||
return FALSE
|
||||
if(action == "rate")
|
||||
var/adjust = text2num(params["adjust"])
|
||||
var/value = text2num(params["value"])
|
||||
if(adjust)
|
||||
value = trackrate + adjust
|
||||
if(value != null)
|
||||
trackrate = CLAMP(value, -7200, 7200)
|
||||
if(trackrate)
|
||||
nexttime = world.time + 36000 / abs(trackrate)
|
||||
return TRUE
|
||||
return FALSE
|
||||
if(action == "tracking")
|
||||
var/mode = text2num(params["mode"])
|
||||
track = mode
|
||||
if(mode == 2 && connected_tracker)
|
||||
connected_tracker.set_angle(SSsun.angle)
|
||||
set_panels(currentdir)
|
||||
else if(mode == 1)
|
||||
targetdir = currentdir
|
||||
if(trackrate)
|
||||
nexttime = world.time + 36000 / abs(trackrate)
|
||||
set_panels(targetdir)
|
||||
return TRUE
|
||||
if(action == "refresh")
|
||||
search_for_connected()
|
||||
if(connected_tracker && track == 2)
|
||||
connected_tracker.set_angle(SSsun.angle)
|
||||
set_panels(currentdir)
|
||||
return TRUE
|
||||
return FALSE
|
||||
|
||||
/obj/machinery/power/solar_control/attackby(obj/item/I, mob/user, params)
|
||||
if(I.tool_behaviour == TOOL_SCREWDRIVER)
|
||||
|
||||
@@ -234,7 +234,7 @@ rules:
|
||||
## Enforce boolean attributes notation in JSX (fixable)
|
||||
react/jsx-boolean-value: error
|
||||
## Enforce or disallow spaces inside of curly braces in JSX attributes and expressions.
|
||||
react/jsx-child-element-spacing: error
|
||||
# react/jsx-child-element-spacing: error
|
||||
## Validate closing bracket location in JSX (fixable)
|
||||
## NOTE: Too fucking annoying because all styles are viable
|
||||
# react/jsx-closing-bracket-location: [error, {
|
||||
|
||||
+210
-37
@@ -1,37 +1,38 @@
|
||||
# tgui-next
|
||||
# tgui
|
||||
|
||||
## Introduction
|
||||
|
||||
tgui is a robust user interface framework of /tg/station. It is rendered
|
||||
completely in the browser, based on JSON data sent from the server.
|
||||
This data flow is always unidirectional, and the only way to make changes
|
||||
to the game state is to dispatch actions which are processed on the server,
|
||||
in a similar method to native BYOND Topic(). Once the action is processed,
|
||||
an updated JSON is sent.
|
||||
tgui is a robust user interface framework of /tg/station.
|
||||
|
||||
tgui is very different from most UIs you will encounter in BYOND programming,
|
||||
and is heavily reliant of Javascript and web technologies as opposed to DM.
|
||||
However, if you are familiar with NanoUI (a library which can be found on almost
|
||||
tgui is very different from most UIs you will encounter in BYOND programming.
|
||||
It is heavily reliant on Javascript and web technologies as opposed to DM.
|
||||
If you are familiar with NanoUI (a library which can be found on almost
|
||||
every other SS13 codebase), tgui should be fairly easy to pick up.
|
||||
|
||||
tgui is a fork of an older tgui (based on Ractive), which is a fork of NanoUI.
|
||||
The server-side code (DM) is similar and derived from NanoUI, while the
|
||||
clientside is a wholly new project with no code in common.
|
||||
## Learn tgui
|
||||
|
||||
To get a clearer picture how to create a completely new interface from scratch,
|
||||
please refer to this [tutorial document](docs/tutorial-and-examples.md).
|
||||
If you don't know how tgui backend works, or have very little knowledge about
|
||||
both frontend and backend, or simply want a step by step instruction,
|
||||
we recommend you first read the document linked above.
|
||||
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
|
||||
|
||||
If you are completely new to frontend and prefer to **learn by doing**,
|
||||
start with our [practical tutorial](docs/tutorial-and-examples.md).
|
||||
|
||||
### Guides
|
||||
|
||||
This project uses **Inferno** - a very fast UI rendering engine with a similar
|
||||
API to React. If you are new to Inferno or React, take your time to read
|
||||
these documents:
|
||||
API to React. Take your time to read these guides:
|
||||
|
||||
- [React guide](https://reactjs.org/docs/hello-world.html)
|
||||
- [Inferno documentation](https://infernojs.org/docs/guides/components) -
|
||||
highlights differences with React.
|
||||
|
||||
If you were already familiar with an older, Ractive-based tgui, and want
|
||||
to translate concepts between old and new tgui, read this
|
||||
[interface conversion guide](docs/converting-old-tgui-interfaces.md).
|
||||
|
||||
## Pre-requisites
|
||||
|
||||
You will need these programs to start developing in tgui:
|
||||
@@ -51,12 +52,19 @@ You will need these programs to start developing in tgui:
|
||||
|
||||
**For MSys2, Git Bash, WSL, Linux or macOS users:**
|
||||
|
||||
First and foremost, run `bin/tgui --install-git-hooks` to install merge
|
||||
drivers which will help you resolve conflicts when rebasing your branches.
|
||||
First and foremost, change your directory to `tgui-next`.
|
||||
|
||||
Run `bin/tgui --install-git-hooks` (optional) to install merge drivers
|
||||
which will assist you in conflict resolution when rebasing your branches.
|
||||
|
||||
Run one of the following:
|
||||
|
||||
- `bin/tgui` - build the project in production mode.
|
||||
- `bin/tgui --dev` - launch a development server, with live log
|
||||
collection, byond cache reloading and hot module replacement.
|
||||
- `bin/tgui --dev` - launch a development server.
|
||||
- tgui development server provides you with incremental compilation,
|
||||
hot module replacement and logging facilities in all running instances
|
||||
of tgui. In short, this means that you will instantly see changes in the
|
||||
game as you code it. Very useful, highly recommended.
|
||||
- `bin/tgui --dev --reload` - reload byond cache once.
|
||||
- `bin/tgui --dev --debug` - run server with debug logging enabled.
|
||||
- `bin/tgui --dev --no-hot` - disable hot module replacement (helps when
|
||||
@@ -74,20 +82,19 @@ If you haven't opened the console already, you can do that by holding
|
||||
Shift and right clicking on the `tgui-next` folder, then pressing
|
||||
either `Open command window here` or `Open PowerShell window here`.
|
||||
|
||||
Run `yarn install`, then:
|
||||
Run `yarn install` to install npm dependencies, then one of the following:
|
||||
|
||||
- `yarn run build` - build the project in production mode.
|
||||
- `yarn run watch` - launch a development server, with live log
|
||||
collection, byond cache reloading and hot module replacement.
|
||||
- `yarn run watch` - launch a development server.
|
||||
- `yarn run lint` - show problems with the code.
|
||||
- `yarn run lint --fix` - auto-fix problems with the code.
|
||||
- `yarn run analyze` - run a bundle analyzer.
|
||||
|
||||
We also got some batch files in store, which are very simple to use:
|
||||
We also got some batch files in store, for those who don't like fiddling
|
||||
with the console:
|
||||
|
||||
- `bin/tgui-build.bat` - build the project in production mode.
|
||||
- `bin/tgui-dev-server.bat` - launch a development server, with live log
|
||||
collection, cache reloading and hot module replacement.
|
||||
- `bin/tgui-dev-server.bat` - launch a development server.
|
||||
|
||||
> Remember to always run a full build before submitting a PR. It creates
|
||||
> a compressed javascript bundle which is then referenced from DM code.
|
||||
@@ -170,6 +177,19 @@ animate starting from `0`, and if omitted, it will not play an initial
|
||||
animation.
|
||||
- `format: function` - Output formatter.
|
||||
- Example: `value => Math.round(value)`.
|
||||
- `children: function` - Pull a raw number to animate more complex things
|
||||
deeper in the DOM tree.
|
||||
- Example: `value => <Icon rotation={value} />`
|
||||
|
||||
### `BlockQuote`
|
||||
|
||||
Just a block quote, just like this example in markdown:
|
||||
|
||||
> Here's an example of a block quote.
|
||||
|
||||
Props:
|
||||
|
||||
- See inherited props: [Box](#box)
|
||||
|
||||
### `Box`
|
||||
|
||||
@@ -241,10 +261,15 @@ all available horizontal space.
|
||||
- `relative` - Relative positioning.
|
||||
- `absolute` - Absolute positioning.
|
||||
- `fixed` - Fixed positioning.
|
||||
- `top: number` - Vertical position of a positioned element.
|
||||
- `bottom: number` - Vertical position of a positioned element.
|
||||
- `left: number` - Horizontal position of a positioned element.
|
||||
- `right: number` - Horizontal position of a positioned element.
|
||||
- `color: string` - An alias to `textColor`.
|
||||
- `textColor: string` - Sets text color.
|
||||
- `#ffffff` - Hex format
|
||||
- `rgba(255, 255, 255, 1)` - RGB format
|
||||
- `purple` - Applies an atomic `color-<name>` class to the element.
|
||||
See `styles/color-map.scss`.
|
||||
- `backgroundColor: string` - Sets background color.
|
||||
- `#ffffff` - Hex format
|
||||
- `rgba(255, 255, 255, 1)` - RGB format
|
||||
|
||||
### `Button`
|
||||
|
||||
@@ -253,7 +278,7 @@ Buttons allow users to take actions, and make choices, with a single click.
|
||||
Props:
|
||||
|
||||
- See inherited props: [Box](#box)
|
||||
- `fluid: boolean` - Tells the button to fill all available horizontal space.
|
||||
- `fluid: boolean` - Fill all available horizontal space.
|
||||
- `icon: string` - Adds an icon to the button.
|
||||
- `color: string` - Button color, as defined in `variables.scss`.
|
||||
- There is also a special color `transparent` - makes the button
|
||||
@@ -359,6 +384,36 @@ remaining space is distributed. It can be a length (e.g. `20%`, `5rem`, etc.),
|
||||
an `auto` or `content` keyword.
|
||||
- `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`
|
||||
|
||||
Helps you to divide horizontal space into two or more equal sections.
|
||||
It is essentially a single-row `Table`, but with some extra features.
|
||||
|
||||
Example:
|
||||
|
||||
```jsx
|
||||
<Grid>
|
||||
<Grid.Column>
|
||||
<Section title="Section 1" content="Hello world!" />
|
||||
</Grid.Column>
|
||||
<Grid.Column size={2}>
|
||||
<Section title="Section 2" content="Hello world!" />
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
```
|
||||
|
||||
Props:
|
||||
|
||||
- See inherited props: [Table](#table)
|
||||
|
||||
### `Grid.Column`
|
||||
|
||||
Props:
|
||||
|
||||
- See inherited props: [Table.Cell](#table-cell)
|
||||
- `size: number` (default: 1) - Size of the column relative to other columns.
|
||||
|
||||
### `Icon`
|
||||
|
||||
Renders one of the FontAwesome icons of your choice.
|
||||
@@ -378,6 +433,25 @@ Props:
|
||||
- `name: string` - Icon name.
|
||||
- `size: number` - Icon size. `1` is normal size, `2` is two times bigger.
|
||||
Fractional numbers are supported.
|
||||
- `rotation: number` - Icon rotation, in degrees.
|
||||
- `spin: boolean` - Whether an icon should be spinning. Good for load
|
||||
indicators.
|
||||
|
||||
### `Input`
|
||||
|
||||
A basic text input, which allow users to enter text into a UI.
|
||||
|
||||
> Input does not support custom font size and height due to the way
|
||||
> it's implemented in CSS. Eventually, this needs to be fixed.
|
||||
|
||||
Props:
|
||||
|
||||
- See inherited props: [Box](#box)
|
||||
- `value: string` - Value of an input.
|
||||
- `fluid: boolean` - Fill all available horizontal space.
|
||||
- `onChange: (e, value) => void` - An event, which fires when you commit
|
||||
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.
|
||||
|
||||
### `LabeledList`
|
||||
|
||||
@@ -440,6 +514,42 @@ Props:
|
||||
|
||||
- `size: number` - Size of the divider.
|
||||
|
||||
### `NoticeBox`
|
||||
|
||||
A notice box, which warns you about something very important.
|
||||
|
||||
Props:
|
||||
|
||||
- See inherited props: [Box](#box)
|
||||
|
||||
### `NumberInput`
|
||||
|
||||
A fancy, interactive number input, which you can either drag up and down
|
||||
to fine tune the value, or single click it to manually type a number.
|
||||
|
||||
Props:
|
||||
|
||||
- `animated: boolean` - Animates the value if it was changed externally.
|
||||
- `fluid: boolean` - Fill all available horizontal space.
|
||||
- `value: number` - Value itself.
|
||||
- `unit: string` - Unit to display to the right of value.
|
||||
- `minValue: number` - Lowest possible value.
|
||||
- `maxValue: number` - Highest possible value.
|
||||
- `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`.
|
||||
- `width: string|number` - Width of the element, in `Box` units or pixels.
|
||||
- `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.
|
||||
|
||||
### `ProgressBar`
|
||||
|
||||
Progress indicators inform users about the status of ongoing processes.
|
||||
@@ -448,8 +558,27 @@ Progress indicators inform users about the status of ongoing processes.
|
||||
<ProgressBar value={0.6} />
|
||||
```
|
||||
|
||||
- `value: number` - Current progress as a floating point number,
|
||||
from 0 to 1. Determines how filled the bar is.
|
||||
Usage of `ranges` prop:
|
||||
|
||||
```jsx
|
||||
<ProgressBar
|
||||
ranges={{
|
||||
good: [0.5, Infinity],
|
||||
average: [0.25, 0.5],
|
||||
bad: [-Infinity, 0.25],
|
||||
}}
|
||||
value={0.6} />
|
||||
```
|
||||
|
||||
Props:
|
||||
|
||||
- `value: number` - Current progress as a floating point number between
|
||||
`minValue` (default: 0) and `maxValue` (default: 1). Determines the
|
||||
percentage and how filled the bar is.
|
||||
- `minValue: number` - Lowest possible value.
|
||||
- `maxValue: number` - Highest possible value.
|
||||
- `ranges: { color: [from, to] }` - Applies a `color` to the progress bar
|
||||
based on whether the value lands in the range between `from` and `to`.
|
||||
- `color: string` - Color of the progress bar.
|
||||
- `content/children: any` - Content to render inside the progress bar.
|
||||
|
||||
@@ -489,6 +618,50 @@ 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.
|
||||
|
||||
### `Table`
|
||||
|
||||
A straight forward mapping to a standard html table, which is slightly
|
||||
simplified (does not need a `<tbody>` tag) and with sane default styles
|
||||
(e.g. table width is 100% by default).
|
||||
|
||||
Example:
|
||||
|
||||
```jsx
|
||||
<Table>
|
||||
<Table.Row>
|
||||
<Table.Cell bold>
|
||||
Hello world!
|
||||
</Table.Cell>
|
||||
<Table.Cell collapsing color="label">
|
||||
Label
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
</Table>
|
||||
```
|
||||
|
||||
Props:
|
||||
|
||||
- See inherited props: [Box](#box)
|
||||
- `collapsing: boolean` - Collapses table to the smallest possible size.
|
||||
|
||||
### `Table.Row`
|
||||
|
||||
A straight forward mapping to `<tr>` element.
|
||||
|
||||
Props:
|
||||
|
||||
- See inherited props: [Box](#box)
|
||||
|
||||
### `Table.Cell`
|
||||
|
||||
A straight forward mapping to `<td>` element.
|
||||
|
||||
Props:
|
||||
|
||||
- See inherited props: [Box](#box)
|
||||
- `collapsing: boolean` - Collapses table cell to the smallest possible size,
|
||||
and stops any text inside from wrapping.
|
||||
|
||||
### `Tabs`
|
||||
|
||||
Tabs make it easy to explore and switch between different views.
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
# Converting old tgui interfaces to tgui-next
|
||||
|
||||
This guide is going to assume you already know roughly how tgui-next works, how to make new uis, etc. It's mostly aimed at helping translate concepts between tgui and tgui-next, and clarify some confusing parts of the transition.
|
||||
|
||||
## Backend
|
||||
|
||||
Backend in almost every case does not require any changes. In particularly heavy ui cases, something to be aware of is the new `ui_static_data()` proc. This proc allows you to split some data sent to the interface off into data that will only be sent on ui initialize and when manually updated by elsewhere in the code. Useful for things like cargo where you have a very large set of mostly identical code.
|
||||
|
||||
Keep in mind that for uis where *all* data doesn't need to be live updating, you can just toggle off autoupdate for the ui instead of messing with static data.
|
||||
|
||||
## Frontend
|
||||
|
||||
The very first thing to note is the name of the `ract` file containing the old interface. Whatever the name is (minus the extension) is going to be what the route key is going to be.
|
||||
|
||||
One thing I like to do before starting work on a conversion is screenshot what the old interface looks like so I have something to reference to make sure that the styling can line up as well.
|
||||
|
||||
## General syntax changes
|
||||
|
||||
Ractive has a fairly different templating syntax from React.
|
||||
|
||||
### `data`
|
||||
|
||||
You likely already know that React data inserts look like this
|
||||
|
||||
```jsx
|
||||
{data.example_data}
|
||||
```
|
||||
|
||||
Ractive looks very similar, the only real difference is that React uses one paranthesis instead of two.
|
||||
|
||||
```ractive
|
||||
{{data.example_data}}
|
||||
```
|
||||
|
||||
However, you may occasionally come across data inserts that instead of referencing the `data` var or things contained within it instead reference `adata`. `adata` was short for animated data, and was used for smooth number animations in interfaces. instead of having a seperate data structure for this. tgui-next instead uses a component, which is `AnimatedNumber`.
|
||||
|
||||
`AnimatedNumber` is used like this
|
||||
|
||||
```jsx
|
||||
<AnimatedNumber value={data.example_data}/>
|
||||
```
|
||||
|
||||
Make sure you don't forget to import it.
|
||||
|
||||
### Conditionals
|
||||
|
||||
Ractive conditionals look very different from React conditionals.
|
||||
|
||||
A ractive `if` (only render if result of expression is true) looks like this
|
||||
|
||||
```ractive
|
||||
{{#if data.condition}}
|
||||
<span>Example Render</span>
|
||||
{{/if}}
|
||||
```
|
||||
|
||||
The equivalent React would be
|
||||
|
||||
```jsx
|
||||
{!!data.condition && (
|
||||
<Fragment>Example Render</Fragment>
|
||||
)}
|
||||
```
|
||||
|
||||
This might look a bit intimidating compared to the reactive part but it's not as complicated as it seems:
|
||||
|
||||
1. A new jsx context is opened with `{}`
|
||||
2. jsx contexts like this always render whatever the return value is, so we can use `&&` to return a value we want. `&&` returns the last true value (or not "falsey" because this is js).
|
||||
3. jsx tags are never "falsey", so a conditioned paired with a jsx tag will mean the condition being true will continue on and return the tag. `()` is just used to contain the tag
|
||||
4. The `!!` is not a special operator, it is a literal double negation. This is because most `false` values coming from byond are going to actually be `0`, which would be rendered if the condition is false. Negating `0` returns `true`, negating `true` returns `false`, which isn't rendered.
|
||||
5. `Fragment` is actually a true "dead tag". It's similar to `span` in that it just contains things without providing functionality, but it's unwrapped before the final render and children of it are injected into its parent. In a case where you only need to render text without any styling, it's probably better to just return a string literal (`"Example Render"`), but this was just to illustrate that you can put any tag in this expression.
|
||||
|
||||
You don't really need to know all this to understand how to use it, but I find it helps with understanding when things go wrong.
|
||||
|
||||
Ractive conditionals can have an `else` as well
|
||||
```ractive
|
||||
{{#if data.condition}}
|
||||
value
|
||||
{{else}}
|
||||
other value
|
||||
{{/if}}
|
||||
```
|
||||
|
||||
Similarly to the previous example, just add a `||` operator to handle the
|
||||
"falsy" condition:
|
||||
|
||||
```jsx
|
||||
{!!data.condition && (
|
||||
<Fragment>value</Fragment>
|
||||
) || (
|
||||
<Fragment>other value</Fragment>
|
||||
)}
|
||||
```
|
||||
|
||||
There's also our good old friend - the ternary:
|
||||
|
||||
```jsx
|
||||
{data.condition ? 'value' : 'other value'}
|
||||
```
|
||||
|
||||
Keep in mind you can also use tags here like the conditional example,
|
||||
and you can mix string literals, values, and tags as well.
|
||||
|
||||
```jsx
|
||||
{data.is_robot ? (
|
||||
<Button content="Robot Button"/>
|
||||
) : 'Not a robot'}
|
||||
```
|
||||
|
||||
### Loops
|
||||
|
||||
Ractive has loops for iterating over data and inserting something for each
|
||||
member of an array or object
|
||||
|
||||
```
|
||||
{{#each data.list_of_foo}}
|
||||
foo {{number}} is here.
|
||||
{{/each}}
|
||||
```
|
||||
|
||||
This didn't care whether the data was an array or an object, and members of each entry of the loop were "unwrapped" so to say. `{{number}}` in that example is referring to the `{{number}}` value on the entry of the list for that iterate.
|
||||
|
||||
The React equivalent to this is going to be `map`.
|
||||
|
||||
_AN IMPORTANT DISTINCTION HERE IS THAT NOW WE CARE WHETHER THIS IS AN OBJECT OR AN ARRAY BEING ACTED ON._
|
||||
|
||||
Objects are represented by `{}`, arrays by `[]`
|
||||
|
||||
"How can I tell?" you may ask. It's fairly simple, associated lists on the byond side are going to be turned into objects when they get json converted, normal lists are going to be turned into arrays.
|
||||
|
||||
`list("bla", "blo")` would become `["bla", "blo"]` and `list("foo" = 1, "bar" = 2)` would become `{"foo": 1, "bar": 2}`
|
||||
|
||||
First things first, above the `return` of the function you're making the interface in, you're going to want to add something like this
|
||||
```jsx
|
||||
const things = data.things || [];
|
||||
```
|
||||
|
||||
This ensures that you'll never be reading a null entry by mistake. Substitute `{}` for objects as appropriate.
|
||||
|
||||
If it's an array, you'll want to do this in the template
|
||||
```jsx
|
||||
{things.map(thing => (
|
||||
<Fragment>Thing {thing.number} is here!</Fragment>
|
||||
))}
|
||||
```
|
||||
|
||||
`map` is a function that calls a passed function (a lambda) on each entry, and returns the value. You should already know that returned tags and values (except `false`) get rendered, so that's how it's rendering each time.
|
||||
|
||||
A lambda is what's known as an anonymous function, it's a function that doesn't have a name that's only used for a specific usage. `map` wants a function that has one parameter, so we define one parameter then use `=>` to say the parameter has to do with the following block.
|
||||
|
||||
`parameter => ()` is just a shorthand for `parameter => {return();}`
|
||||
|
||||
This is quite a bit higher concept than ractive's each statements, so feel free to look around and ~~copy paste~~ learn from how other interfaces use this.
|
||||
|
||||
Now for objects, there's a genuinely pretty gross syntax here. We apoligize, it's related to ie8 compatibility nonsense.
|
||||
|
||||
```jsx
|
||||
{map((value, key) => {
|
||||
return (
|
||||
<Fragment>Key is {key}, value is {value}</Fragment>
|
||||
);
|
||||
})(fooObject)}
|
||||
```
|
||||
|
||||
Again, sorry for this syntax. `fooObject` would be the object being iterated on, value would be the value of the iterated entry on the list, and key would be the key. the naming of value and key isn't important here, but knowing that it goes `value`, `key` in that order is important.
|
||||
|
||||
It is sometimes better to preemptively convert an object to array before
|
||||
the big return statement, like this:
|
||||
|
||||
```jsx
|
||||
const fooArray = map((value, key) => {
|
||||
return { key, value };
|
||||
})(fooObject);
|
||||
```
|
||||
|
||||
Or if you just want to discard all keys, this will also work nicely:
|
||||
|
||||
```jsx
|
||||
const fooArray = toArray(fooObject);
|
||||
```
|
||||
|
||||
Also occasionally you'd see an else:
|
||||
|
||||
```
|
||||
{{#each data.potentially_empty_list}}
|
||||
Thing "{{name}}" is in this list!
|
||||
{{else}}
|
||||
None found!
|
||||
{{/each}}
|
||||
```
|
||||
|
||||
This would iterate using the first contents each time, or display the second option if the list was empty.
|
||||
|
||||
To do a similar thing in JSX, just check if array is empty like this:
|
||||
|
||||
```jsx
|
||||
{fooArray.length === 0 && 'fooArray is empty.'}
|
||||
{fooArray.map(foo => (
|
||||
<Fragment>Foo is {foo}</Fragment>
|
||||
))}
|
||||
```
|
||||
|
||||
### Extra Stuff
|
||||
|
||||
I'll put some extra stuff here when I think of it.
|
||||
|
||||
## Components
|
||||
|
||||
This will be a reference of tgui components and the tgui-next equivalent.
|
||||
|
||||
### `ui-display`
|
||||
|
||||
Equivalent of `<ui-display>` is `<Section>`
|
||||
|
||||
```
|
||||
<ui-display title="Status">
|
||||
Contents
|
||||
</ui-display>
|
||||
```
|
||||
|
||||
becomes
|
||||
|
||||
```jsx
|
||||
<Section title="Status">
|
||||
Contents
|
||||
</Section>
|
||||
```
|
||||
|
||||
A feature sometimes used is if `ui-display` has the `button` property, it will contain a `partial` command. This becomes the `buttons` property on `Section`:
|
||||
|
||||
```
|
||||
<ui-display title="Status" button>
|
||||
{{#partial button}}
|
||||
<ui-button /> // lots more button bullshit here
|
||||
{{/partial}}
|
||||
Contents
|
||||
</ui-display>
|
||||
```
|
||||
|
||||
becomes
|
||||
|
||||
```jsx
|
||||
<Section
|
||||
title="Status"
|
||||
buttons={(
|
||||
<Button />
|
||||
)}>
|
||||
Contents
|
||||
</Section>
|
||||
```
|
||||
|
||||
### `ui-section`
|
||||
|
||||
Very important to note `ui-section` is NOT the equivalent of `Section`
|
||||
|
||||
`<ui-section>` does not have a direct equivalent, but the closest equivalent is `<LabeledList>`
|
||||
|
||||
```
|
||||
<ui-section label="power">
|
||||
No Power
|
||||
</ui-section>
|
||||
<ui-section label="connection">
|
||||
No Connection
|
||||
</ui-section>
|
||||
```
|
||||
|
||||
becomes
|
||||
|
||||
```jsx
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="power">
|
||||
No Power
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="connection">
|
||||
No Connection
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
```
|
||||
|
||||
Important to note that `LabeledList.Item` has `buttons` as well.
|
||||
|
||||
Also good to know that if you need the contents of a `LabeledList.Item` to be colored, you can just set the `color` prop on it instead of putting a `span` inside it.
|
||||
|
||||
### `ui-notice`
|
||||
|
||||
`<ui-notice>` has a direct equivalent in `<NoticeBox>`
|
||||
|
||||
```
|
||||
<ui-notice>
|
||||
Notice stuff!
|
||||
</ui-notice>
|
||||
```
|
||||
|
||||
becomes
|
||||
|
||||
```jsx
|
||||
<NoticeBox>
|
||||
Notice stuff!
|
||||
</NoticeBox>
|
||||
```
|
||||
|
||||
### `ui-button`
|
||||
|
||||
The equivalent of `ui-button` is `Button` but it works quite a bit differently.
|
||||
|
||||
```
|
||||
<ui-button
|
||||
state='{{data.condition ? "disabled" : null}}'
|
||||
action="ui_action"
|
||||
params={param: value}>
|
||||
Click
|
||||
</ui-button>
|
||||
```
|
||||
|
||||
becomes
|
||||
|
||||
```
|
||||
<Button
|
||||
content="Click"
|
||||
disabled={data.condition}
|
||||
onClick={() => act(ref, "ui_action", {param: value})}/>
|
||||
```
|
||||
@@ -1,22 +1,25 @@
|
||||
# TGUI Backend Documentation
|
||||
# Tutorial and Examples
|
||||
|
||||
## Main concepts
|
||||
|
||||
Basic tgui backend code consists of defining a few procs. In these procs
|
||||
you will handle a request to open or update a UI (typically by updating a UI
|
||||
if it exists or setting up and opening it if it does not), a request for data,
|
||||
in which you build a list to be passed as JSON to the UI, and an action
|
||||
handler, which handles any user input.
|
||||
Basic tgui backend code consists of the following vars and procs:
|
||||
|
||||
- The atom, which UI corresponds to in the game world, is in most cases
|
||||
known as the `src_object`.
|
||||
- Frontend data is built in `ui_data` proc, which munges whatever complex
|
||||
data your `src_object` has into a list.
|
||||
- The action/topic handler, `ui_act`, is what recieves input from the user
|
||||
and acts on it.
|
||||
- The request/update proc, `ui_interact` is where you open your UI and set
|
||||
options like title, size, autoupdate, theme, and more.
|
||||
- Finally, `ui_state` (set in `ui_interact`) dictates under what conditions
|
||||
```
|
||||
ui_interact(mob/user, ui_key, datum/tgui/ui, force_open,
|
||||
datum/tgui/master_ui, datum/ui_state/state)
|
||||
ui_data(mob/user)
|
||||
ui_act(action, params)
|
||||
```
|
||||
|
||||
- `src_object` - The atom, which UI corresponds to in the game world.
|
||||
- `ui_interact` - The proc where you will handle a request to open an
|
||||
interface. Typically, you would update an existing UI (if it exists),
|
||||
or set up a new instance of UI by calling the `SStgui` subsystem.
|
||||
- `ui_data` - In this proc you munges whatever complex data your `src_object`
|
||||
has into an associative list, which will then be sent to UI as a JSON string.
|
||||
- `ui_act` - This proc receives user actions and reacts to them by changing
|
||||
the state of the game.
|
||||
- `ui_state` (set in `ui_interact`) - This var dictates under what conditions
|
||||
a UI may be interacted with. This may be the standard checks that check if
|
||||
you are in range and conscious, or more.
|
||||
|
||||
@@ -115,20 +118,14 @@ This object contains a few special values:
|
||||
- `config` is always the same and is part of core tgui
|
||||
(it will be explained later),
|
||||
- `data` is the data returned from `ui_data`
|
||||
- `adata` is the same, but with certain values (numbers at this time)
|
||||
interpolated in order to allow animation.
|
||||
|
||||
```jsx
|
||||
import { Section, LabeledList } from '../components';
|
||||
|
||||
const SampleInterface = props => {
|
||||
// Extract state from props
|
||||
const { state } = props;
|
||||
// Extract config and data from the state
|
||||
const { config, data } = state;
|
||||
// Extract window reference (will be used later for dispatching actions)
|
||||
const { ref } = config;
|
||||
// Return the Virtual DOM
|
||||
return (
|
||||
<Section title="Health status">
|
||||
<LabeledList>
|
||||
@@ -145,11 +142,10 @@ const SampleInterface = props => {
|
||||
```
|
||||
|
||||
This syntax can be very confusing at first, but it is very important to
|
||||
realize that this is just a natural extension of javascript. This syntax
|
||||
simply creates a Virtual DOM object, which you can treat as any other
|
||||
object in javascript. Here are some control flow examples:
|
||||
realize that this is just a natural extension of javascript. Here's a few
|
||||
examples of this syntax:
|
||||
|
||||
Returning different elements based on a condition:
|
||||
Return a different element based on a condition:
|
||||
|
||||
```jsx
|
||||
if (condition) {
|
||||
@@ -158,7 +154,7 @@ if (condition) {
|
||||
return <Bar />;
|
||||
```
|
||||
|
||||
Conditionally rendering a element inside of another element:
|
||||
Conditionally render a element inside of another element:
|
||||
|
||||
```jsx
|
||||
<Box>
|
||||
@@ -168,7 +164,7 @@ Conditionally rendering a element inside of another element:
|
||||
</Box>
|
||||
```
|
||||
|
||||
Looping over the array to make element for each item:
|
||||
Looping over the array to make an element for each item:
|
||||
|
||||
```jsx
|
||||
<LabeledList>
|
||||
@@ -180,6 +176,24 @@ Looping over the array to make element for each item:
|
||||
</LabeledList>
|
||||
```
|
||||
|
||||
### Routing table
|
||||
|
||||
Once you finished creating your interface, you need to add a route entry to
|
||||
the large `ROUTES` object, otherwise tgui won't know when and how to render
|
||||
your interface. Key of this `ROUTES` object corresponds to the interface
|
||||
name you use in DM code.
|
||||
|
||||
```js
|
||||
import { SampleInterface } from './interfaces/SampleInterface';
|
||||
|
||||
const ROUTES = {
|
||||
sample_interface: {
|
||||
component: () => SampleInterface,
|
||||
scrollable: true,
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Copypasta
|
||||
|
||||
We all do it, even the best of us. If you just want to make a tgui **fast**,
|
||||
@@ -196,17 +210,16 @@ upon code review):
|
||||
/obj/copypasta/ui_data(mob/user)
|
||||
var/list/data = list()
|
||||
data["var"] = var
|
||||
|
||||
return data
|
||||
|
||||
/obj/copypasta/ui_act(action, params)
|
||||
if(..())
|
||||
return
|
||||
switch(action)
|
||||
if("copypasta")
|
||||
var/newvar = params["var"]
|
||||
var = Clamp(newvar, min_val, max_val) // Just a demo of proper input sanitation.
|
||||
. = TRUE
|
||||
if(action == "copypasta")
|
||||
var/newvar = params["var"]
|
||||
// A demo of proper input sanitation.
|
||||
var = CLAMP(newvar, min_val, max_val)
|
||||
return TRUE
|
||||
update_icon() // Not applicable to all objects.
|
||||
```
|
||||
|
||||
@@ -216,13 +229,9 @@ And the template:
|
||||
import { Section, LabeledList } from '../components';
|
||||
|
||||
const SampleInterface = props => {
|
||||
// Extract state from props
|
||||
const { state } = props;
|
||||
// Extract config and data from the state
|
||||
const { config, data } = state;
|
||||
// Extract window reference (will be used later for dispatching actions)
|
||||
const { ref } = config;
|
||||
// Return the UI
|
||||
return (
|
||||
<Section title="Section name">
|
||||
<LabeledList>
|
||||
|
||||
@@ -63,6 +63,9 @@ export const retrace = stack => {
|
||||
.map(frame => {
|
||||
// Stringify the frame
|
||||
const { file, methodName, lineNumber } = frame;
|
||||
if (!file) {
|
||||
return ` at ${methodName}`;
|
||||
}
|
||||
const compactPath = file
|
||||
.replace(/^webpack:\/\/\/?/, './')
|
||||
.replace(/.*node_modules\//, '');
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Box } from './Box';
|
||||
|
||||
export const BlockQuote = props => {
|
||||
const { style, ...rest } = props;
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
'color': 'rgba(255, 255, 255, 0.5)',
|
||||
'border-left': '2px solid rgba(255, 255, 255, 0.5)',
|
||||
'padding-left': '6px',
|
||||
...style,
|
||||
}}
|
||||
{...rest} />
|
||||
);
|
||||
};
|
||||
@@ -16,6 +16,10 @@ export const unit = value => {
|
||||
}
|
||||
};
|
||||
|
||||
const isColorCode = str => typeof str === 'string' && (
|
||||
str.startsWith('#') || str.startsWith('rgb')
|
||||
);
|
||||
|
||||
const mapRawPropTo = attrName => (style, value) => {
|
||||
if (!isFalsy(value)) {
|
||||
style[attrName] = value;
|
||||
@@ -42,6 +46,12 @@ const mapDirectionalUnitPropTo = (attrName, dirs) => (style, value) => {
|
||||
}
|
||||
};
|
||||
|
||||
const mapColorPropTo = attrName => (style, value) => {
|
||||
if (isColorCode(value)) {
|
||||
style[attrName] = value;
|
||||
}
|
||||
};
|
||||
|
||||
const styleMapperByPropName = {
|
||||
// Direct mapping
|
||||
position: mapRawPropTo('position'),
|
||||
@@ -67,6 +77,10 @@ const styleMapperByPropName = {
|
||||
mb: mapUnitPropTo('margin-bottom'),
|
||||
ml: mapUnitPropTo('margin-left'),
|
||||
mr: mapUnitPropTo('margin-right'),
|
||||
// Color props
|
||||
color: mapColorPropTo('color'),
|
||||
textColor: mapColorPropTo('color'),
|
||||
backgroundColor: mapColorPropTo('background-color'),
|
||||
};
|
||||
|
||||
export const computeBoxProps = props => {
|
||||
@@ -103,11 +117,11 @@ export const Box = props => {
|
||||
const {
|
||||
as = 'div',
|
||||
className,
|
||||
color,
|
||||
content,
|
||||
children,
|
||||
...rest
|
||||
} = props;
|
||||
const color = props.textColor || props.color;
|
||||
// Render props
|
||||
if (typeof children === 'function') {
|
||||
return children(computeBoxProps(props));
|
||||
@@ -119,7 +133,7 @@ export const Box = props => {
|
||||
as,
|
||||
classes([
|
||||
className,
|
||||
color && 'color-' + color,
|
||||
color && !isColorCode(color) && 'color-' + color,
|
||||
]),
|
||||
content || children,
|
||||
ChildFlags.UnknownChildren,
|
||||
|
||||
@@ -51,18 +51,19 @@ export const Button = props => {
|
||||
tabIndex={!disabled && '0'}
|
||||
unselectable={tridentVersion <= 4}
|
||||
onclick={e => {
|
||||
if (disabled || !onClick) {
|
||||
return;
|
||||
}
|
||||
refocusLayout();
|
||||
onClick(e);
|
||||
if (!disabled && onClick) {
|
||||
onClick(e);
|
||||
}
|
||||
}}
|
||||
onKeyDown={e => {
|
||||
const keyCode = window.event ? e.which : e.keyCode;
|
||||
// Simulate a click when pressing space or enter.
|
||||
if (keyCode === KEY_SPACE || keyCode === KEY_ENTER) {
|
||||
e.preventDefault();
|
||||
onClick(e);
|
||||
if (!disabled && onClick) {
|
||||
onClick(e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Refocus layout on pressing escape.
|
||||
|
||||
@@ -5,16 +5,16 @@
|
||||
rgba(0, 0, 0, 1),
|
||||
rgba(255, 255, 255, 1));
|
||||
|
||||
transition: color, background-color 50ms;
|
||||
transition: color 50ms, background-color 50ms;
|
||||
background-color: $color;
|
||||
color: $text-color;
|
||||
|
||||
&:hover {
|
||||
transition: color, background-color 0ms;
|
||||
transition: color 0ms, background-color 0ms;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
transition: color, background-color 100ms;
|
||||
transition: color 100ms, background-color 100ms;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
|
||||
@@ -14,12 +14,18 @@ export const Grid = props => {
|
||||
|
||||
Grid.defaultHooks = pureComponentHooks;
|
||||
|
||||
export const GridItem = props => {
|
||||
export const GridColumn = props => {
|
||||
const { size = 1, style, ...rest } = props;
|
||||
return (
|
||||
<Table.Cell {...props} />
|
||||
<Table.Cell
|
||||
style={{
|
||||
width: size + '%',
|
||||
...style,
|
||||
}}
|
||||
{...rest} />
|
||||
);
|
||||
};
|
||||
|
||||
Grid.defaultHooks = pureComponentHooks;
|
||||
|
||||
Grid.Item = GridItem;
|
||||
Grid.Column = GridColumn;
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { classes, pureComponentHooks } from 'common/react';
|
||||
import { Component, createRef } from 'inferno';
|
||||
import { Box } from './Box';
|
||||
|
||||
/* eslint-disable react/destructuring-assignment */
|
||||
export class Input extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.inputRef = createRef();
|
||||
this.state = {
|
||||
editing: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const nextValue = this.props.value;
|
||||
const input = this.inputRef.current;
|
||||
if (input) {
|
||||
input.value = nextValue;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const { editing } = this.state;
|
||||
const prevValue = prevProps.value;
|
||||
const nextValue = this.props.value;
|
||||
const input = this.inputRef.current;
|
||||
if (input && !editing && prevValue !== nextValue) {
|
||||
input.value = nextValue;
|
||||
}
|
||||
}
|
||||
|
||||
setEditing(editing) {
|
||||
this.setState({ editing });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
// Input only props
|
||||
const {
|
||||
onInput,
|
||||
onChange,
|
||||
value,
|
||||
...boxProps
|
||||
} = props;
|
||||
// Box props
|
||||
const {
|
||||
className,
|
||||
fluid,
|
||||
...rest
|
||||
} = boxProps;
|
||||
return (
|
||||
<Box
|
||||
className={classes([
|
||||
'Input',
|
||||
fluid && 'Input--fluid',
|
||||
className,
|
||||
])}
|
||||
{...rest}>
|
||||
<div className="Input__baseline">
|
||||
.
|
||||
</div>
|
||||
<input
|
||||
ref={this.inputRef}
|
||||
type="text"
|
||||
className="Input__input"
|
||||
onInput={e => {
|
||||
this.setEditing(true);
|
||||
if (onInput) {
|
||||
onInput(e, e.target.value);
|
||||
}
|
||||
}}
|
||||
onFocus={e => {
|
||||
this.setEditing(true);
|
||||
}}
|
||||
onBlur={e => {
|
||||
const { editing } = this.state;
|
||||
if (editing) {
|
||||
this.setEditing(false);
|
||||
if (onChange) {
|
||||
onChange(e, e.target.value);
|
||||
}
|
||||
}
|
||||
}}
|
||||
onKeyDown={e => {
|
||||
if (e.keyCode === 13) {
|
||||
this.setEditing(false);
|
||||
if (onChange) {
|
||||
onChange(e, e.target.value);
|
||||
}
|
||||
if (onInput) {
|
||||
onInput(e, e.target.value);
|
||||
}
|
||||
e.target.blur();
|
||||
return;
|
||||
}
|
||||
if (e.keyCode === 27) {
|
||||
this.setEditing(false);
|
||||
e.target.value = props.value;
|
||||
e.target.blur();
|
||||
return;
|
||||
}
|
||||
}} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Input.defaultHooks = pureComponentHooks;
|
||||
@@ -0,0 +1,46 @@
|
||||
.Input {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 120px;
|
||||
border: 1px solid $input-base-color;
|
||||
border: 1px solid rgba($input-base-color, 0.75);
|
||||
border-radius: 2px;
|
||||
color: #fff;
|
||||
background-color: #000;
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
padding: 0 4px;
|
||||
margin-right: 2px;
|
||||
line-height: 17px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.Input--fluid {
|
||||
display: block;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.Input__baseline {
|
||||
display: inline-block;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.Input__input {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
line-height: 17px;
|
||||
height: 17px;
|
||||
margin: 0;
|
||||
padding: 0 6px;
|
||||
font-family: Verdana, sans-serif;
|
||||
background-color: transparent;
|
||||
color: #fff;
|
||||
color: inherit;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { clamp } from 'common/math';
|
||||
import { pureComponentHooks } from 'common/react';
|
||||
import { classes, pureComponentHooks } from 'common/react';
|
||||
import { Component, createRef } from 'inferno';
|
||||
import { tridentVersion } from '../byond';
|
||||
import { AnimatedNumber } from './AnimatedNumber';
|
||||
@@ -20,13 +20,15 @@ export class NumberInput extends Component {
|
||||
};
|
||||
|
||||
// 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,
|
||||
});
|
||||
setTimeout(() => this.setState({
|
||||
clearTimeout(this.flickerTimer);
|
||||
this.flickerTimer = setTimeout(() => this.setState({
|
||||
suppressingFlicker: false,
|
||||
}), suppressFlicker);
|
||||
}
|
||||
@@ -68,6 +70,9 @@ export class NumberInput extends Component {
|
||||
const state = { ...prevState };
|
||||
const offset = state.origin - e.screenY;
|
||||
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(
|
||||
@@ -75,7 +80,9 @@ export class NumberInput extends Component {
|
||||
minValue - step, maxValue + step);
|
||||
// Clamp the final value
|
||||
state.value = clamp(
|
||||
state.internalValue - state.internalValue % step,
|
||||
state.internalValue
|
||||
- state.internalValue % step
|
||||
+ stepOffset,
|
||||
minValue, maxValue);
|
||||
state.origin = e.screenY;
|
||||
}
|
||||
@@ -125,6 +132,8 @@ export class NumberInput extends Component {
|
||||
suppressingFlicker,
|
||||
} = this.state;
|
||||
const {
|
||||
className,
|
||||
fluid,
|
||||
animated,
|
||||
value,
|
||||
unit,
|
||||
@@ -157,7 +166,11 @@ export class NumberInput extends Component {
|
||||
));
|
||||
return (
|
||||
<Box
|
||||
className="NumberInput"
|
||||
className={classes([
|
||||
'NumberInput',
|
||||
fluid && 'NumberInput--fluid',
|
||||
className,
|
||||
])}
|
||||
minWidth={width}
|
||||
onMouseDown={this.handleDragStart}>
|
||||
<div className="NumberInput__barContainer">
|
||||
@@ -172,7 +185,7 @@ export class NumberInput extends Component {
|
||||
{contentElement}
|
||||
<input
|
||||
ref={this.inputRef}
|
||||
className="NumberInput__editable"
|
||||
className="NumberInput__input"
|
||||
style={{
|
||||
display: !editing ? 'none' : undefined,
|
||||
}}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
$input-base-color: #88bfff;
|
||||
|
||||
.NumberInput {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
@@ -16,6 +14,10 @@ $input-base-color: #88bfff;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.NumberInput--fluid {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.NumberInput__content {
|
||||
margin-left: 6px;
|
||||
}
|
||||
@@ -37,7 +39,7 @@ $input-base-color: #88bfff;
|
||||
background-color: $input-base-color;
|
||||
}
|
||||
|
||||
.NumberInput__editable {
|
||||
.NumberInput__input {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -49,6 +51,7 @@ $input-base-color: #88bfff;
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
line-height: 17px;
|
||||
height: 17px;
|
||||
margin: 0;
|
||||
padding: 0 6px;
|
||||
font-family: Verdana, sans-serif;
|
||||
|
||||
@@ -2,8 +2,27 @@ import { classes, pureComponentHooks } from 'common/react';
|
||||
import { clamp, toFixed } from 'common/math';
|
||||
|
||||
export const ProgressBar = props => {
|
||||
const { value, content, color, children } = props;
|
||||
const {
|
||||
value,
|
||||
minValue = 0,
|
||||
maxValue = 1,
|
||||
ranges = {},
|
||||
content,
|
||||
children,
|
||||
} = props;
|
||||
let { color } = props;
|
||||
const scaledValue = (value - minValue) / (maxValue - minValue);
|
||||
const hasContent = content !== undefined || children !== undefined;
|
||||
if (!color) {
|
||||
// Cycle through ranges in key order to determine progressbar color.
|
||||
for (let rangeName of Object.keys(ranges)) {
|
||||
const range = ranges[rangeName];
|
||||
if (range && value >= range[0] && value <= range[1]) {
|
||||
color = rangeName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={classes([
|
||||
@@ -13,12 +32,12 @@ export const ProgressBar = props => {
|
||||
<div
|
||||
className="ProgressBar__fill"
|
||||
style={{
|
||||
'width': (clamp(value, 0, 1) * 100) + '%',
|
||||
'width': (clamp(scaledValue, 0, 1) * 100) + '%',
|
||||
}} />
|
||||
<div className="ProgressBar__content">
|
||||
{hasContent && content}
|
||||
{hasContent && children}
|
||||
{!hasContent && toFixed(value * 100) + '%'}
|
||||
{!hasContent && toFixed(scaledValue * 100) + '%'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
transition: background-color, width 500ms;
|
||||
transition: background-color 500ms, width 500ms;
|
||||
background-color: $bar-color-normal;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,12 +9,12 @@
|
||||
.TitleBar__clickable {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
background-color: darken($titlebar-color-background, 0%);
|
||||
transition: color, background-color 250ms;
|
||||
transition: color 250ms, background-color 250ms;
|
||||
|
||||
&:hover {
|
||||
color: rgba(255, 255, 255, 1.0);
|
||||
background-color: #c00;
|
||||
transition: color, background-color 0ms;
|
||||
transition: color 0ms, background-color 0ms;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
export { AnimatedNumber } from './AnimatedNumber';
|
||||
export { BlockQuote } from './BlockQuote';
|
||||
export { Box } from './Box';
|
||||
export { Button } from './Button';
|
||||
export { Flex } from './Flex';
|
||||
export { Grid } from './Grid';
|
||||
export { Icon } from './Icon';
|
||||
export { Input } from './Input';
|
||||
export { LabeledList } from './LabeledList';
|
||||
export { NoticeBox } from './NoticeBox';
|
||||
export { NumberInput } from './NumberInput';
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { Fragment, Component } from 'inferno';
|
||||
import { Section, Tabs, Box, Button, Flex, ProgressBar, Tooltip } from '../components';
|
||||
import { Component, Fragment } from 'inferno';
|
||||
import {
|
||||
Box, Button, Flex, Input, LabeledList, NumberInput,
|
||||
ProgressBar, Section, Tabs, Tooltip,
|
||||
} from '../components';
|
||||
|
||||
const COLORS_ARBITRARY = [
|
||||
'black',
|
||||
@@ -44,6 +47,7 @@ export const KitchenSink = props => {
|
||||
<KitchenSinkProgress />
|
||||
<KitchenSinkTabs />
|
||||
<KitchenSinkTooltips />
|
||||
<KitchenSinkInputs />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
@@ -116,7 +120,16 @@ class KitchenSinkProgress extends Component {
|
||||
const { progress } = this.state;
|
||||
return (
|
||||
<Section title="Progress">
|
||||
<ProgressBar value={progress} />
|
||||
<ProgressBar
|
||||
ranges={{
|
||||
good: [0.5, Infinity],
|
||||
bad: [-Infinity, 0.1],
|
||||
average: [0, 0.5],
|
||||
}}
|
||||
minValue={-1}
|
||||
maxValue={1}
|
||||
value={progress}
|
||||
content={`value: ${Number(progress).toFixed(1)}`} />
|
||||
<Button
|
||||
content="-0.1"
|
||||
onClick={() => this.setState(prevState => ({
|
||||
@@ -187,6 +200,62 @@ const KitchenSinkTooltips = props => {
|
||||
);
|
||||
};
|
||||
|
||||
class KitchenSinkInputs extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
number: 0,
|
||||
text: 'Sample text',
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { number, text } = this.state;
|
||||
return (
|
||||
<Section title="Inputs">
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="NumberInput">
|
||||
<NumberInput
|
||||
animated
|
||||
width={10}
|
||||
step={1}
|
||||
stepPixelSize={5}
|
||||
value={number}
|
||||
minValue={-100}
|
||||
maxValue={100}
|
||||
onChange={(e, value) => this.setState({
|
||||
number: value,
|
||||
})} />
|
||||
<NumberInput
|
||||
animated
|
||||
width={10}
|
||||
step={1}
|
||||
stepPixelSize={5}
|
||||
value={number}
|
||||
minValue={-100}
|
||||
maxValue={100}
|
||||
onDrag={(e, value) => this.setState({
|
||||
number: value,
|
||||
})} />
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Input">
|
||||
<Input
|
||||
value={text}
|
||||
onChange={(e, value) => this.setState({
|
||||
text: value,
|
||||
})} />
|
||||
<Input
|
||||
value={text}
|
||||
onInput={(e, value) => this.setState({
|
||||
text: value,
|
||||
})} />
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const BoxOfSampleText = props => {
|
||||
return (
|
||||
<Box {...props}>
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import { toTitleCase } from 'common/string';
|
||||
import { Component, Fragment } from 'inferno';
|
||||
import { act } from '../byond';
|
||||
import { BlockQuote, Box, Button, NumberInput, Section, Table } from '../components';
|
||||
|
||||
export const OreRedemptionMachine = props => {
|
||||
const { state } = props;
|
||||
const { config, data } = state;
|
||||
const { ref } = config;
|
||||
const {
|
||||
unclaimedPoints,
|
||||
materials,
|
||||
alloys,
|
||||
diskDesigns,
|
||||
hasDisk,
|
||||
} = data;
|
||||
return (
|
||||
<Fragment>
|
||||
<Section>
|
||||
<BlockQuote mb={1}>
|
||||
This machine only accepts ore.<br />
|
||||
Gibtonite and Slag are not accepted.
|
||||
</BlockQuote>
|
||||
<Box>
|
||||
<Box inline color="label" mr={1}>
|
||||
Unclaimed points:
|
||||
</Box>
|
||||
{unclaimedPoints}
|
||||
<Button
|
||||
ml={2}
|
||||
content="Claim"
|
||||
disabled={unclaimedPoints === 0}
|
||||
onClick={() => act(ref, 'Claim')} />
|
||||
</Box>
|
||||
</Section>
|
||||
<Section>
|
||||
{hasDisk && (
|
||||
<Fragment>
|
||||
<Box mb={1}>
|
||||
<Button
|
||||
icon="eject"
|
||||
content="Eject design disk"
|
||||
onClick={() => act(ref, 'diskEject')} />
|
||||
</Box>
|
||||
<Table>
|
||||
{diskDesigns.map(design => (
|
||||
<Table.Row key={design.index}>
|
||||
<Table.Cell>
|
||||
File {design.index}: {design.name}
|
||||
</Table.Cell>
|
||||
<Table.Cell collapsing>
|
||||
<Button
|
||||
disabled={!design.canupload}
|
||||
content="Upload"
|
||||
onClick={() => act(ref, 'diskUpload', {
|
||||
design: design.index,
|
||||
})} />
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table>
|
||||
</Fragment>
|
||||
) || (
|
||||
<Button
|
||||
icon="save"
|
||||
content="Insert design disk"
|
||||
onClick={() => act(ref, 'diskInsert')} />
|
||||
)}
|
||||
</Section>
|
||||
<Section title="Materials">
|
||||
<Table>
|
||||
{materials.map(material => (
|
||||
<MaterialRow
|
||||
key={material.id}
|
||||
material={material}
|
||||
onRelease={amount => act(ref, 'Release', {
|
||||
id: material.id,
|
||||
sheets: amount,
|
||||
})} />
|
||||
))}
|
||||
</Table>
|
||||
</Section>
|
||||
<Section title="Alloys">
|
||||
<Table>
|
||||
{alloys.map(material => (
|
||||
<MaterialRow
|
||||
key={material.id}
|
||||
material={material}
|
||||
onRelease={amount => act(ref, 'Release', {
|
||||
id: material.id,
|
||||
sheets: amount,
|
||||
})} />
|
||||
))}
|
||||
</Table>
|
||||
</Section>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
class MaterialRow extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
amount: 1,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { amount } = this.state;
|
||||
const { material, onRelease } = this.props;
|
||||
const amountAvailable = Math.floor(material.amount);
|
||||
return (
|
||||
<Table.Row>
|
||||
<Table.Cell>
|
||||
{toTitleCase(material.name).replace('Alloy', '')}
|
||||
</Table.Cell>
|
||||
<Table.Cell collapsing textAlign="right">
|
||||
<Box mr={2} color="label" inline>
|
||||
{material.value && material.value + ' cr'}
|
||||
</Box>
|
||||
</Table.Cell>
|
||||
<Table.Cell collapsing textAlign="right">
|
||||
<Box mr={2} color="label" inline>
|
||||
{amountAvailable} sheets
|
||||
</Box>
|
||||
</Table.Cell>
|
||||
<Table.Cell collapsing>
|
||||
<NumberInput
|
||||
width="32px"
|
||||
step={1}
|
||||
stepPixelSize={5}
|
||||
minValue={1}
|
||||
maxValue={50}
|
||||
value={amount}
|
||||
onChange={(e, value) => this.setState({
|
||||
amount: value,
|
||||
})} />
|
||||
<Button
|
||||
disabled={amountAvailable < 1}
|
||||
content="Release"
|
||||
onClick={() => onRelease(amount)} />
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import { map } from 'common/fp';
|
||||
import { toFixed } from 'common/math';
|
||||
import { act } from '../byond';
|
||||
import { Box, Button, LabeledList, NumberInput, Section } from '../components';
|
||||
|
||||
const CHANNELS = [
|
||||
{
|
||||
name: 'Syndicate',
|
||||
freq: 1213,
|
||||
color: '#a52a2a',
|
||||
},
|
||||
{
|
||||
name: 'Red Team',
|
||||
freq: 1215,
|
||||
color: '#ff4444',
|
||||
},
|
||||
{
|
||||
name: 'Blue Team',
|
||||
freq: 1217,
|
||||
color: '#3434fd',
|
||||
},
|
||||
{
|
||||
name: 'CentCom',
|
||||
freq: 1337,
|
||||
color: '#2681a5',
|
||||
},
|
||||
{
|
||||
name: 'Supply',
|
||||
freq: 1347,
|
||||
color: '#b88646',
|
||||
},
|
||||
{
|
||||
name: 'Service',
|
||||
freq: 1349,
|
||||
color: '#6ca729',
|
||||
},
|
||||
{
|
||||
name: 'Science',
|
||||
freq: 1351,
|
||||
color: '#c68cfa',
|
||||
},
|
||||
{
|
||||
name: 'Command',
|
||||
freq: 1353,
|
||||
color: '#5177ff',
|
||||
},
|
||||
{
|
||||
name: 'Medical',
|
||||
freq: 1355,
|
||||
color: '#57b8f0',
|
||||
},
|
||||
{
|
||||
name: 'Engineering',
|
||||
freq: 1357,
|
||||
color: '#f37746',
|
||||
},
|
||||
{
|
||||
name: 'Security',
|
||||
freq: 1359,
|
||||
color: '#dd3535',
|
||||
},
|
||||
{
|
||||
name: 'AI Private',
|
||||
freq: 1447,
|
||||
color: '#d65d95',
|
||||
},
|
||||
{
|
||||
name: 'Common',
|
||||
freq: 1459,
|
||||
color: '#1ecc43',
|
||||
},
|
||||
];
|
||||
|
||||
export const Radio = props => {
|
||||
const { state } = props;
|
||||
const { config, data } = state;
|
||||
const { ref } = config;
|
||||
const {
|
||||
freqlock,
|
||||
frequency,
|
||||
minFrequency,
|
||||
maxFrequency,
|
||||
listening,
|
||||
broadcasting,
|
||||
command,
|
||||
useCommand,
|
||||
subspace,
|
||||
subspaceSwitchable,
|
||||
} = data;
|
||||
const tunedChannel = CHANNELS
|
||||
.find(channel => channel.freq === frequency);
|
||||
const channels = map((value, key) => ({
|
||||
name: key,
|
||||
status: !!value,
|
||||
}))(data.channels);
|
||||
return (
|
||||
<Section>
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Frequency">
|
||||
{freqlock && (
|
||||
<Box inline color="light-gray">
|
||||
{toFixed(frequency / 10, 1) + ' kHz'}
|
||||
</Box>
|
||||
) || (
|
||||
<NumberInput
|
||||
animate
|
||||
unit="kHz"
|
||||
step={0.2}
|
||||
stepPixelSize={10}
|
||||
minValue={minFrequency / 10}
|
||||
maxValue={maxFrequency / 10}
|
||||
value={frequency / 10}
|
||||
format={value => toFixed(value, 1)}
|
||||
onDrag={(e, value) => act(ref, 'frequency', {
|
||||
adjust: (value - frequency / 10),
|
||||
})} />
|
||||
)}
|
||||
{tunedChannel && (
|
||||
<Box inline color={tunedChannel.color} ml={2}>
|
||||
[{tunedChannel.name}]
|
||||
</Box>
|
||||
)}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Audio">
|
||||
<Button
|
||||
textAlign="center"
|
||||
width="37px"
|
||||
icon={listening ? 'volume-up' : 'volume-mute'}
|
||||
selected={listening}
|
||||
onClick={() => act(ref, 'listen')} />
|
||||
<Button
|
||||
textAlign="center"
|
||||
width="37px"
|
||||
icon={broadcasting ? 'microphone' : 'microphone-slash'}
|
||||
selected={broadcasting}
|
||||
onClick={() => act(ref, 'broadcast')} />
|
||||
{!!command && (
|
||||
<Button
|
||||
ml={1}
|
||||
icon="bullhorn"
|
||||
selected={useCommand}
|
||||
content={`High volume ${useCommand ? 'ON' : 'OFF'}`}
|
||||
onClick={() => act(ref, 'command')} />
|
||||
)}
|
||||
{!!subspaceSwitchable && (
|
||||
<Button
|
||||
ml={1}
|
||||
icon="bullhorn"
|
||||
selected={subspace}
|
||||
content={`Subspace Tx ${subspace ? 'ON' : 'OFF'}`}
|
||||
onClick={() => act(ref, 'subspace')} />
|
||||
)}
|
||||
</LabeledList.Item>
|
||||
{!!subspace && (
|
||||
<LabeledList.Item label="Channels">
|
||||
{channels.length === 0 && (
|
||||
<Box inline color="bad">
|
||||
No encryption keys installed.
|
||||
</Box>
|
||||
)}
|
||||
{channels.map(channel => (
|
||||
<Box key={channel.name}>
|
||||
<Button
|
||||
icon={channel.status ? 'check-square-o' : 'square-o'}
|
||||
selected={channel.status}
|
||||
content={channel.name}
|
||||
onClick={() => act(ref, 'channel', {
|
||||
channel: channel.name,
|
||||
})} />
|
||||
</Box>
|
||||
))}
|
||||
</LabeledList.Item>
|
||||
)}
|
||||
</LabeledList>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,119 @@
|
||||
import { toFixed } from 'common/math';
|
||||
import { Fragment } from 'inferno';
|
||||
import { act } from '../byond';
|
||||
import { Box, Button, Grid, LabeledList, NumberInput, ProgressBar, Section } from '../components';
|
||||
|
||||
export const SolarControl = props => {
|
||||
const { state } = props;
|
||||
const { config, data } = state;
|
||||
const { ref } = config;
|
||||
const {
|
||||
generated,
|
||||
angle,
|
||||
tracking_state,
|
||||
tracking_rate,
|
||||
connected_panels,
|
||||
connected_tracker,
|
||||
} = data;
|
||||
return (
|
||||
<Fragment>
|
||||
<Section
|
||||
title="Status"
|
||||
buttons={(
|
||||
<Button
|
||||
icon="sync"
|
||||
content="Scan for new hardware"
|
||||
onClick={() => act(ref, 'refresh')} />
|
||||
)}>
|
||||
<Grid>
|
||||
<Grid.Column>
|
||||
<LabeledList>
|
||||
<LabeledList.Item
|
||||
label="Solar tracker"
|
||||
color={connected_tracker ? 'good' : 'bad'}>
|
||||
{connected_tracker ? 'OK' : 'N/A'}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item
|
||||
label="Solar panels"
|
||||
color={connected_panels > 0 ? 'good' : 'bad'}>
|
||||
{connected_panels}
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Grid.Column>
|
||||
<Grid.Column size={1.5}>
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Power output">
|
||||
<ProgressBar
|
||||
ranges={{
|
||||
good: [60000, Infinity],
|
||||
average: [30000, 60000],
|
||||
bad: [-Infinity, 30000],
|
||||
}}
|
||||
minValue={0}
|
||||
maxValue={90000}
|
||||
value={generated}
|
||||
content={generated + ' W'} />
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
</Section>
|
||||
<Section title="Controls">
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Tracking">
|
||||
<Button
|
||||
icon="times"
|
||||
content="Off"
|
||||
selected={tracking_state === 0}
|
||||
onClick={() => act(ref, 'tracking', { mode: 0 })} />
|
||||
<Button
|
||||
icon="clock-o"
|
||||
content="Timed"
|
||||
selected={tracking_state === 1}
|
||||
onClick={() => act(ref, 'tracking', { mode: 1 })} />
|
||||
<Button
|
||||
icon="sync"
|
||||
content="Auto"
|
||||
selected={tracking_state === 2}
|
||||
disabled={!connected_tracker}
|
||||
onClick={() => act(ref, 'tracking', { mode: 2 })} />
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Angle">
|
||||
{(tracking_state === 0 || tracking_state === 1) && (
|
||||
<NumberInput
|
||||
width="52px"
|
||||
unit="°"
|
||||
step={1}
|
||||
stepPixelSize={2}
|
||||
minValue={-360}
|
||||
maxValue={+720}
|
||||
value={angle}
|
||||
format={angle => Math.round(360 + angle) % 360}
|
||||
onDrag={(e, value) => act(ref, 'angle', { value })} />
|
||||
)}
|
||||
{tracking_state === 1 && (
|
||||
<NumberInput
|
||||
width="80px"
|
||||
unit="°/h"
|
||||
step={5}
|
||||
stepPixelSize={2}
|
||||
minValue={-7200}
|
||||
maxValue={7200}
|
||||
value={tracking_rate}
|
||||
format={rate => {
|
||||
const sign = Math.sign(rate) > 0 ? '+' : '-';
|
||||
return sign + toFixed(Math.abs(rate));
|
||||
}}
|
||||
onDrag={(e, value) => act(ref, 'rate', { value })} />
|
||||
)}
|
||||
{tracking_state === 2 && (
|
||||
<Box inline color="label" mt="3px">
|
||||
{angle + ' °'} (auto)
|
||||
</Box>
|
||||
)}
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Section>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -21,26 +21,28 @@ import { ChemDispenser } from './interfaces/ChemDispenser';
|
||||
import { ChemFilter } from './interfaces/ChemFilter';
|
||||
import { ChemHeater } from './interfaces/ChemHeater';
|
||||
import { ChemMaster } from './interfaces/ChemMaster';
|
||||
import { ChemPress } from './interfaces/ChemPress';
|
||||
import { ChemSplitter } from './interfaces/ChemSplitter';
|
||||
import { ChemSynthesizer } from './interfaces/ChemSynthesizer';
|
||||
import { CodexGigas } from './interfaces/CodexGigas';
|
||||
import { Crayon } from './interfaces/Crayon';
|
||||
import { CrewConsole } from './interfaces/CrewConsole';
|
||||
import { Cryo } from './interfaces/Cryo';
|
||||
import { DisposalUnit } from './interfaces/DisposalUnit';
|
||||
import { KitchenSink } from './interfaces/KitchenSink';
|
||||
import { LanguageMenu } from './interfaces/LanguageMenu';
|
||||
import { Mint } from './interfaces/Mint';
|
||||
import { OperatingComputer } from './interfaces/OperatingComputer';
|
||||
import { OreRedemptionMachine } from './interfaces/OreRedemptionMachine';
|
||||
import { PersonalCrafting } from './interfaces/PersonalCrafting';
|
||||
import { PortableGenerator } from './interfaces/PortableGenerator';
|
||||
import { Radio } from './interfaces/Radio';
|
||||
import { ShuttleManipulator } from './interfaces/ShuttleManipulator';
|
||||
import { SmartVend } from './interfaces/SmartVend';
|
||||
import { SMES } from './interfaces/SMES';
|
||||
import { SolarControl } from './interfaces/SolarControl';
|
||||
import { ThermoMachine } from './interfaces/ThermoMachine';
|
||||
import { VaultController } from './interfaces/VaultController';
|
||||
import { Wires } from './interfaces/Wires';
|
||||
import { ChemSynthesizer } from './interfaces/ChemSynthesizer';
|
||||
import { ChemPress } from './interfaces/ChemPress';
|
||||
|
||||
const ROUTES = {
|
||||
achievements: {
|
||||
@@ -179,6 +181,10 @@ const ROUTES = {
|
||||
component: () => Mint,
|
||||
scrollable: false,
|
||||
},
|
||||
ore_redemption_machine: {
|
||||
component: () => OreRedemptionMachine,
|
||||
scrollable: true,
|
||||
},
|
||||
operating_computer: {
|
||||
component: () => OperatingComputer,
|
||||
scrollable: true,
|
||||
@@ -191,6 +197,10 @@ const ROUTES = {
|
||||
component: () => PortableGenerator,
|
||||
scrollable: false,
|
||||
},
|
||||
radio: {
|
||||
component: () => Radio,
|
||||
scrollable: false,
|
||||
},
|
||||
shuttle_manipulator: {
|
||||
component: () => ShuttleManipulator,
|
||||
scrollable: true,
|
||||
@@ -203,6 +213,10 @@ const ROUTES = {
|
||||
component: () => SMES,
|
||||
scrollable: false,
|
||||
},
|
||||
solar_control: {
|
||||
component: () => SolarControl,
|
||||
scrollable: false,
|
||||
},
|
||||
thermomachine: {
|
||||
component: () => ThermoMachine,
|
||||
scrollable: false,
|
||||
@@ -218,12 +232,15 @@ const ROUTES = {
|
||||
};
|
||||
|
||||
export const getRoute = state => {
|
||||
// Show a kitchen sink
|
||||
if (state.showKitchenSink) {
|
||||
return {
|
||||
component: () => KitchenSink,
|
||||
scrollable: true,
|
||||
};
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
// Show a kitchen sink
|
||||
if (state.showKitchenSink) {
|
||||
const { KitchenSink } = require('./interfaces/KitchenSink');
|
||||
return {
|
||||
component: () => KitchenSink,
|
||||
scrollable: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
// Refer to the routing table
|
||||
return ROUTES[state.config && state.config.interface];
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Components
|
||||
@import '../components/Button.scss';
|
||||
@import '../components/Flex.scss';
|
||||
@import '../components/Input.scss';
|
||||
@import '../components/LabeledList.scss';
|
||||
@import '../components/Layout.scss';
|
||||
@import '../components/NoticeBox.scss';
|
||||
|
||||
@@ -118,3 +118,6 @@ $color-damage-oxy: #3498db;
|
||||
$color-damage-toxin: #2ecc71;
|
||||
$color-damage-burn: #e67e22;
|
||||
$color-damage-brute: #e74c3c;
|
||||
|
||||
// Inputs
|
||||
$input-base-color: #88bfff;
|
||||
|
||||
Reference in New Issue
Block a user