Skip to content

Latest commit

 

History

History
 
 

tgui-next

tgui-next

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 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 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.

To get a clearer picture how to create a completely new interface from scratch, please refer to this tutorial document. 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.

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:

Pre-requisites

You will need these programs to start developing in tgui:

MSys2 closely replicates a unix-like environment which is necessary for the bin/tgui script to run. It comes with a robust "mintty" terminal emulator which is better than any standard Windows shell, it supports "git" out of the box (almost like Git for Windows, but better), has a "pacman" package manager, and you can install a text editor like "vim" for a full boomer experience.

Workflow

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 npm install, then:

  • npm run build - build the project in production mode.
  • npm run watch - launch a development server, with live log collection, cache reloading and hot module replacement.
  • npm run lint - show problems with the code.
  • npm run lint --fix - auto-fix problems with the code.
  • npm run analyze - run a bundle analyzer.

For MSys2, WSL, Linux or macOS users:

  • bin/tgui - build the project in production mode.
  • bin/tgui --dev - launch a development server, with live log collection, cache reloading and hot module replacement.
  • bin/tgui --dev --reload-once - reload byond cache once.
  • bin/tgui --dev --debug - run server with debug logging enabled.
  • bin/tgui --lint - show problems with the code.
  • bin/tgui --lint --fix - auto-fix problems with the code.
  • bin/tgui --analyze - run a bundle analyzer.
  • bin/tgui --clean - clean up project repo.
  • bin/tgui [webpack options] - build the project with custom webpack options.

For absolute brainlets, we also got a batch file in store. Double click it to build the project:

  • bin/tgui-build.bat - build the project in production mode.

Remember to always run a full build before submitting a PR. It creates a compressed javascript bundle which is then referenced from DM code. We prefer to keep it version controlled, so that people could build the game just by using Dream Maker.

Project structure

  • /packages - Each folder here represents a self-contained Node module.
  • /packages/common - Helper functions
  • /packages/tgui/index.js - Application entry point.
  • /packages/tgui/components - Basic UI building blocks.
  • /packages/tgui/interfaces - Actual in-game interfaces. Interface takes data via the state prop and outputs an html-like stucture, which you can build using existing UI components.
  • /packages/tgui/routes.js - This is where you want to register new interfaces, otherwise they simply won't load.
  • /packages/tgui/layout.js - A root-level component, holding the 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. 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 in UI components, and most of the stylesheets referenced here are located in /packages/tgui/components. These stylesheets closely follow the BEM methodology.
  • /packages/tgui/styles/functions.scss - Useful SASS functions. Stuff like lighten, darken, luminance are defined here.

Component reference

Notice: This documentation might be out of date, so always check the source code to see the most up-to-date information.

These are the components which you can use for interface construction. If you have trouble finding the exact prop you need on a component, please note, that most of these components inherit from other basic components, such as Box. This component in particular provides a lot of styling options for all components, e.g. color and opacity, thus 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.
    • 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.
    • You should never use children explicitly as a prop on an element.
  • Inferno supports both camelcase (onClick) and lowercase (onclick) event names.
    • Camel case names are what's called "synthetic" events, and are the preferred way of handling events in React, for efficiency and performance reasons. Please read Inferno Event Handling to understand what this is about.
    • 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 component straight up does not support lowercase event handlers. Use the camel case onClick instead.

AnimatedNumber

This component provides animations for numeric values.

Props:

  • value: number - Value to animate.
  • initial: number - Initial value to use in animation when element first appears. If you set initial to 0 for example, number will always animate starting from 0, and if omitted, it will not play an initial animation.
  • format: function - Output formatter.
    • Example: value => Math.round(value).

Box

The Box component serves as a wrapper component for most of the CSS utility needs. It creates a new DOM element, a <div> by default that can be changed with the as property. Let's say you want to use a <span> instead:

<Box as="span" m={1}>
  <Button />
</Box>

This works great when the changes can be isolated to a new DOM element. For instance, you can change the margin this way.

However, sometimes you have to target the underlying DOM element. For instance, you want to change the text color of the button. The Button component defines its own color. CSS inheritance doesn't help.

To workaround this problem, the Box children accept a render props function. This way, Button can pull out the className generated by the Box.

<Box color="primary">
  {props => <Button {...props} />}
</Box>

Box units, like width, height and margins can be defined in two ways:

  • By plain numbers (1 unit equals 0.5em);
  • In absolute measures, by providing a full unit string (e.g. 100px).

Units which are used in Box are 0.5em, which are half font-size. Default font size is 12px, so each unit is effectively 6px in size. If you need more precision, you can always use fractional numbers.

Props:

  • as: string - The component used for the root node.
  • color: string - Applies an atomic color-<name> class to the element.
    • See styles/atomic/color.scss.
  • width: number - Box width.
  • minWidth: number - Box minimum width.
  • maxWidth: number - Box maximum width.
  • height: number - Box height.
  • minHeight: number - Box minimum height.
  • maxHeight: number - Box maximum height.
  • lineHeight: number - Directly affects the height of text lines. Useful for adjusting button height.
  • inline: boolean - Forces the Box to appear as an inline-block, or in other words, makes the Box flow with the text instead of taking all available horizontal space.
  • m: number - Margin on all sides.
  • mx: number - Horizontal margin.
  • my: number - Vertical margin.
  • mt: number - Top margin.
  • mb: number - Bottom margin.
  • ml: number - Left margin.
  • mr: number - Right margin.
  • opacity: number - Opacity, from 0 to 1.
  • bold: boolean - Make text bold.
  • italic: boolean - Make text italic.
  • textAlign: string - Align text inside the box.
    • left (default)
    • center
    • right
  • position: string - A direct mapping to position CSS property.
    • 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.

Button

Buttons allow users to take actions, and make choices, with a single click.

Props:

  • See inherited props: Box
  • fluid: boolean - Tells the button to 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 transparent and slightly dim when inactive.
  • disabled: boolean - Disables and greys out the button.
  • selected: boolean - Activates the button (gives it a green color).
  • tooltip: string - A fancy, boxy tooltip, which appears when hovering over the button.
  • tooltipPosition: string - Position of the tooltip.
    • top - Show tooltip above the button.
    • bottom (default) - Show tooltip below the button.
    • left - Show tooltip on the left of the button.
    • right - Show tooltip on the right of the button.
  • title: string - A native browser tooltip, which appears when hovering over the button.
  • content/children: any - Content to render inside the button.
  • onClick: function - Called when element is clicked.

Flex

Quickly manage the layout, alignment, and sizing of grid columns, navigation, components, and more with a full suite of responsive flexbox utilities.

If you are new to or unfamiliar with flexbox, we encourage you to read this CSS-Tricks flexbox guide.

Consists of two elements: <Flex> and <Flex.Item>. Both of them provide the most straight-forward mapping to flex CSS properties as possible.

One of the most basic usage of flex, is to align certain elements to the left, and certain elements to the right:

<Flex>
  <Flex.Item>
    Button description
  </Flex.Item>
  <Flex.Item grow={1} />
  <Flex.Item>
    <Button content="Perform an action" />
  </Flex.Item>
</Flex>

Flex item with grow property serves as a "filler", to separate the other two flex items as far as possible from each other.

Props:

  • See inherited props: 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.
    • row-reverse - right to left.
    • column - top to bottom.
    • column-reverse - bottom to top.
  • wrap: string - By default, flex items will all try to fit onto one line. You can change that and allow the items to wrap as needed with this property.
    • nowrap (default) - all flex items will be on one line
    • wrap - flex items will wrap onto multiple lines, from top to bottom.
    • wrap-reverse - flex items will wrap onto multiple lines from bottom to top.
  • align: string - Default alignment of all children.
    • stretch (default) - stretch to fill the container.
    • start - items are placed at the start of the cross axis.
    • end - items are placed at the end of the cross axis.
    • center - items are centered on the cross axis.
    • baseline - items are aligned such as their baselines align.
  • justify: string - This defines the alignment along the main axis. It helps distribute extra free space leftover when either all the flex items on a line are inflexible, or are flexible but have reached their maximum size. It also exerts some control over the alignment of items when they overflow the line.
    • flex-start (default) - items are packed toward the start of the flex-direction.
    • flex-end - items are packed toward the end of the flex-direction.
    • space-between - items are evenly distributed in the line; first item is on the start line, last item on the end line
    • space-around - items are evenly distributed in the line with equal space around them. Note that visually the spaces aren't equal, since all the items have equal space on both sides. The first item will have one unit of space against the container edge, but two units of space between the next item because that next item has its own spacing that applies.
    • space-evenly - items are distributed so that the spacing between any two items (and the space to the edges) is equal.
    • TBD (not all properties are supported in IE11).

Flex.Item

Props:

  • See inherited props: Box
  • order: number - By default, flex items are laid out in the source order. However, the order property controls the order in which they appear in the flex container.
  • grow: number - This defines the ability for a flex item to grow if necessary. It accepts a unitless value that serves as a proportion. It dictates what amount of the available space inside the flex container the 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.), 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.

Icon

Renders one of the FontAwesome icons of your choice.

<Icon name="plus" />

To smoothen the transition from v4 to v5, we have added a v4 semantic to transform names with -o suffixes to FA Regular icons. For example:

  • square will get transformed to fas square
  • square-o will get transformed to far square

Props:

  • See inherited props: Box
  • name: string - Icon name.
  • size: number - Icon size. 1 is normal size, 2 is two times bigger. Fractional numbers are supported.

LabeledList

LabeledList is a continuous, vertical list of text and other content, where every item is labeled. It works just like a two column table, where first column is labels, and second column is content.

<LabeledList>
  <LabeledList.Item label="Item">
    Content
  </LabeledList.Item>
</LabeledList>

If you want to have a button on the right side of an item (for example, to perform some sort of action), there is a way to do that:

<LabeledList>
  <LabeledList.Item
    label="Item"
    buttons={(
      <Button content="Click me!" />
    )}>
    Content
  </LabeledList.Item>
</LabeledList>

Props:

  • children: LabeledList.Item - Items to render.

LabeledList.Item

Props:

  • label: string - Item label.
  • color: string - Sets the color of the text.
  • buttons: any - Buttons to render aside the content.
  • content/children: any - Content of this labeled item.

LabeledList.Divider

Adds some empty space between LabeledList items.

Example:

<LabeledList>
  <LabeledList.Item label="Foo">
    Content
  </LabeledList.Item>
  <LabeledList.Divider size={1} />
</LabeledList>

Props:

  • size: number - Size of the divider.

ProgressBar

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.
  • color: string - Color of the progress bar.
  • content/children: any - Content to render inside the progress bar.

Section

Section is a surface that displays content and actions on a single topic.

They should be easy to scan for relevant and actionable information. Elements, like text and images, should be placed in them in a way that clearly indicates hierarchy.

Section can also be titled to clearly define its purpose.

<Section title="Cargo">
  Here you can order supply crates.
</Section>

If you want to have a button on the right side of an section title (for example, to perform some sort of action), there is a way to do that:

<Section
  title="Cargo"
  buttons={(
    <Button content="Send shuttle" />
  )}>
  Here you can order supply crates.
</Section>
  • See inherited props: Box
  • title: string - Title of the section.
  • level: number - Section level in hierarchy. Default is 1, higher number 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.

Tabs

Tabs make it easy to explore and switch between different views.

Here is an example of how you would construct a simple tabbed view:

<Tabs>
  <Tabs.Tab label="Item one">
    Content for Item one.
  </Tabs.Tab>
  <Tabs.Tab label="Item two">
    Content for Item two.
  </Tabs.Tab>
</Tabs>

This is a rather simple example. In the real world, you might be constructing very complex tabbed views which can tax UI performance. This is because your tabs are being rendered regardless of their visibility status!

There is a simple fix however. Tabs accept functions as children, which will be called to retrieve content only when the tab is visible:

<Tabs>
  <Tabs.Tab key="tab_1" label="Item one">
    {() => (
      <Fragment>
        Content for Item one.
      </Fragment>
    )}
  </Tabs.Tab>
  <Tabs.Tab key="tab_2" label="Item two">
    {() => (
      <Fragment>
        Content for Item two.
      </Fragment>
    )}
  </Tabs.Tab>
</Tabs>

You might not always need this, but it is highly recommended to always use this method. Notice the key prop on tabs - it uniquely identifies the tab and is used for determining which tab is currently active. It can be either explicitly provided as a key prop, or if omitted, it will be implicitly derived from the tab's label prop.

Props:

  • vertical: boolean - Use a vertical configuration, where tabs will appear stacked on the left side of the container.
  • children: Tab[] - This component only accepts tabs as its children.

Tabs.Tab

An individual tab element. Tabs function like buttons, so they inherit a lot of Button props.

Props:

  • See inherited props: Button
  • key: string - A unique identifier for the tab.
  • label: string - Tab label.
  • icon: string - Tab icon.
  • content/children: any - Content to render inside the tab.
  • onClick: function - Called when element is clicked.

Tooltip

A boxy tooltip from tgui 1. It is very hacky in its current state, and requires setting position: relative on the container.

Please note, that Button component has a tooltip prop, and it is recommended to use that prop instead.

Usage:

<Box position="relative">
  Sample text.
  <Tooltip
    position="bottom"
    content="Box tooltip" />
</Box>

Props:

  • position: string - Tooltip position.
  • content/children: string - Content of the tooltip. Must be a plain string. Fragments or other elements are not supported.