building pipeline

This commit is contained in:
Letter N
2021-03-04 21:51:20 +08:00
parent 23c04cac12
commit dcab40cdb6
134 changed files with 10776 additions and 5991 deletions
+19 -13
View File
@@ -10,7 +10,7 @@ jobs:
run_linters:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
name: Run Linters
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Setup cache
@@ -21,18 +21,20 @@ jobs:
- name: Install Tools
run: |
pip3 install setuptools
bash tools/ci/install_build_tools.sh
bash tools/ci/install_node.sh
bash tools/ci/install_spaceman_dmm.sh dreamchecker
pip3 install -r tools/requirements.txt
tools/bootstrap/python -c ''
- name: Run Linters
run: |
bash tools/ci/check_filedirs.sh tgstation.dme
bash tools/ci/check_changelogs.sh
find . -name "*.php" -print0 | xargs -0 -n1 php -l
find . -name "*.json" -not -path "*/node_modules/*" -print0 | xargs -0 python3 ./tools/json_verifier.py
bash tools/ci/build_tgui.sh
tgui/bin/tgui --lint
tgui/bin/tgui --test
bash tools/ci/check_grep.sh
tools/bootstrap/python -m dmi.test
tools/bootstrap/python -m mapmerge2.dmm_test
~/dreamchecker > ${GITHUB_WORKSPACE}/output-annotations.txt 2>&1
- name: Annotate Lints
uses: yogstation13/DreamAnnotate@v1
@@ -43,7 +45,7 @@ jobs:
compile_all_maps:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
name: Compile Maps
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Setup cache
@@ -56,11 +58,13 @@ jobs:
bash tools/ci/install_byond.sh
source $HOME/BYOND/byond/bin/byondsetup
python3 tools/ci/template_dm_generator.py
tgui/bin/tgui --build
bash tools/ci/dm.sh -DCIBUILDING -DCITESTING -DALL_MAPS tgstation.dme
run_all_tests:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
name: Integration Tests
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
services:
mysql:
image: mysql:latest
@@ -87,14 +91,16 @@ jobs:
run: |
sudo dpkg --add-architecture i386
sudo apt update || true
sudo apt install libssl1.1:i386
sudo apt install -o APT::Immediate-Configure=false libssl1.1:i386
bash tools/ci/install_rust_g.sh
# - name: Compile and run tests
# run: |
# bash tools/ci/install_byond.sh
# source $HOME/BYOND/byond/bin/byondsetup
# bash tools/ci/dm.sh -DCIBUILDING tgstation.dme
# bash tools/ci/run_server.sh
- name: Compile and run tests
run: |
bash tools/ci/install_byond.sh
source $HOME/BYOND/byond/bin/byondsetup
tgui/bin/tgui --build
# bash tools/ci/dm.sh -DCIBUILDING tgstation.dme
# bash tools/ci/run_server.sh
test_windows:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
name: Windows Build
+1 -1
View File
@@ -7,7 +7,7 @@ on:
jobs:
compile:
name: "Compile changelogs"
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: "Check for CHANGELOG_ENABLER secret and pass true to output if it exists to be checked by later steps"
id: value_holder
+1 -1
View File
@@ -8,7 +8,7 @@ on:
jobs:
publish:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
+1 -1
View File
@@ -6,7 +6,7 @@ on:
jobs:
generate_documentation:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Setup cache
+1 -1
View File
@@ -5,7 +5,7 @@ on:
jobs:
link_rounds:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: Cidatel-Station-13/round_linker@master #notice: fork the round linkies from tg!!
with:
+1 -1
View File
@@ -7,7 +7,7 @@ on:
jobs:
update-dmapi:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
name: Update the TGS DMAPI
steps:
- name: Clone
+1 -1
View File
@@ -1218,7 +1218,7 @@
. |= SEND_SIGNAL(src, COMSIG_ATOM_INTERCEPT_Z_FALL, AM, levels)
///Sets the custom materials for an item.
/atom/proc/set_custom_materials(var/list/materials, multiplier = 1)
/atom/proc/set_custom_materials(list/materials, multiplier = 1)
if(custom_materials) //Only runs if custom materials existed at first. Should usually be the case but check anyways
for(var/i in custom_materials)
var/datum/material/custom_material = SSmaterials.GetMaterialRef(i)
+1 -1
View File
@@ -1,6 +1,6 @@
/.yarn/**
/**/node_modules
/**/*.bundle.*
/**/*.chunk.*
/**/*.hot-update.*
/packages/inferno/**
/packages/tgui/public/shim-*.js
+1
View File
@@ -12,3 +12,4 @@ rules:
selfClosing: after-props,
nonEmpty: after-props,
}]
react/display-name: error
+6 -4
View File
@@ -1,6 +1,7 @@
parser: babel-eslint
root: true
parser: '@typescript-eslint/parser'
parserOptions:
ecmaVersion: 2019
ecmaVersion: 2020
sourceType: module
ecmaFeatures:
jsx: true
@@ -279,7 +280,8 @@ rules:
no-shadow-restricted-names: error
## Disallow the use of undeclared variables unless mentioned
## in /*global*/ comments
no-undef: error
## NOTE: Pointless when TypeScript can check for this
# no-undef: error
## Disallow initializing variables to undefined
no-undef-init: error
## Disallow the use of undefined as an identifier
@@ -583,7 +585,7 @@ rules:
## Rule enforces consistent usage of destructuring assignment in component
# react/destructuring-assignment: [error, always, { ignoreClassFields: true }]
## Prevent missing displayName in a React component definition
react/display-name: error
# react/display-name: error
## Forbid certain props on Components
# react/forbid-component-props: error
## Forbid certain props on DOM Nodes
+2 -1
View File
@@ -18,4 +18,5 @@ bin/tgui text eol=lf
## Treat bundles as binary and ignore them during conflicts
*.bundle.* binary merge=tgui-merge-bundle
*.chunk.* binary merge=tgui-merge-bundle
.yarn/releases/* binary
.yarn/releases/**/* binary
.yarn/plugins/**/* binary
+4 -5
View File
@@ -10,14 +10,13 @@ package-lock.json
!/.yarn/plugins
!/.yarn/sdks
!/.yarn/versions
!/.yarn/lock.yml
## Build artifacts
/public/.tmp/**/*
/public/**/*.hot-update.*
/public/**/*.map
/public/**/*
!/public/*.html
## Previously ignored locations that are kept to avoid confusing git
## while transitioning to a new project structure.
/packages/tgui/public/.tmp/**/*
/packages/tgui/public/**/*.hot-update.*
/packages/tgui/public/**/*.map
/packages/tgui/public/**
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -2,7 +2,7 @@
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve, dirname} = require(`path`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.js";
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "eslint",
"version": "7.4.0-pnpify",
"version": "7.19.0-pnpify",
"main": "./lib/api.js",
"type": "commonjs"
}
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.js";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require typescript/bin/tsc
require(absPnpApiPath).setup();
}
}
// Defer to the real typescript/bin/tsc your application uses
module.exports = absRequire(`typescript/bin/tsc`);
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.js";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require typescript/bin/tsserver
require(absPnpApiPath).setup();
}
}
// Defer to the real typescript/bin/tsserver your application uses
module.exports = absRequire(`typescript/bin/tsserver`);
+6
View File
@@ -0,0 +1,6 @@
{
"name": "typescript",
"version": "4.1.5-pnpify",
"main": "./lib/typescript.js",
"type": "commonjs"
}
+21 -1
View File
@@ -1 +1,21 @@
yarnPath: .yarn/releases/yarn-2.3.3.cjs
enableScripts: false
logFilters:
## DISABLED_BUILD_SCRIPTS
- code: YN0004
level: discard
## INCOMPATIBLE_OS - fsevents junk
- code: YN0062
level: discard
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
spec: "@yarnpkg/plugin-workspace-tools"
preferAggregateCacheInfo: true
preferInteractive: true
yarnPath: .yarn/releases/yarn-2.4.0.cjs
+12
View File
@@ -87,6 +87,7 @@ Run `.\bin\tgui.bat` with any of the options listed below.
doing development on IE8).
- `bin/tgui --lint` - Show problems with the code.
- `bin/tgui --fix` - Auto-fix problems with the code.
- `bin/tgui --test` - Run tests.
- `bin/tgui --analyze` - Run a bundle analyzer.
- `bin/tgui --clean` - Clean up project repo.
- `bin/tgui [webpack options]` - Build the project with custom webpack
@@ -126,6 +127,17 @@ variable, with a full path to BYOND cache.
BYOND_CACHE="E:/Libraries/Documents/BYOND/cache"
```
**Webpack errors out with some cryptic messages!**
> Example: `No template for dependency: PureExpressionDependency`
Webpack stores its cache on disk since tgui 4.3, and it is very sensitive
to build configuration. So if you update webpack, or share the same cache
directory between development and production build, it will start
hallucinating.
To fix this kind of problem, run `bin/tgui --clean` and try again.
## Developer Tools
When developing with `tgui-dev-server`, you will have access to certain
+43
View File
@@ -0,0 +1,43 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
const createBabelConfig = options => {
const { mode, presets = [], plugins = [] } = options;
return {
presets: [
['@babel/preset-typescript', {
allowDeclareFields: true,
}],
['@babel/preset-env', {
modules: 'commonjs',
useBuiltIns: 'entry',
corejs: '3.8',
spec: false,
loose: true,
targets: [],
}],
...presets,
],
plugins: [
['@babel/plugin-proposal-class-properties', {
loose: true,
}],
'@babel/plugin-transform-jscript',
'babel-plugin-inferno',
'babel-plugin-transform-remove-console',
'common/string.babel-plugin.cjs',
...plugins,
],
};
};
module.exports = api => {
api.cache(true);
const mode = process.env.NODE_ENV;
return createBabelConfig({ mode });
};
module.exports.createBabelConfig = createBabelConfig;
+40 -30
View File
@@ -26,9 +26,18 @@ fi
base_dir="$(dirname "$(tgui-realpath "${0}")")/.."
base_dir="$(tgui-realpath "${base_dir}")"
## Make use of nvm if it exists
if [[ -e "${HOME}/.nvm/nvm.sh" ]]; then
source "${HOME}/.nvm/nvm.sh"
fi
## Fall back to running Yarn from the repo
if ! hash yarn 2>/dev/null; then
alias yarn="node '${base_dir}/.yarn/releases/yarn-2.3.3.cjs'"
yarn_releases=("${base_dir}"/.yarn/releases/yarn-*.cjs)
yarn_release="${yarn_releases[0]}"
yarn() {
node "${yarn_release}" "${@}"
}
fi
@@ -54,22 +63,32 @@ task-dev-server() {
}
## Run a linter through all packages
task-eslint() {
task-lint() {
cd "${base_dir}"
yarn run eslint packages "${@}"
yarn run tsc
echo "tgui: type check passed"
yarn run eslint packages --ext .js,.jsx,.ts,.tsx,.cjs,.mjs "${@}"
echo "tgui: eslint check passed"
}
task-test() {
cd "${base_dir}"
yarn run jest
}
## Mr. Proper
task-clean() {
cd "${base_dir}"
## Build artifacts
rm -rf public/.tmp
rm -f public/*.map
rm -f public/*.chunk.*
rm -f public/*.bundle.*
rm -f public/*.hot-update.*
## Yarn artifacts
rm -rf .yarn/cache
rm -rf .yarn/unplugged
rm -rf .yarn/webpack
rm -rf .yarn/build-state.yml
rm -rf .yarn/install-state.gz
rm -f .pnp.js
@@ -78,19 +97,6 @@ task-clean() {
rm -f **/package-lock.json
}
## Validates current build against the build stored in git
task-validate-build() {
cd "${base_dir}"
local diff
diff="$(git diff public/*)"
if [[ -n ${diff} ]]; then
echo "Error: our build differs from the build committed into git."
echo "Please rebuild tgui."
exit 1
fi
echo "tgui: build is ok"
}
## Installs merge drivers and git hooks
task-install-git-hooks() {
cd "${base_dir}"
@@ -133,16 +139,6 @@ if [[ ${1} == "--install-git-hooks" ]]; then
exit 0
fi
## Continuous integration scenario
if [[ ${1} == "--ci" ]]; then
task-clean
task-install
task-eslint
task-webpack --mode=production
task-validate-build
exit 0
fi
if [[ ${1} == "--clean" ]]; then
task-clean
exit 0
@@ -158,21 +154,28 @@ fi
if [[ ${1} == '--lint' ]]; then
shift 1
task-install
task-eslint "${@}"
task-lint "${@}"
exit 0
fi
if [[ ${1} == '--lint-harder' ]]; then
shift 1
task-install
task-eslint -c .eslintrc-harder.yml "${@}"
task-lint -c .eslintrc-harder.yml "${@}"
exit 0
fi
if [[ ${1} == '--fix' ]]; then
shift 1
task-install
task-eslint --fix "${@}"
task-lint --fix "${@}"
exit 0
fi
if [[ ${1} == '--test' ]]; then
shift 1
task-install
task-test "${@}"
exit 0
fi
@@ -184,9 +187,16 @@ if [[ ${1} == '--analyze' ]]; then
fi
## Make a production webpack build
if [[ ${1} == '--build' ]]; then
task-install
task-webpack --mode=production
exit 0
fi
## Make a production webpack build + Run eslint
if [[ -z ${1} ]]; then
task-install
task-eslint
task-lint --fix
task-webpack --mode=production
exit 0
fi
+1 -1
View File
@@ -1,7 +1,7 @@
@echo off
rem Copyright (c) 2020 Aleksej Komarov
rem SPDX-License-Identifier: MIT
call powershell.exe -NoLogo -ExecutionPolicy Bypass -File "%~dp0\tgui.ps1" --dev %*
call powershell.exe -NoLogo -ExecutionPolicy Bypass -File "%~dp0\tgui_.ps1" --dev %*
rem Pause if launched in a separate shell unless initiated from powershell
echo %PSModulePath% | findstr %USERPROFILE% >NUL
if %errorlevel% equ 0 exit 0
+1 -1
View File
@@ -1,7 +1,7 @@
@echo off
rem Copyright (c) 2020 Aleksej Komarov
rem SPDX-License-Identifier: MIT
call powershell.exe -NoLogo -ExecutionPolicy Bypass -File "%~dp0\tgui.ps1" %*
call powershell.exe -NoLogo -ExecutionPolicy Bypass -File "%~dp0\tgui_.ps1" %*
rem Pause if launched in a separate shell unless initiated from powershell
echo %PSModulePath% | findstr %USERPROFILE% >NUL
if %errorlevel% equ 0 exit 0
+67 -37
View File
@@ -4,6 +4,18 @@
## Initial set-up
## --------------------------------------------------------
## Enable strict mode and stop of first cmdlet error
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$PSDefaultParameterValues['*:ErrorAction'] = 'Stop'
## Validates exit code of external commands
function Throw-On-Native-Failure {
if (-not $?) {
exit 1
}
}
## Normalize current directory
$basedir = Split-Path $MyInvocation.MyCommand.Path
$basedir = Resolve-Path "$($basedir)\.."
@@ -15,7 +27,9 @@ Set-Location $basedir
## --------------------------------------------------------
function yarn {
node.exe ".yarn\releases\yarn-2.3.3.cjs" @Args
$YarnRelease = Get-ChildItem -Filter ".yarn\releases\yarn-*.cjs" | Select-Object -First 1
node ".yarn\releases\$YarnRelease" @Args
Throw-On-Native-Failure
}
function Remove-Quiet {
@@ -37,11 +51,17 @@ function task-dev-server {
}
## Run a linter through all packages
function task-eslint {
yarn run eslint packages @Args
function task-lint {
yarn run tsc
Write-Output "tgui: type check passed"
yarn run eslint packages --ext .js,.jsx,.ts,.tsx,.cjs,.mjs @Args
Write-Output "tgui: eslint check passed"
}
function task-test {
yarn run jest
}
## Mr. Proper
function task-clean {
## Build artifacts
@@ -51,6 +71,7 @@ function task-clean {
## Yarn artifacts
Remove-Quiet -Recurse -Force ".yarn\cache"
Remove-Quiet -Recurse -Force ".yarn\unplugged"
Remove-Quiet -Recurse -Force ".yarn\webpack"
Remove-Quiet -Recurse -Force ".yarn\build-state.yml"
Remove-Quiet -Recurse -Force ".yarn\install-state.gz"
Remove-Quiet -Force ".pnp.js"
@@ -63,50 +84,59 @@ function task-clean {
## Main
## --------------------------------------------------------
if ($Args[0] -eq "--clean") {
task-clean
exit 0
}
if ($Args.Length -gt 0) {
if ($Args[0] -eq "--clean") {
task-clean
exit 0
}
if ($Args[0] -eq "--dev") {
$Rest = $Args | Select-Object -Skip 1
task-install
task-dev-server @Rest
exit 0
}
if ($Args[0] -eq "--dev") {
$Rest = $Args | Select-Object -Skip 1
task-install
task-dev-server @Rest
exit 0
}
if ($Args[0] -eq "--lint") {
$Rest = $Args | Select-Object -Skip 1
task-install
task-eslint @Rest
exit 0
}
if ($Args[0] -eq "--lint") {
$Rest = $Args | Select-Object -Skip 1
task-install
task-lint @Rest
exit 0
}
if ($Args[0] -eq "--lint-harder") {
$Rest = $Args | Select-Object -Skip 1
task-install
task-eslint -c ".eslintrc-harder.yml" @Rest
exit 0
}
if ($Args[0] -eq "--lint-harder") {
$Rest = $Args | Select-Object -Skip 1
task-install
task-lint -c ".eslintrc-harder.yml" @Rest
exit 0
}
if ($Args[0] -eq "--fix") {
$Rest = $Args | Select-Object -Skip 1
task-install
task-eslint --fix @Rest
exit 0
}
if ($Args[0] -eq "--fix") {
$Rest = $Args | Select-Object -Skip 1
task-install
task-lint --fix @Rest
exit 0
}
## Analyze the bundle
if ($Args[0] -eq "--analyze") {
task-install
task-webpack --mode=production --analyze
exit 0
if ($Args[0] -eq "--test") {
$Rest = $Args | Select-Object -Skip 1
task-install
task-test @Rest
exit 0
}
## Analyze the bundle
if ($Args[0] -eq "--analyze") {
task-install
task-webpack --mode=production --analyze
exit 0
}
}
## Make a production webpack build
if ($Args.Length -eq 0) {
task-install
task-eslint
task-lint
task-webpack --mode=production
exit 0
}
+115 -29
View File
@@ -43,6 +43,7 @@ Make sure to add new items to this list if you document new components.
- [`RoundGauge`](#roundgauge)
- [`Section`](#section)
- [`Slider`](#slider)
- [`Stack`](#stack)
- [`Table`](#table)
- [`Table.Row`](#tablerow)
- [`Table.Cell`](#tablecell)
@@ -390,10 +391,9 @@ to the left, and certain elements to the right:
```jsx
<Flex>
<Flex.Item>
<Flex.Item grow={1}>
Button description
</Flex.Item>
<Flex.Item grow={1} />
<Flex.Item>
<Button>
Perform an action
@@ -402,16 +402,15 @@ to the left, and certain elements to the right:
</Flex>
```
Flex item with `grow` property serves as a "filler", to separate the other
two flex items as far as possible from each other.
Flex item with `grow` property will grow to take all available empty space,
while flex items without grow will take the minimum amount of space. This
effectively places the last flex item to the very end of the flex container.
**Props:**
- See inherited props: [Box](#box)
- `spacing: number` - Spacing between flex items, in integer units
(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.
- ~~`spacing: number`~~ - **Removed in tgui 4.3**,
use [Stack](#stack) instead.
- `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
@@ -458,16 +457,16 @@ when they overflow the line.
- `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
- `grow: number | boolean` - 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 any
flex-related calculations are done. Has to be a length (e.g. `20%`, `5rem`),
an `auto` or `content` keyword.
- `shrink: number | boolean` - This defines the ability for a flex item to
shrink if necessary. Inverse of `grow`.
- `basis: number | 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).
@@ -839,10 +838,12 @@ If you want to have a button on the right side of an section title
</Section>
```
**New:** Sections can now be nested, and will automatically font size of the
header according to their nesting level. Previously this was done via `level`
prop, but now it is automatically calculated.
- See inherited props: [Box](#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.
- `fill: boolean` - If true, fills all available vertical space.
- `fitted: boolean` - If true, removes all section padding.
@@ -884,6 +885,76 @@ 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.
### `Stack`
A higher-level component, that is based on [Flex](#flex). The main difference
from `Flex`, is that this component automatically adds spacing between
all stack items, reducing the boilerplate that you have to write!
Consists of two elements: `<Stack>` and `<Stack.Item>`.
Stacks can be vertical by adding a `vertical` property.
**Example:**
```jsx
<Stack>
<Stack.Item grow>
Button description
</Stack.Item>
<Stack.Item>
<Button>
Perform an action
</Button>
</Stack.Item>
</Stack>
```
**Example of a high-level window layout:**
Stacks can be used for high level window layout.
Make sure to use the `fill` property.
```jsx
<Window>
<Window.Content>
<Stack fill>
<Stack.Item>
<Section fill>
Sidebar
</Section>
</Stack.Item>
<Stack.Item grow>
<Stack fill vertical>
<Stack.Item grow>
<Section fill scrollable>
Main content
</Section>
</Stack.Item>
<Stack.Item>
<Section>
Bottom pane
</Section>
</Stack.Item>
</Stack>
</Stack.Item>
</Stack>
</Window.Content>
</Window>
```
**Props:**
- See inherited props: [Flex](#flex)
- `fill: boolean` - If set, stack will fill all available height.
- `vertical: boolean` - If set, stack will work in vertical mode.
### `Stack.Item`
**Props:**
- See inherited props: [Flex.Item](#flexitem)
### `Table`
A straight forward mapping to a standard html table, which is slightly
@@ -956,25 +1027,41 @@ Notice that tabs do not contain state. It is your job to track the selected
tab, handle clicks and place tab content where you need it. In return, you get
a lot of flexibility in regards to how you can layout your tabs.
Tabs also support a vertical configuration. This is usually paired with a
[Flex](#flex) component to render tab content to the right.
Tabs also support a vertical configuration. This is usually paired with
[Stack](#stack) to render tab content to the right.
```jsx
<Flex>
<Flex.Item>
<Stack>
<Stack.Item>
<Tabs vertical>
...
</Tabs>
</Flex.Item>
<Flex.Item grow={1} basis={0}>
</Stack.Item>
<Stack.Item grow={1} basis={0}>
Tab content.
</Flex.Item>
</Flex>
</Stack.Item>
</Stack>
```
If you need to combine a tab section with other elements, or if you want to
add scrollable functionality to tabs, pair them with the [Section](#section)
component:
```jsx
<Section fill fitted scrollable width="128px">
<Tabs vertical>
...
</Tabs>
... other things ...
</Section>
```
**Props:**
- See inherited props: [Box](#box)
- `fluid: boolean` - If true, tabs will take all available horizontal space.
- `fill: boolean` - Similarly to `fill` on [Section](#section), tabs will fill
all available vertical space. Only makes sense in a vertical configuration.
- `vertical: boolean` - Use a vertical configuration, where tabs will be
stacked vertically.
- `children: Tab[]` - This component only accepts tabs as its children.
@@ -1030,9 +1117,7 @@ it in one way or another.
Example:
```jsx
<Window
theme="hackerman"
resizable>
<Window theme="hackerman">
<Window.Content scrollable>
Hello, world!
</Window.Content>
@@ -1046,7 +1131,8 @@ Example:
- `theme: string` - A name of the theme.
- For a list of themes, see `packages/tgui/styles/themes`.
- `title: string` - Window title.
- `resizable: boolean` - Controls resizability of the window.
- `width: number` - Window width.
- `height: number` - Window height.
- `noClose: boolean` - Controls the ability to close the window.
- `children: any` - Child elements, which are rendered directly inside the
window. If you use a [Dimmer](#dimmer) or [Modal](#modal) in your UI,
+23
View File
@@ -0,0 +1,23 @@
## Jest
You can now write and run unit tests in tgui.
It's quite simple: create a file ending in `.test.ts` or `.spec.ts` (usually with the same filename as the file you're testing), and create a test case:
```js
test('something', () => {
expect('a').toBe('a');
});
```
To run the tests, type the following into the terminal:
```
bin/tgui --test
```
There is an example test in `packages/common/react.spec.ts`.
You can read more about Jest here: https://jestjs.io/docs/en/getting-started
Note, that there is still no real solution to test UIs for now, even though a lot of the support is here (jest + jsdom). That will come later.
+134 -110
View File
@@ -1,117 +1,141 @@
interface ByondType {
/**
* True if javascript is running in BYOND.
*/
IS_BYOND: boolean;
/**
* @file
* @copyright 2021 Aleksej Komarov
* @license MIT
*/
declare global {
type ByondType = {
/**
* True if javascript is running in BYOND.
*/
IS_BYOND: boolean;
/**
* True if browser is IE8 or lower.
*/
IS_LTE_IE8: boolean;
/**
* True if browser is IE9 or lower.
*/
IS_LTE_IE9: boolean;
/**
* True if browser is IE10 or lower.
*/
IS_LTE_IE10: boolean;
/**
* True if browser is IE11 or lower.
*/
IS_LTE_IE11: boolean;
/**
* Makes a BYOND call.
*
* If path is empty, this will trigger a Topic call.
* You can reference a specific object by setting the "src" parameter.
*
* See: https://secure.byond.com/docs/ref/skinparams.html
*/
call(path: string, params: object): void;
/**
* Makes an asynchronous BYOND call. Returns a promise.
*/
callAsync(path: string, params: object): Promise<any>;
/**
* Makes a Topic call.
*
* You can reference a specific object by setting the "src" parameter.
*/
topic(params: object): void;
/**
* Runs a command or a verb.
*/
command(command: string): void;
/**
* Retrieves all properties of the BYOND skin element.
*
* Returns a promise with a key-value object containing all properties.
*/
winget(id: string): Promise<object>;
/**
* Retrieves all properties of the BYOND skin element.
*
* Returns a promise with a key-value object containing all properties.
*/
winget(id: string, propName: '*'): Promise<object>;
/**
* Retrieves an exactly one property of the BYOND skin element,
* as defined in `propName`.
*
* Returns a promise with the value of that property.
*/
winget(id: string, propName: string): Promise<any>;
/**
* Retrieves multiple properties of the BYOND skin element,
* as defined in the `propNames` array.
*
* Returns a promise with a key-value object containing listed properties.
*/
winget(id: string, propNames: string[]): Promise<object>;
/**
* Assigns properties to BYOND skin elements.
*/
winset(props: object): void;
/**
* Assigns properties to the BYOND skin element.
*/
winset(id: string, props: object): void;
/**
* Sets a property on the BYOND skin element to a certain value.
*/
winset(id: string, propName: string, propValue: any): void;
/**
* Parses BYOND JSON.
*
* Uses a special encoding to preverse Infinity and NaN.
*/
parseJson(text: string): any;
/**
* Loads a stylesheet into the document.
*/
loadCss(url: string): void;
/**
* Loads a script into the document.
*/
loadJs(url: string): void;
};
/**
* True if browser is IE8 or lower.
* Object that provides access to Byond Skin API and is available in
* any tgui application.
*/
IS_LTE_IE8: boolean;
const Byond: ByondType;
/**
* True if browser is IE9 or lower.
*/
IS_LTE_IE9: boolean;
interface Window {
/**
* ID of the Byond window this script is running on.
* Should be used as a parameter to winget/winset.
*/
__windowId__: string;
Byond: ByondType;
}
/**
* True if browser is IE10 or lower.
*/
IS_LTE_IE10: boolean;
/**
* True if browser is IE11 or lower.
*/
IS_LTE_IE11: boolean;
/**
* Makes a BYOND call.
*
* If path is empty, this will trigger a Topic call.
* You can reference a specific object by setting the "src" parameter.
*
* See: https://secure.byond.com/docs/ref/skinparams.html
*/
call(path: string, params: object): void;
/**
* Makes an asynchronous BYOND call. Returns a promise.
*/
callAsync(path: string, params: object): Promise<any>;
/**
* Makes a Topic call.
*
* You can reference a specific object by setting the "src" parameter.
*/
topic(params: object): void;
/**
* Runs a command or a verb.
*/
command(command: string): void;
/**
* Retrieves all properties of the BYOND skin element.
*
* Returns a promise with a key-value object containing all properties.
*/
winget(id: string): Promise<object>;
/**
* Retrieves all properties of the BYOND skin element.
*
* Returns a promise with a key-value object containing all properties.
*/
winget(id: string, propName: '*'): Promise<object>;
/**
* Retrieves an exactly one property of the BYOND skin element,
* as defined in `propName`.
*
* Returns a promise with the value of that property.
*/
winget(id: string, propName: string): Promise<any>;
/**
* Retrieves multiple properties of the BYOND skin element,
* as defined in the `propNames` array.
*
* Returns a promise with a key-value object containing listed properties.
*/
winget(id: string, propNames: string[]): Promise<object>;
/**
* Assigns properties to BYOND skin elements.
*/
winset(props: object): void;
/**
* Assigns properties to the BYOND skin element.
*/
winset(id: string, props: object): void;
/**
* Sets a property on the BYOND skin element to a certain value.
*/
winset(id: string, propName: string, propValue: any): void;
/**
* Parses BYOND JSON.
*
* Uses a special encoding to preverse Infinity and NaN.
*/
parseJson(text: string): any;
/**
* Loads a stylesheet into the document.
*/
loadCss(url: string): void;
/**
* Loads a script into the document.
*/
loadJs(url: string): void;
}
declare const Byond: ByondType;
export {};
+14
View File
@@ -0,0 +1,14 @@
module.exports = {
roots: ['<rootDir>/packages'],
testMatch: [
'<rootDir>/packages/**/__tests__/*.{js,jsx,ts,tsx}',
'<rootDir>/packages/**/*.{spec,test}.{js,jsx,ts,tsx}',
],
testEnvironment: 'jsdom',
testRunner: require.resolve('jest-circus/runner'),
transform: {
'^.+\\.(js|jsx|ts|tsx|cjs|mjs)$': require.resolve('babel-jest'),
},
moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json'],
resetMocks: true,
};
+31 -23
View File
@@ -1,35 +1,43 @@
{
"private": true,
"name": "tgui-workspace",
"version": "4.2.0",
"version": "4.3.0",
"workspaces": [
"packages/*"
],
"dependencies": {
"@babel/core": "^7.11.1",
"@babel/plugin-transform-jscript": "^7.10.4",
"@babel/preset-env": "^7.11.0",
"babel-eslint": "^10.0.3",
"babel-loader": "^8.1.0",
"@babel/core": "^7.12.17",
"@babel/eslint-parser": "^7.12.17",
"@babel/plugin-proposal-class-properties": "^7.12.13",
"@babel/plugin-transform-jscript": "^7.12.13",
"@babel/preset-env": "^7.12.17",
"@babel/preset-typescript": "^7.12.17",
"@types/jest": "^26.0.20",
"@types/jsdom": "^16.2.6",
"@types/node": "^14.14.31",
"@typescript-eslint/parser": "^4.15.1",
"babel-jest": "^26.6.3",
"babel-loader": "^8.2.2",
"babel-plugin-inferno": "^6.1.1",
"babel-plugin-transform-remove-console": "^6.9.4",
"common": "workspace:*",
"css-loader": "^4.2.1",
"cssnano": "^4.1.10",
"eslint": "^7.4.0",
"eslint-plugin-react": "^7.17.0",
"extract-css-chunks-webpack-plugin": "^4.7.5",
"file-loader": "^6.0.0",
"inferno": "^7.4.2",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"pnp-webpack-plugin": "^1.6.4",
"sass": "^1.26.10",
"sass-loader": "^9.0.3",
"style-loader": "^1.2.1",
"terser-webpack-plugin": "^4.1.0",
"url-loader": "^4.1.0",
"webpack": "^4.44.1",
"webpack-bundle-analyzer": "^3.8.0",
"webpack-cli": "^3.3.12"
"css-loader": "^5.0.2",
"eslint": "^7.20.0",
"eslint-plugin-react": "^7.22.0",
"file-loader": "^6.2.0",
"inferno": "^7.4.8",
"jest": "^26.6.3",
"jest-circus": "^26.6.3",
"jsdom": "^16.4.0",
"mini-css-extract-plugin": "^1.3.8",
"sass": "^1.32.8",
"sass-loader": "^11.0.1",
"style-loader": "^2.0.0",
"terser-webpack-plugin": "^5.1.1",
"typescript": "^4.1.5",
"url-loader": "^4.1.1",
"webpack": "^5.23.0",
"webpack-bundle-analyzer": "^4.4.0",
"webpack-cli": "^4.5.0"
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
{
"private": true,
"name": "common",
"version": "4.2.0"
"version": "4.3.0"
}
+20
View File
@@ -0,0 +1,20 @@
/**
* @file
* @copyright 2021 Aleksej Komarov
* @license MIT
*/
import { classes } from './react';
describe('classes', () => {
test('empty', () => {
expect(classes([])).toBe('');
});
test('result contains inputs', () => {
const output = classes(['foo', 'bar', false, true, 0, 1, 'baz']);
expect(output).toContain('foo');
expect(output).toContain('bar');
expect(output).toContain('baz');
});
});
@@ -6,11 +6,8 @@
/**
* Helper for conditionally adding/removing classes in React
*
* @param {any[]} classNames
* @return {string}
*/
export const classes = classNames => {
export const classes = (classNames: (string | BooleanLike)[]) => {
let className = '';
for (let i = 0; i < classNames.length; i++) {
const part = classNames[i];
@@ -25,9 +22,9 @@ export const classes = classNames => {
* Normalizes children prop, so that it is always an array of VDom
* elements.
*/
export const normalizeChildren = children => {
export const normalizeChildren = <T>(children: T | T[]) => {
if (Array.isArray(children)) {
return children.flat().filter(value => value);
return children.flat().filter(value => value) as T[];
}
if (typeof children === 'object') {
return [children];
@@ -39,7 +36,7 @@ export const normalizeChildren = children => {
* Shallowly checks if two objects are different.
* Credit: https://github.com/developit/preact-compat
*/
export const shallowDiffers = (a, b) => {
export const shallowDiffers = (a: object, b: object) => {
let i;
for (i in a) {
if (!(i in b)) {
@@ -66,8 +63,14 @@ export const pureComponentHooks = {
/**
* A helper to determine whether the object is renderable by React.
*/
export const canRender = value => {
export const canRender = (value: unknown) => {
return value !== undefined
&& value !== null
&& typeof value !== 'boolean';
};
/**
* A common case in tgui, when you pass a value conditionally, these are
* the types that can fall through the condition.
*/
export type BooleanLike = number | boolean | null | undefined;
+3 -3
View File
@@ -108,15 +108,15 @@ export const combineReducers = reducersObj => {
* returns the action type, allowing it to be used in reducer logic that
* is looking for that action type.
*
* @param type The action type to use for created actions.
* @param prepare (optional) a method that takes any number of arguments
* @param {string} type The action type to use for created actions.
* @param {any} prepare (optional) a method that takes any number of arguments
* and returns { payload } or { payload, meta }. If this is given, the
* resulting action creator will pass it's arguments to this method to
* calculate payload & meta.
*
* @public
*/
export const createAction = (type, prepare) => {
export const createAction = (type, prepare = null) => {
const actionCreator = (...args) => {
if (!prepare) {
return { type, payload: args[0] };
+4 -5
View File
@@ -4,7 +4,7 @@
* @license MIT
*/
import { setupWebpack, getWebpackConfig } from './webpack.js';
import { createCompiler } from './webpack.js';
import { reloadByondCache } from './reloader.js';
const noHot = process.argv.includes('--no-hot');
@@ -12,7 +12,7 @@ const noTmp = process.argv.includes('--no-tmp');
const reloadOnce = process.argv.includes('--reload');
const setupServer = async () => {
const config = await getWebpackConfig({
const compiler = await createCompiler({
mode: 'development',
hot: !noHot,
devServer: true,
@@ -20,12 +20,11 @@ const setupServer = async () => {
});
// Reload cache once
if (reloadOnce) {
const bundleDir = config.output.path;
await reloadByondCache(bundleDir);
await reloadByondCache(compiler.bundleDir);
return;
}
// Run a development server
await setupWebpack(config);
await compiler.watch();
};
setupServer();
+3 -4
View File
@@ -1,15 +1,14 @@
{
"private": true,
"name": "tgui-dev-server",
"version": "4.2.0",
"version": "4.3.0",
"dependencies": {
"axios": "^0.19.2",
"axios": "^0.21.1",
"common": "workspace:*",
"esm": "^3.2.25",
"glob": "^7.1.6",
"source-map": "^0.7.3",
"stacktrace-parser": "^0.1.10",
"webpack": "^4.44.1",
"ws": "^7.3.1"
"ws": "^7.4.3"
}
}
+64 -45
View File
@@ -7,58 +7,77 @@
import { createLogger } from 'common/logging.js';
import fs from 'fs';
import { createRequire } from 'module';
import { promisify } from 'util';
import webpack from 'webpack';
import { dirname } from 'path';
import { loadSourceMaps, setupLink } from './link/server.js';
import { reloadByondCache } from './reloader.js';
import { resolveGlob } from './util.js';
const logger = createLogger('webpack');
export const getWebpackConfig = async options => {
const require = createRequire(import.meta.url);
const cwd = process.cwd();
const createConfig = await require(cwd + '/webpack.config.js');
return createConfig({}, options);
/**
* @param {any} config
* @return {WebpackCompiler}
*/
export const createCompiler = async options => {
const compiler = new WebpackCompiler();
await compiler.setup(options);
return compiler;
};
export const setupWebpack = async config => {
logger.log('setting up');
const bundleDir = config.output.path;
// Setup link
const link = setupLink();
// Instantiate the compiler
const compiler = webpack(config);
// Clear garbage before compiling
compiler.hooks.watchRun.tapPromise('tgui-dev-server', async () => {
const files = await resolveGlob(bundleDir, './*.hot-update.*');
logger.log(`clearing garbage (${files.length} files)`);
for (let file of files) {
await promisify(fs.unlink)(file);
class WebpackCompiler {
async setup(options) {
// Create a require context that is relative to project root
// and retrieve all necessary dependencies.
const requireFromRoot = createRequire(dirname(import.meta.url) + '/../..');
const webpack = await requireFromRoot('webpack');
const createConfig = await requireFromRoot('./webpack.config.js');
const config = createConfig({}, options);
// Inject the HMR plugin into the config if we're using it
if (options.hot) {
config.plugins.push(new webpack.HotModuleReplacementPlugin());
}
logger.log('compiling');
});
// Start reloading when it's finished
compiler.hooks.done.tap('tgui-dev-server', async stats => {
// Load source maps
await loadSourceMaps(bundleDir);
// Reload cache
await reloadByondCache(bundleDir);
// Notify all clients that update has happened
link.broadcastMessage({
type: 'hotUpdate',
this.webpack = webpack;
this.config = config;
this.bundleDir = config.output.path;
}
async watch() {
logger.log('setting up');
// Setup link
const link = setupLink();
// Instantiate the compiler
const compiler = this.webpack.webpack(this.config);
// Clear garbage before compiling
compiler.hooks.watchRun.tapPromise('tgui-dev-server', async () => {
const files = await resolveGlob(this.bundleDir, './*.hot-update.*');
logger.log(`clearing garbage (${files.length} files)`);
for (let file of files) {
fs.unlinkSync(file);
}
logger.log('compiling');
});
});
// Start watching
logger.log('watching for changes');
compiler.watch({}, (err, stats) => {
if (err) {
logger.error('compilation error', err);
return;
}
stats
.toString(config.devServer.stats)
.split('\n')
.forEach(line => logger.log(line));
});
};
// Start reloading when it's finished
compiler.hooks.done.tap('tgui-dev-server', async stats => {
// Load source maps
await loadSourceMaps(this.bundleDir);
// Reload cache
await reloadByondCache(this.bundleDir);
// Notify all clients that update has happened
link.broadcastMessage({
type: 'hotUpdate',
});
});
// Start watching
logger.log('watching for changes');
compiler.watch({}, (err, stats) => {
if (err) {
logger.error('compilation error', err);
return;
}
stats
.toString(this.config.devServer.stats)
.split('\n')
.forEach(line => logger.log(line));
});
}
}
+22 -26
View File
@@ -4,7 +4,7 @@
* @license MIT
*/
import { Button, Flex, Section } from 'tgui/components';
import { Button, Section, Stack } from 'tgui/components';
import { Pane } from 'tgui/layouts';
import { NowPlayingWidget, useAudio } from './audio';
import { ChatPanel, ChatTabs } from './chat';
@@ -34,19 +34,17 @@ export const Panel = (props, context) => {
}
return (
<Pane theme={settings.theme === 'default' ? 'light' : settings.theme}>
<Flex
direction="column"
height="100%">
<Flex.Item>
<Stack fill vertical>
<Stack.Item>
<Section fitted>
<Flex mx={0.5} align="center">
<Flex.Item mx={0.5} grow={1} overflowX="auto">
<Stack mr={1} align="center">
<Stack.Item grow overflowX="auto">
<ChatTabs />
</Flex.Item>
<Flex.Item mx={0.5}>
</Stack.Item>
<Stack.Item>
<PingIndicator />
</Flex.Item>
<Flex.Item mx={0.5}>
</Stack.Item>
<Stack.Item>
<Button
color="grey"
selected={audio.visible}
@@ -54,8 +52,8 @@ export const Panel = (props, context) => {
tooltip="Music player"
tooltipPosition="bottom-left"
onClick={() => audio.toggle()} />
</Flex.Item>
<Flex.Item mx={0.5}>
</Stack.Item>
<Stack.Item>
<Button
icon={settings.visible ? 'times' : 'cog'}
selected={settings.visible}
@@ -64,23 +62,23 @@ export const Panel = (props, context) => {
: 'Open settings'}
tooltipPosition="bottom-left"
onClick={() => settings.toggle()} />
</Flex.Item>
</Flex>
</Stack.Item>
</Stack>
</Section>
</Flex.Item>
</Stack.Item>
{audio.visible && (
<Flex.Item mt={1}>
<Stack.Item>
<Section>
<NowPlayingWidget />
</Section>
</Flex.Item>
</Stack.Item>
)}
{settings.visible && (
<Flex.Item mt={1}>
<Stack.Item>
<SettingsPanel />
</Flex.Item>
</Stack.Item>
)}
<Flex.Item mt={1} grow={1}>
<Stack.Item grow>
<Section fill fitted position="relative">
<Pane.Content scrollable>
<ChatPanel lineHeight={settings.lineHeight} />
@@ -107,8 +105,8 @@ export const Panel = (props, context) => {
)}
</Notifications>
</Section>
</Flex.Item>
</Flex>
</Stack.Item>
</Stack>
</Pane>
);
};
@@ -130,9 +128,7 @@ const HoboPanel = (props, context) => {
Settings
</Button>
{settings.visible && (
<Flex.Item mt={1}>
<SettingsPanel />
</Flex.Item>
<SettingsPanel />
) || (
<ChatPanel lineHeight={settings.lineHeight} />
)}
@@ -6,7 +6,6 @@
import { toFixed } from 'common/math';
import { useDispatch, useSelector } from 'common/redux';
import { Fragment } from 'inferno';
import { Button, Flex, Knob } from 'tgui/components';
import { useSettings } from '../settings';
import { selectAudio } from './selectors';
@@ -19,7 +18,7 @@ export const NowPlayingWidget = (props, context) => {
return (
<Flex align="center">
{audio.playing && (
<Fragment>
<>
<Flex.Item
shrink={0}
mx={0.5}
@@ -36,7 +35,7 @@ export const NowPlayingWidget = (props, context) => {
}}>
{title || 'Unknown Track'}
</Flex.Item>
</Fragment>
</>
) || (
<Flex.Item grow={1} color="label">
Nothing to play.
@@ -5,7 +5,7 @@
*/
import { useDispatch, useSelector } from 'common/redux';
import { Button, Collapsible, Divider, Flex, Input, Section } from 'tgui/components';
import { Button, Collapsible, Divider, Input, Section, Stack } from 'tgui/components';
import { removeChatPage, toggleAcceptedType, updateChatPage } from './actions';
import { MESSAGE_TYPES } from './constants';
import { selectCurrentChatPage } from './selectors';
@@ -14,9 +14,9 @@ export const ChatPageSettings = (props, context) => {
const page = useSelector(context, selectCurrentChatPage);
const dispatch = useDispatch(context);
return (
<Section fill>
<Flex mx={-0.5} align="center">
<Flex.Item mx={0.5} grow={1}>
<Section>
<Stack align="center">
<Stack.Item grow={1}>
<Input
fluid
value={page.name}
@@ -24,8 +24,8 @@ export const ChatPageSettings = (props, context) => {
pageId: page.id,
name: value,
}))} />
</Flex.Item>
<Flex.Item mx={0.5}>
</Stack.Item>
<Stack.Item>
<Button
icon="times"
color="red"
@@ -34,8 +34,8 @@ export const ChatPageSettings = (props, context) => {
}))}>
Remove
</Button>
</Flex.Item>
</Flex>
</Stack.Item>
</Stack>
<Divider />
<Section title="Messages to display" level={2}>
{MESSAGE_TYPES
+3 -3
View File
@@ -5,7 +5,7 @@
*/
import { shallowDiffers } from 'common/react';
import { Component, createRef, Fragment } from 'inferno';
import { Component, createRef } from 'inferno';
import { Button } from 'tgui/components';
import { chatRenderer } from './renderer';
@@ -55,7 +55,7 @@ export class ChatPanel extends Component {
scrollTracking,
} = this.state;
return (
<Fragment>
<>
<div className="Chat" ref={this.ref} />
{!scrollTracking && (
<Button
@@ -65,7 +65,7 @@ export class ChatPanel extends Component {
Scroll to bottom
</Button>
)}
</Fragment>
</>
);
}
}
+2 -2
View File
@@ -58,13 +58,13 @@ export const MESSAGE_TYPES = [
type: MESSAGE_TYPE_RADIO,
name: 'Radio',
description: 'All departments of radio messages',
selector: '.alert, .syndradio, .centradio, .airadio, .entradio, .comradio, .secradio, .engradio, .medradio, .sciradio, .supradio, .srvradio, .expradio, .radio, .deptradio, .newscaster',
selector: '.alert, .minorannounce, .syndradio, .airadio, .entradio, .centcomradio, .aiprivradio, .comradio, .secradio, .engradio, .medradio, .sciradio, .suppradio, .servradio, .radio, .deptradio, .binarysay, .newscaster',
},
{
type: MESSAGE_TYPE_INFO,
name: 'Info',
description: 'Non-urgent messages from the game and items',
selector: '.notice:not(.pm), .adminnotice, .info, .sinister, .cult',
selector: '.notice:not(.pm), .adminnotice, .info, .sinister, .cult, .infoplain',
},
{
type: MESSAGE_TYPE_WARNING,
@@ -4,6 +4,7 @@
* @license MIT
*/
import DOMPurify from 'dompurify';
import { storage } from 'common/storage';
import { loadSettings, updateSettings } from '../settings/actions';
import { selectSettings } from '../settings/selectors';
@@ -13,6 +14,14 @@ import { createMessage, serializeMessage } from './model';
import { chatRenderer } from './renderer';
import { selectChat, selectCurrentChatPage } from './selectors';
// List of blacklisted tags
const FORBID_TAGS = [
'a',
'iframe',
'link',
'video',
];
const saveChatToStorage = async store => {
const state = selectChat(store.getState());
const fromIndex = Math.max(0,
@@ -35,6 +44,13 @@ const loadChatFromStorage = async store => {
return;
}
if (messages) {
for (let message of messages) {
if (message.html) {
message.html = DOMPurify.sanitize(message.html, {
FORBID_TAGS,
});
}
}
const batch = [
...messages,
createMessage({
+1 -1
View File
@@ -451,7 +451,7 @@ class ChatRenderer {
cssText += 'body, html { background-color: #141414 }\n';
// Compile chat log as HTML text
let messagesHtml = '';
for (let message of this.messages) {
for (let message of this.visibleMessages) {
if (message.node) {
messagesHtml += message.node.outerHTML + '\n';
}
+3 -2
View File
@@ -1,10 +1,11 @@
{
"private": true,
"name": "tgui-panel",
"version": "4.2.0",
"version": "4.3.0",
"dependencies": {
"common": "workspace:*",
"inferno": "^7.4.2",
"dompurify": "^2.2.6",
"inferno": "^7.4.8",
"tgui": "workspace:*",
"tgui-dev-server": "workspace:*",
"tgui-polyfill": "workspace:*"
+8 -1
View File
@@ -11,6 +11,7 @@ import { PING_INTERVAL, PING_QUEUE_SIZE, PING_TIMEOUT } from './constants';
export const pingMiddleware = store => {
let initialized = false;
let index = 0;
let interval;
const pings = [];
const sendPing = () => {
for (let i = 0; i < PING_QUEUE_SIZE; i++) {
@@ -32,9 +33,15 @@ export const pingMiddleware = store => {
const { type, payload } = action;
if (!initialized) {
initialized = true;
setInterval(sendPing, PING_INTERVAL);
interval = setInterval(sendPing, PING_INTERVAL);
sendPing();
}
if (type === 'roundrestart') {
// Stop pinging because dreamseeker is currently reconnecting.
// Topic calls in the middle of reconnect will crash the connection.
clearInterval(interval);
return next(action);
}
if (type === 'pingReply') {
const { index } = payload;
const ping = pings[index];
@@ -5,21 +5,22 @@
*/
import { toFixed } from 'common/math';
import { useLocalState } from 'tgui/backend';
import { useDispatch, useSelector } from 'common/redux';
import { Box, Button, ColorBox, Divider, Dropdown, Flex, Input, LabeledList, NumberInput, Section, Tabs, TextArea } from 'tgui/components';
import { Box, Button, ColorBox, Divider, Dropdown, Flex, Input, LabeledList, NumberInput, Section, Stack, Tabs, TextArea } from 'tgui/components';
import { ChatPageSettings } from '../chat';
import { rebuildChat, saveChatToDisk } from '../chat/actions';
import { THEMES } from '../themes';
import { changeSettingsTab, updateSettings } from './actions';
import { SETTINGS_TABS } from './constants';
import { FONTS, SETTINGS_TABS } from './constants';
import { selectActiveTab, selectSettings } from './selectors';
export const SettingsPanel = (props, context) => {
const activeTab = useSelector(context, selectActiveTab);
const dispatch = useDispatch(context);
return (
<Flex>
<Flex.Item mr={1}>
<Stack fill>
<Stack.Item>
<Section fitted fill minHeight="8em">
<Tabs vertical>
{SETTINGS_TABS.map(tab => (
@@ -34,30 +35,32 @@ export const SettingsPanel = (props, context) => {
))}
</Tabs>
</Section>
</Flex.Item>
<Flex.Item grow={1} basis={0}>
</Stack.Item>
<Stack.Item grow={1} basis={0}>
{activeTab === 'general' && (
<SettingsGeneral />
)}
{activeTab === 'chatPage' && (
<ChatPageSettings />
)}
</Flex.Item>
</Flex>
</Stack.Item>
</Stack>
);
};
export const SettingsGeneral = (props, context) => {
const {
theme,
fontFamily,
fontSize,
lineHeight,
highlightText,
highlightColor,
} = useSelector(context, selectSettings);
const dispatch = useDispatch(context);
const [freeFont, setFreeFont] = useLocalState(context, "freeFont", false);
return (
<Section fill>
<Section>
<LabeledList>
<LabeledList.Item label="Theme">
<Dropdown
@@ -67,6 +70,38 @@ export const SettingsGeneral = (props, context) => {
theme: value,
}))} />
</LabeledList.Item>
<LabeledList.Item label="Font style">
<Stack inline align="baseline">
<Stack.Item>
{!freeFont && (
<Dropdown
selected={fontFamily}
options={FONTS}
onSelected={value => dispatch(updateSettings({
fontFamily: value,
}))} />
) || (
<Input
value={fontFamily}
onChange={(e, value) => dispatch(updateSettings({
fontFamily: value,
}))}
/>
)}
</Stack.Item>
<Stack.Item>
<Button
content="Custom font"
icon={freeFont? "lock-open" : "lock"}
color={freeFont? "good" : "bad"}
ml={1}
onClick={() => {
setFreeFont(!freeFont);
}}
/>
</Stack.Item>
</Stack>
</LabeledList.Item>
<LabeledList.Item label="Font size">
<NumberInput
width="4em"
@@ -14,3 +14,20 @@ export const SETTINGS_TABS = [
name: 'Chat Tabs',
},
];
export const FONTS_DISABLED = "Default";
export const FONTS = [
FONTS_DISABLED,
'Verdana',
'Arial',
'Arial Black',
'Comic Sans MS',
'Impact',
'Lucida Sans Unicode',
'Tahoma',
'Trebuchet MS',
'Courier New',
'Lucida Console',
];
@@ -8,6 +8,7 @@ import { storage } from 'common/storage';
import { setClientTheme } from '../themes';
import { loadSettings, updateSettings } from './actions';
import { selectSettings } from './selectors';
import { FONTS_DISABLED } from './constants';
const setGlobalFontSize = fontSize => {
document.documentElement.style
@@ -16,6 +17,15 @@ const setGlobalFontSize = fontSize => {
.setProperty('font-size', fontSize + 'px');
};
const setGlobalFontFamily = fontFamily => {
if (fontFamily === FONTS_DISABLED) fontFamily = null;
document.documentElement.style
.setProperty('font-family', fontFamily);
document.body.style
.setProperty('font-family', fontFamily);
};
export const settingsMiddleware = store => {
let initialized = false;
return next => action => {
@@ -37,6 +47,7 @@ export const settingsMiddleware = store => {
const settings = selectSettings(store.getState());
// Update global UI font size
setGlobalFontSize(settings.fontSize);
setGlobalFontFamily(settings.fontFamily);
// Save settings to the web storage
storage.set('panel-settings', settings);
return;
+2 -1
View File
@@ -5,11 +5,12 @@
*/
import { changeSettingsTab, loadSettings, openChatSettings, toggleSettings, updateSettings } from './actions';
import { SETTINGS_TABS } from './constants';
import { FONTS, SETTINGS_TABS } from './constants';
const initialState = {
version: 1,
fontSize: 13,
fontFamily: FONTS[0],
lineHeight: 1.2,
theme: 'default',
adminMusicVolume: 0.5,
@@ -26,7 +26,7 @@ $color-bg-section: base.$color-bg-section !default;
vertical-align: middle;
background-color: crimson;
border-radius: 10px;
transition: font-size 200ms;
transition: font-size 200ms ease-out;
&:before {
content: 'x';
@@ -338,7 +338,7 @@ em {
font-weight: bold;
}
.say {
.say, .emote, .infoplain {
}
.deadsay {
@@ -417,10 +417,6 @@ em {
color: #d82020;
}
.emote {
// font-style: italic;
}
.userdanger {
color: #c51e1e;
font-weight: bold;
@@ -457,6 +453,11 @@ em {
font-weight: bold;
}
.minorannounce {
font-weight: bold;
font-size: 185%;
}
.greenannounce {
color: #059223;
font-weight: bold;
@@ -541,6 +542,7 @@ em {
color: #059223;
}
/* hornichems */
.userlove {
color: #ff42a6;
font-style: italic;
@@ -848,6 +850,11 @@ em {
font-family: "Courier New", cursive, sans-serif;
}
.tape_recorder {
color: #FF0000;
font-family: "Courier New", cursive, sans-serif;
}
.command_headset {
font-weight: bold;
font-size: 125%;
@@ -902,6 +909,7 @@ em {
font-style: italic;
}
/*jacq */
.spooky {
color: #FF9100;
}
@@ -273,7 +273,7 @@ a.popt {
.italic,
.italics {
.italics {
font-style: italic;
}
@@ -356,7 +356,7 @@ em {
font-weight: bold;
}
.say {
.say, .emote, .infoplain {
}
.deadsay {
@@ -442,10 +442,6 @@ h1.alert, h2.alert {
color: #000000;
}
.emote {
// font-style: italic;
}
.userdanger {
color: #ff0000;
font-weight: bold;
@@ -497,6 +493,11 @@ h1.alert, h2.alert {
font-weight: bold;
}
.minorannounce {
font-weight: bold;
font-size: 185%;
}
.greenannounce {
color: #00ff00;
font-weight: bold;
@@ -889,6 +890,11 @@ h1.alert, h2.alert {
font-family: "Courier New", cursive, sans-serif;
}
.tape_recorder {
color: #800000;
font-family: "Courier New", cursive, sans-serif;
}
.command_headset {
font-weight: bold;
font-size: 125%; /* 160% default, changed so command can scream less */
@@ -927,7 +933,7 @@ h1.alert, h2.alert {
.clown {
color: #FF69Bf;
font-size: 125%; /* 160% default, changed so "command" can scream less */
font-size: 125%;
font-family: "Comic Sans MS", cursive, sans-serif;
font-weight: bold;
}
@@ -40,6 +40,7 @@
@include meta.load-css('~tgui/styles/components/ProgressBar.scss');
@include meta.load-css('~tgui/styles/components/Section.scss');
@include meta.load-css('~tgui/styles/components/Slider.scss');
@include meta.load-css('~tgui/styles/components/Stack.scss');
@include meta.load-css('~tgui/styles/components/Table.scss');
@include meta.load-css('~tgui/styles/components/Tabs.scss');
@include meta.load-css('~tgui/styles/components/TextArea.scss');
@@ -36,13 +36,7 @@
'text-color': rgba(0, 0, 0, 0.5),
'color-default': rgba(0, 0, 0, 1),
));
@include meta.load-css('~tgui/styles/components/Section.scss', $with: (
// 'background-color': rgba(0, 0, 0, 0.1),
'shadow-color': rgba(0, 0, 0, 0.1),
'shadow-size': 1em,
'shadow-type': outset,
'shadow-offset': 0 0.2em,
));
@include meta.load-css('~tgui/styles/components/Section.scss');
@include meta.load-css('~tgui/styles/components/Button.scss', $with: (
'color-default': #bbbbbb,
'color-disabled': #363636,
+5
View File
@@ -14,3 +14,8 @@ import './ie8';
import './dom4';
import './css-om';
import './inferno';
// Fetch is required for Webpack HMR
if (module.hot) {
require('whatwg-fetch');
}
+4 -3
View File
@@ -1,9 +1,10 @@
{
"private": true,
"name": "tgui-polyfill",
"version": "4.2.0",
"version": "4.3.0",
"dependencies": {
"core-js": "^3.6.5",
"regenerator-runtime": "^0.13.7"
"core-js": "^3.9.0",
"regenerator-runtime": "^0.13.7",
"whatwg-fetch": "^3.6.1"
}
}
+385
View File
@@ -0,0 +1,385 @@
/**
* This file provides a clear separation layer between backend updates
* and what state our React app sees.
*
* Sometimes backend can response without a "data" field, but our final
* state will still contain previous "data" because we are merging
* the response with already existing state.
*
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { perf } from 'common/perf';
import { createAction } from 'common/redux';
import { setupDrag } from './drag';
import { focusMap } from './focus';
import { createLogger } from './logging';
import { resumeRenderer, suspendRenderer } from './renderer';
const logger = createLogger('backend');
export const backendUpdate = createAction('backend/update');
export const backendSetSharedState = createAction('backend/setSharedState');
export const backendSuspendStart = createAction('backend/suspendStart');
export const backendSuspendSuccess = () => ({
type: 'backend/suspendSuccess',
payload: {
timestamp: Date.now(),
},
});
const initialState = {
config: {},
data: {},
shared: {},
// Start as suspended
suspended: Date.now(),
suspending: false,
};
export const backendReducer = (state = initialState, action) => {
const { type, payload } = action;
if (type === 'backend/update') {
// Merge config
const config = {
...state.config,
...payload.config,
};
// Merge data
const data = {
...state.data,
...payload.static_data,
...payload.data,
};
// Merge shared states
const shared = { ...state.shared };
if (payload.shared) {
for (let key of Object.keys(payload.shared)) {
const value = payload.shared[key];
if (value === '') {
shared[key] = undefined;
}
else {
shared[key] = JSON.parse(value);
}
}
}
// Return new state
return {
...state,
config,
data,
shared,
suspended: false,
};
}
if (type === 'backend/setSharedState') {
const { key, nextState } = payload;
return {
...state,
shared: {
...state.shared,
[key]: nextState,
},
};
}
if (type === 'backend/suspendStart') {
return {
...state,
suspending: true,
};
}
if (type === 'backend/suspendSuccess') {
const { timestamp } = payload;
return {
...state,
data: {},
shared: {},
config: {
...state.config,
title: '',
status: 1,
},
suspending: false,
suspended: timestamp,
};
}
return state;
};
export const backendMiddleware = store => {
let fancyState;
let suspendInterval;
return next => action => {
const { suspended } = selectBackend(store.getState());
const { type, payload } = action;
if (type === 'update') {
store.dispatch(backendUpdate(payload));
return;
}
if (type === 'suspend') {
store.dispatch(backendSuspendSuccess());
return;
}
if (type === 'ping') {
sendMessage({
type: 'pingReply',
});
return;
}
if (type === 'backend/suspendStart' && !suspendInterval) {
logger.log(`suspending (${window.__windowId__})`);
// Keep sending suspend messages until it succeeds.
// It may fail multiple times due to topic rate limiting.
const suspendFn = () => sendMessage({
type: 'suspend',
});
suspendFn();
suspendInterval = setInterval(suspendFn, 2000);
}
if (type === 'backend/suspendSuccess') {
suspendRenderer();
clearInterval(suspendInterval);
suspendInterval = undefined;
Byond.winset(window.__windowId__, {
'is-visible': false,
});
setImmediate(() => focusMap());
}
if (type === 'backend/update') {
const fancy = payload.config?.window?.fancy;
// Initialize fancy state
if (fancyState === undefined) {
fancyState = fancy;
}
// React to changes in fancy
else if (fancyState !== fancy) {
logger.log('changing fancy mode to', fancy);
fancyState = fancy;
Byond.winset(window.__windowId__, {
titlebar: !fancy,
'can-resize': !fancy,
});
}
}
// Resume on incoming update
if (type === 'backend/update' && suspended) {
// Show the payload
logger.log('backend/update', payload);
// Signal renderer that we have resumed
resumeRenderer();
// Setup drag
setupDrag();
// We schedule this for the next tick here because resizing and unhiding
// during the same tick will flash with a white background.
setImmediate(() => {
perf.mark('resume/start');
// Doublecheck if we are not re-suspended.
const { suspended } = selectBackend(store.getState());
if (suspended) {
return;
}
Byond.winset(window.__windowId__, {
'is-visible': true,
});
perf.mark('resume/finish');
if (process.env.NODE_ENV !== 'production') {
logger.log('visible in',
perf.measure('render/finish', 'resume/finish'));
}
});
}
return next(action);
};
};
/**
* Sends a message to /datum/tgui_window.
*/
export const sendMessage = (message: any = {}) => {
const { payload, ...rest } = message;
const data: any = {
// Message identifying header
tgui: 1,
window_id: window.__windowId__,
// Message body
...rest,
};
// JSON-encode the payload
if (payload !== null && payload !== undefined) {
data.payload = JSON.stringify(payload);
}
Byond.topic(data);
};
/**
* Sends an action to `ui_act` on `src_object` that this tgui window
* is associated with.
*/
export const sendAct = (action: string, payload: object = {}) => {
// Validate that payload is an object
const isObject = typeof payload === 'object'
&& payload !== null
&& !Array.isArray(payload);
if (!isObject) {
logger.error(`Payload for act() must be an object, got this:`, payload);
return;
}
sendMessage({
type: 'act/' + action,
payload,
});
};
type BackendState<TData> = {
config: {
title: string,
status: number,
interface: string,
window: {
key: string,
size: [number, number],
fancy: boolean,
locked: boolean,
},
client: {
ckey: string,
address: string,
computer_id: string,
},
user: {
name: string,
observer: number,
},
},
data: TData,
shared: Record<string, any>,
suspending: boolean,
suspended: boolean,
}
/**
* Selects a backend-related slice of Redux state
*/
export const selectBackend = <TData>(state: any): BackendState<TData> => (
state.backend || {}
);
/**
* A React hook (sort of) for getting tgui state and related functions.
*
* This is supposed to be replaced with a real React Hook, which can only
* be used in functional components.
*
* You can make
*/
export const useBackend = <TData>(context: any) => {
const { store } = context;
const state = selectBackend<TData>(store.getState());
return {
...state,
act: sendAct,
};
};
/**
* A tuple that contains the state and a setter function for it.
*/
type StateWithSetter<T> = [T, (nextState: T) => void];
/**
* Allocates state on Redux store without sharing it with other clients.
*
* Use it when you want to have a stateful variable in your component
* that persists between renders, but will be forgotten after you close
* the UI.
*
* It is a lot more performant than `setSharedState`.
*
* @param context React context.
* @param key Key which uniquely identifies this state in Redux store.
* @param initialState Initializes your global variable with this value.
*/
export const useLocalState = <T>(
context: any,
key: string,
initialState: T,
): StateWithSetter<T> => {
const { store } = context;
const state = selectBackend(store.getState());
const sharedStates = state.shared ?? {};
const sharedState = (key in sharedStates)
? sharedStates[key]
: initialState;
return [
sharedState,
nextState => {
store.dispatch(backendSetSharedState({
key,
nextState: (
typeof nextState === 'function'
? nextState(sharedState)
: nextState
),
}));
},
];
};
/**
* Allocates state on Redux store, and **shares** it with other clients
* in the game.
*
* Use it when you want to have a stateful variable in your component
* that persists not only between renders, but also gets pushed to other
* clients that observe this UI.
*
* This makes creation of observable s
*
* @param context React context.
* @param key Key which uniquely identifies this state in Redux store.
* @param initialState Initializes your global variable with this value.
*/
export const useSharedState = <T>(
context: any,
key: string,
initialState: T,
): StateWithSetter<T> => {
const { store } = context;
const state = selectBackend(store.getState());
const sharedStates = state.shared ?? {};
const sharedState = (key in sharedStates)
? sharedStates[key]
: initialState;
return [
sharedState,
nextState => {
sendMessage({
type: 'setSharedState',
key,
value: JSON.stringify(
typeof nextState === 'function'
? nextState(sharedState)
: nextState
) || '',
});
},
];
};
@@ -4,15 +4,64 @@
* @license MIT
*/
import { classes, pureComponentHooks } from 'common/react';
import { createVNode } from 'inferno';
import { BooleanLike, classes, pureComponentHooks } from 'common/react';
import { createVNode, InfernoNode } from 'inferno';
import { ChildFlags, VNodeFlags } from 'inferno-vnode-flags';
import { CSS_COLORS } from '../constants';
export interface BoxProps {
[key: string]: any;
as?: string;
className?: string | BooleanLike;
children?: InfernoNode;
position?: string | BooleanLike;
overflow?: string | BooleanLike;
overflowX?: string | BooleanLike;
overflowY?: string | BooleanLike;
top?: string | BooleanLike;
bottom?: string | BooleanLike;
left?: string | BooleanLike;
right?: string | BooleanLike;
width?: string | BooleanLike;
minWidth?: string | BooleanLike;
maxWidth?: string | BooleanLike;
height?: string | BooleanLike;
minHeight?: string | BooleanLike;
maxHeight?: string | BooleanLike;
fontSize?: string | BooleanLike;
fontFamily?: string;
lineHeight?: string | BooleanLike;
opacity?: number;
textAlign?: string | BooleanLike;
verticalAlign?: string | BooleanLike;
inline?: BooleanLike;
bold?: BooleanLike;
italic?: BooleanLike;
nowrap?: BooleanLike;
m?: string | BooleanLike;
mx?: string | BooleanLike;
my?: string | BooleanLike;
mt?: string | BooleanLike;
mb?: string | BooleanLike;
ml?: string | BooleanLike;
mr?: string | BooleanLike;
p?: string | BooleanLike;
px?: string | BooleanLike;
py?: string | BooleanLike;
pt?: string | BooleanLike;
pb?: string | BooleanLike;
pl?: string | BooleanLike;
pr?: string | BooleanLike;
color?: string | BooleanLike;
textColor?: string | BooleanLike;
backgroundColor?: string | BooleanLike;
fillPositionedParent?: boolean;
}
/**
* Coverts our rem-like spacing unit into a CSS unit.
*/
export const unit = value => {
export const unit = (value: unknown): string | undefined => {
if (typeof value === 'string') {
// Transparently convert pixels into rem units
if (value.endsWith('px') && !Byond.IS_LTE_IE8) {
@@ -31,7 +80,7 @@ export const unit = value => {
/**
* Same as `unit`, but half the size for integers numbers.
*/
export const halfUnit = value => {
export const halfUnit = (value: unknown): string | undefined => {
if (typeof value === 'string') {
return unit(value);
}
@@ -40,10 +89,13 @@ export const halfUnit = value => {
}
};
const isColorCode = str => !isColorClass(str);
const isColorCode = (str: unknown) => !isColorClass(str);
const isColorClass = str => typeof str === 'string'
&& CSS_COLORS.includes(str);
const isColorClass = (str: unknown): boolean => {
if (typeof str === 'string') {
return CSS_COLORS.includes(str);
}
};
const mapRawPropTo = attrName => (style, value) => {
if (typeof value === 'number' || typeof value === 'string') {
@@ -155,8 +207,8 @@ const styleMapperByPropName = {
},
};
export const computeBoxProps = props => {
const computedProps = {};
export const computeBoxProps = (props: BoxProps) => {
const computedProps: HTMLAttributes<any> = {};
const computedStyles = {};
// Compute props
for (let propName of Object.keys(props)) {
@@ -195,7 +247,7 @@ export const computeBoxProps = props => {
return computedProps;
};
export const computeBoxClassName = props => {
export const computeBoxClassName = (props: BoxProps) => {
const color = props.textColor || props.color;
const backgroundColor = props.backgroundColor;
return classes([
@@ -204,7 +256,7 @@ export const computeBoxClassName = props => {
]);
};
export const Box = props => {
export const Box = (props: BoxProps) => {
const {
as = 'div',
className,
@@ -4,10 +4,18 @@
* @license MIT
*/
import { classes, pureComponentHooks } from 'common/react';
import { Box, unit } from './Box';
import { BooleanLike, classes, pureComponentHooks } from 'common/react';
import { Box, BoxProps, unit } from './Box';
export const computeFlexProps = props => {
export interface FlexProps extends BoxProps {
direction: string | BooleanLike;
wrap: string | BooleanLike;
align: string | BooleanLike;
justify: string | BooleanLike;
inline: BooleanLike;
}
export const computeFlexProps = (props: FlexProps) => {
const {
className,
direction,
@@ -15,7 +23,6 @@ export const computeFlexProps = props => {
align,
justify,
inline,
spacing = 0,
...rest
} = props;
return {
@@ -27,7 +34,6 @@ export const computeFlexProps = props => {
: 'Flex--iefix'
),
inline && 'Flex--inline',
spacing > 0 && 'Flex--spacing--' + spacing,
className,
]),
style: {
@@ -47,7 +53,15 @@ export const Flex = props => (
Flex.defaultHooks = pureComponentHooks;
export const computeFlexItemProps = props => {
export interface FlexItemProps extends BoxProps {
grow?: number;
order?: number;
shrink?: number;
basis?: string | BooleanLike;
align?: string | BooleanLike;
}
export const computeFlexItemProps = (props: FlexItemProps) => {
const {
className,
style,
@@ -69,8 +83,8 @@ export const computeFlexItemProps = props => {
]),
style: {
...style,
'flex-grow': grow,
'flex-shrink': shrink,
'flex-grow': grow !== undefined && Number(grow),
'flex-shrink': shrink !== undefined && Number(shrink),
'flex-basis': unit(basis),
'order': order,
'align-self': align,
@@ -79,7 +93,7 @@ export const computeFlexItemProps = props => {
};
};
export const FlexItem = props => (
const FlexItem = props => (
<Box {...computeFlexItemProps(props)} />
);
+2
View File
@@ -7,6 +7,7 @@
import { Table } from './Table';
import { pureComponentHooks } from 'common/react';
/** @deprecated */
export const Grid = props => {
const { children, ...rest } = props;
return (
@@ -20,6 +21,7 @@ export const Grid = props => {
Grid.defaultHooks = pureComponentHooks;
/** @deprecated */
export const GridColumn = props => {
const { size = 1, style, ...rest } = props;
return (
@@ -4,11 +4,16 @@
* @license MIT
*/
import { classes, pureComponentHooks } from 'common/react';
import { BooleanLike, classes, pureComponentHooks } from 'common/react';
import { InfernoNode } from 'inferno';
import { Box, unit } from './Box';
import { Divider } from './Divider';
export const LabeledList = props => {
type LabeledListProps = {
children: InfernoNode;
};
export const LabeledList = (props: LabeledListProps) => {
const { children } = props;
return (
<table className="LabeledList">
@@ -19,7 +24,19 @@ export const LabeledList = props => {
LabeledList.defaultHooks = pureComponentHooks;
export const LabeledListItem = props => {
type LabeledListItemProps = {
className?: string | BooleanLike;
label?: string | BooleanLike;
labelColor?: string | BooleanLike;
color?: string | BooleanLike;
textAlign?: string | BooleanLike;
buttons?: InfernoNode,
/** @deprecated */
content?: any,
children?: InfernoNode;
};
const LabeledListItem = (props: LabeledListItemProps) => {
const {
className,
label,
@@ -68,7 +85,11 @@ export const LabeledListItem = props => {
LabeledListItem.defaultHooks = pureComponentHooks;
export const LabeledListDivider = props => {
type LabeledListDividerProps = {
size?: number;
};
const LabeledListDivider = (props: LabeledListDividerProps) => {
const padding = props.size
? unit(Math.max(0, props.size - 1))
: 0;
@@ -5,26 +5,42 @@
*/
import { canRender, classes } from 'common/react';
import { Component, createRef } from 'inferno';
import { Component, createRef, InfernoNode, RefObject } from 'inferno';
import { addScrollableNode, removeScrollableNode } from '../events';
import { computeBoxClassName, computeBoxProps } from './Box';
import { BoxProps, computeBoxClassName, computeBoxProps } from './Box';
interface SectionProps extends BoxProps {
className?: string;
title?: string;
buttons?: InfernoNode;
fill?: boolean;
fitted?: boolean;
scrollable?: boolean;
/** @deprecated This property no longer works, please remove it. */
level?: boolean;
/** @deprecated Please use `scrollable` property */
overflowY?: any;
}
export class Section extends Component<SectionProps> {
scrollableRef: RefObject<HTMLDivElement>;
scrollable: boolean;
export class Section extends Component {
constructor(props) {
super(props);
this.ref = createRef();
this.scrollableRef = createRef();
this.scrollable = props.scrollable;
}
componentDidMount() {
if (this.scrollable) {
addScrollableNode(this.ref.current);
addScrollableNode(this.scrollableRef.current);
}
}
componentWillUnmount() {
if (this.scrollable) {
removeScrollableNode(this.ref.current);
removeScrollableNode(this.scrollableRef.current);
}
}
@@ -32,7 +48,6 @@ export class Section extends Component {
const {
className,
title,
level = 1,
buttons,
fill,
fitted,
@@ -41,27 +56,16 @@ export class Section extends Component {
...rest
} = this.props;
const hasTitle = canRender(title) || canRender(buttons);
const content = fitted
? children
: (
<div
ref={this.ref}
className="Section__content">
{children}
</div>
);
return (
<div
ref={fitted ? this.ref : undefined}
className={classes([
'Section',
'Section--level--' + level,
Byond.IS_LTE_IE8 && 'Section--iefix',
fill && 'Section--fill',
fitted && 'Section--fitted',
scrollable && 'Section--scrollable',
className,
...computeBoxClassName(rest),
computeBoxClassName(rest),
])}
{...computeBoxProps(rest)}>
{hasTitle && (
@@ -74,7 +78,11 @@ export class Section extends Component {
</div>
</div>
)}
{content}
<div className="Section__rest">
<div ref={this.scrollableRef} className="Section__content">
{children}
</div>
</div>
</div>
);
}
+64
View File
@@ -0,0 +1,64 @@
/**
* @file
* @copyright 2021 Aleksej Komarov
* @license MIT
*/
import { classes } from 'common/react';
import { Flex, FlexItemProps, FlexProps } from './Flex';
interface StackProps extends FlexProps {
vertical?: boolean;
fill?: boolean;
}
export const Stack = (props: StackProps) => {
const { className, vertical, fill, ...rest } = props;
return (
<Flex
className={classes([
'Stack',
fill && 'Stack--fill',
vertical
? 'Stack--vertical'
: 'Stack--horizontal',
className,
])}
direction={vertical ? 'column' : 'row'}
{...rest} />
);
};
const StackItem = (props: FlexProps) => {
const { className, ...rest } = props;
return (
<Flex.Item
className={classes([
'Stack__item',
className,
])}
{...rest} />
);
};
Stack.Item = StackItem;
interface StackDividerProps extends FlexItemProps {
hidden?: boolean;
}
const StackDivider = (props: StackDividerProps) => {
const { className, hidden, ...rest } = props;
return (
<Flex.Item
className={classes([
'Stack__item',
'Stack__divider',
hidden && 'Stack__divider--hidden',
className,
])}
{...rest} />
);
};
Stack.Divider = StackDivider;
+2
View File
@@ -12,6 +12,7 @@ export const Tabs = props => {
const {
className,
vertical,
fill,
fluid,
children,
...rest
@@ -23,6 +24,7 @@ export const Tabs = props => {
vertical
? 'Tabs--vertical'
: 'Tabs--horizontal',
fill && 'Tabs--fill',
fluid && 'Tabs--fluid',
className,
computeBoxClassName(rest),
+3 -2
View File
@@ -31,8 +31,9 @@ export { ProgressBar } from './ProgressBar';
export { RoundGauge } from './RoundGauge';
export { Section } from './Section';
export { Slider } from './Slider';
export { Stack } from './Stack';
export { Table } from './Table';
export { TextArea } from './TextArea';
export { Tabs } from './Tabs';
export { Tooltip } from './Tooltip';
export { TextArea } from './TextArea';
export { TimeDisplay } from './TimeDisplay';
export { Tooltip } from './Tooltip';
+5
View File
@@ -30,6 +30,11 @@ export const COLORS = {
burn: '#e67e22',
brute: '#e74c3c',
},
// reagent / chemistry related colours
reagent: {
acidicbuffer: "#fbc314",
basicbuffer: "#3853a4",
},
};
// Colors defined in CSS
+18 -642
View File
@@ -4,117 +4,46 @@
* @license MIT
*/
import { Fragment } from 'inferno';
import { useBackend, useLocalState } from '../backend';
import { Blink, BlockQuote, Box, Button, ByondUi, Collapsible, DraggableControl, Flex, Icon, Input, Knob, LabeledList, NoticeBox, NumberInput, ProgressBar, Section, Slider, Tabs, Tooltip } from '../components';
import { formatSiUnit } from '../format';
import { useLocalState } from '../backend';
import { Flex, Section, Tabs } from '../components';
import { Pane, Window } from '../layouts';
import { createLogger } from '../logging';
const logger = createLogger('KitchenSink');
const r = require.context('../stories', false, /\.stories\.js$/);
const COLORS_SPECTRUM = [
'red',
'orange',
'yellow',
'olive',
'green',
'teal',
'blue',
'violet',
'purple',
'pink',
'brown',
'grey',
];
const COLORS_STATES = [
'good',
'average',
'bad',
'black',
'white',
];
const PAGES = [
{
title: 'Button',
component: () => KitchenSinkButton,
},
{
title: 'Blink',
component: () => KitchenSinkBlinker,
},
{
title: 'Box',
component: () => KitchenSinkBox,
},
{
title: 'Flex & Sections',
component: () => KitchenSinkFlexAndSections,
},
{
title: 'ProgressBar',
component: () => KitchenSinkProgressBar,
},
{
title: 'Tabs',
component: () => KitchenSinkTabs,
},
{
title: 'Tooltip',
component: () => KitchenSinkTooltip,
},
{
title: 'Input / Control',
component: () => KitchenSinkInput,
},
{
title: 'Collapsible',
component: () => KitchenSinkCollapsible,
},
{
title: 'BlockQuote',
component: () => KitchenSinkBlockQuote,
},
{
title: 'ByondUi',
component: () => KitchenSinkByondUi,
},
{
title: 'Themes',
component: () => KitchenSinkThemes,
},
{
title: 'Storage',
component: () => KitchenSinkStorage,
},
];
/**
* @returns {{
* meta: {
* title: string,
* render: () => any,
* },
* }[]}
*/
const getStories = () => r.keys().map(path => r(path));
export const KitchenSink = (props, context) => {
const { panel } = props;
const [theme] = useLocalState(context, 'kitchenSinkTheme');
const [pageIndex, setPageIndex] = useLocalState(context, 'pageIndex', 0);
const PageComponent = PAGES[pageIndex].component();
const stories = getStories();
const story = stories[pageIndex];
const Layout = panel ? Pane : Window;
return (
<Layout
title="Kitchen Sink"
width={600}
height={500}
theme={theme}
resizable>
theme={theme}>
<Flex height="100%">
<Flex.Item m={1} mr={0}>
<Section fill fitted>
<Tabs vertical>
{PAGES.map((page, i) => (
{stories.map((story, i) => (
<Tabs.Tab
key={i}
color="transparent"
selected={i === pageIndex}
onClick={() => setPageIndex(i)}>
{page.title}
{story.meta.title}
</Tabs.Tab>
))}
</Tabs>
@@ -124,563 +53,10 @@ export const KitchenSink = (props, context) => {
position="relative"
grow={1}>
<Layout.Content scrollable>
<PageComponent />
{story.meta.render()}
</Layout.Content>
</Flex.Item>
</Flex>
</Layout>
);
};
const KitchenSinkButton = props => {
return (
<Section>
<Box mb={1}>
<Button content="Simple" />
<Button selected content="Selected" />
<Button altSelected content="Alt Selected" />
<Button disabled content="Disabled" />
<Button color="transparent" content="Transparent" />
<Button icon="cog" content="Icon" />
<Button icon="power-off" />
<Button fluid content="Fluid" />
<Button
my={1}
lineHeight={2}
minWidth={15}
textAlign="center"
content="With Box props" />
</Box>
<Box mb={1}>
{COLORS_STATES.map(color => (
<Button
key={color}
color={color}
content={color} />
))}
<br />
{COLORS_SPECTRUM.map(color => (
<Button
key={color}
color={color}
content={color} />
))}
<br />
{COLORS_SPECTRUM.map(color => (
<Box inline
mx="7px"
key={color}
color={color}>
{color}
</Box>
))}
</Box>
</Section>
);
};
const KitchenSinkBlinker = props => {
return (
<Section>
<Blink>
Blink
</Blink>
</Section>
);
};
const KitchenSinkBox = props => {
return (
<Section>
<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>
</Section>
);
};
const KitchenSinkFlexAndSections = (props, context) => {
const [grow, setGrow] = useLocalState(
context, 'fs_grow', 1);
const [direction, setDirection] = useLocalState(
context, 'fs_direction', 'column');
const [fill, setFill] = useLocalState(
context, 'fs_fill', true);
const [hasTitle, setHasTitle] = useLocalState(
context, 'fs_title', true);
return (
<Flex
height="100%"
direction="column">
<Flex.Item mb={1}>
<Section>
<Button
fluid
onClick={() => setDirection(
direction === 'column' ? 'row' : 'column'
)}>
{`Flex direction="${direction}"`}
</Button>
<Button
fluid
onClick={() => setGrow(Number(!grow))}>
{`Flex.Item grow={${grow}}`}
</Button>
<Button
fluid
onClick={() => setFill(!fill)}>
{`Section fill={${String(fill)}}`}
</Button>
<Button
fluid
selected={hasTitle}
onClick={() => setHasTitle(!hasTitle)}>
{`Section title`}
</Button>
</Section>
</Flex.Item>
<Flex.Item grow={1}>
<Flex
height="100%"
direction={direction}>
<Flex.Item
mr={direction === 'row' && 1}
mb={direction === 'column' && 1}
grow={grow}>
<Section
title={hasTitle && "Section 1"}
fill={fill}>
Content
</Section>
</Flex.Item>
<Flex.Item grow={grow}>
<Section
title={hasTitle && "Section 2"}
fill={fill}>
Content
</Section>
</Flex.Item>
</Flex>
</Flex.Item>
</Flex>
);
};
const KitchenSinkProgressBar = (props, context) => {
const [
progress,
setProgress,
] = useLocalState(context, 'progress', 0.5);
return (
<Section>
<ProgressBar
ranges={{
good: [0.5, Infinity],
bad: [-Infinity, 0.1],
average: [0, 0.5],
}}
minValue={-1}
maxValue={1}
value={progress}>
Value: {Number(progress).toFixed(1)}
</ProgressBar>
<Box mt={1}>
<Button
content="-0.1"
onClick={() => setProgress(progress - 0.1)} />
<Button
content="+0.1"
onClick={() => setProgress(progress + 0.1)} />
</Box>
</Section>
);
};
const KitchenSinkTabs = (props, context) => {
const [tabIndex, setTabIndex] = useLocalState(context, 'tabIndex', 0);
const [tabProps, setTabProps] = useLocalState(context, 'tabProps', {});
const TAB_RANGE = [
'Tab #1',
'Tab #2',
'Tab #3',
'Tab #4',
];
return (
<Fragment>
<Section>
<Button.Checkbox
inline
content="vertical"
checked={tabProps.vertical}
onClick={() => setTabProps({
...tabProps,
vertical: !tabProps.vertical,
})} />
<Button.Checkbox
inline
content="leftSlot"
checked={tabProps.leftSlot}
onClick={() => setTabProps({
...tabProps,
leftSlot: !tabProps.leftSlot,
})} />
<Button.Checkbox
inline
content="rightSlot"
checked={tabProps.rightSlot}
onClick={() => setTabProps({
...tabProps,
rightSlot: !tabProps.rightSlot,
})} />
<Button.Checkbox
inline
content="icon"
checked={tabProps.icon}
onClick={() => setTabProps({
...tabProps,
icon: !tabProps.icon,
})} />
<Button.Checkbox
inline
content="fluid"
checked={tabProps.fluid}
onClick={() => setTabProps({
...tabProps,
fluid: !tabProps.fluid,
})} />
<Button.Checkbox
inline
content="left aligned"
checked={tabProps.leftAligned}
onClick={() => setTabProps({
...tabProps,
leftAligned: !tabProps.leftAligned,
})} />
</Section>
<Section fitted>
<Tabs
vertical={tabProps.vertical}
fluid={tabProps.fluid}
textAlign={tabProps.leftAligned && 'left'}>
{TAB_RANGE.map((text, i) => (
<Tabs.Tab
key={i}
selected={i === tabIndex}
icon={tabProps.icon && 'info-circle'}
leftSlot={tabProps.leftSlot && (
<Button
circular
compact
color="transparent"
icon="times" />
)}
rightSlot={tabProps.rightSlot && (
<Button
circular
compact
color="transparent"
icon="times" />
)}
onClick={() => setTabIndex(i)}>
{text}
</Tabs.Tab>
))}
</Tabs>
</Section>
</Fragment>
);
};
const KitchenSinkTooltip = props => {
const positions = [
'top',
'left',
'right',
'bottom',
'bottom-left',
'bottom-right',
];
return (
<Section>
<Box>
<Box inline position="relative" mr={1}>
Box (hover me).
<Tooltip content="Tooltip text." />
</Box>
<Button
tooltip="Tooltip text."
content="Button" />
</Box>
<Box mt={1}>
{positions.map(position => (
<Button
key={position}
color="transparent"
tooltip="Tooltip text."
tooltipPosition={position}
content={position} />
))}
</Box>
</Section>
);
};
const KitchenSinkInput = (props, context) => {
const [
number,
setNumber,
] = useLocalState(context, 'number', 0);
const [
text,
setText,
] = useLocalState(context, 'text', "Sample text");
return (
<Section>
<LabeledList>
<LabeledList.Item label="Input (onChange)">
<Input
value={text}
onChange={(e, value) => setText(value)} />
</LabeledList.Item>
<LabeledList.Item label="Input (onInput)">
<Input
value={text}
onInput={(e, value) => setText(value)} />
</LabeledList.Item>
<LabeledList.Item label="NumberInput (onChange)">
<NumberInput
animated
width="40px"
step={1}
stepPixelSize={5}
value={number}
minValue={-100}
maxValue={100}
onChange={(e, value) => setNumber(value)} />
</LabeledList.Item>
<LabeledList.Item label="NumberInput (onDrag)">
<NumberInput
animated
width="40px"
step={1}
stepPixelSize={5}
value={number}
minValue={-100}
maxValue={100}
onDrag={(e, value) => setNumber(value)} />
</LabeledList.Item>
<LabeledList.Item label="Slider (onDrag)">
<Slider
step={1}
stepPixelSize={5}
value={number}
minValue={-100}
maxValue={100}
onDrag={(e, value) => setNumber(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) => setNumber(value)} />
<Knob
ml={1}
inline
bipolar
size={1}
step={1}
stepPixelSize={2}
value={number}
minValue={-100}
maxValue={100}
onDrag={(e, value) => setNumber(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) => setNumber(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>
</Section>
);
};
const KitchenSinkCollapsible = props => {
return (
<Section>
<Collapsible
title="Collapsible Demo"
buttons={(
<Button icon="cog" />
)}>
<BoxWithSampleText />
</Collapsible>
</Section>
);
};
const BoxWithSampleText = props => {
return (
<Box {...props}>
<Box italic>
Jackdaws love my big sphinx of quartz.
</Box>
<Box mt={1} bold>
The wide electrification of the southern
provinces will give a powerful impetus to the
growth of agriculture.
</Box>
</Box>
);
};
const KitchenSinkBlockQuote = props => {
return (
<Section>
<BlockQuote>
<BoxWithSampleText />
</BlockQuote>
</Section>
);
};
const KitchenSinkByondUi = (props, context) => {
const { config } = useBackend(context);
const [code, setCode] = useLocalState(context,
'byondUiEvalCode',
`Byond.winset('${window.__windowId__}', {\n 'is-visible': true,\n})`);
return (
<Fragment>
<Section title="Button">
<ByondUi
params={{
type: 'button',
text: 'Button',
}} />
</Section>
<Section
title="Make BYOND calls"
buttons={(
<Button
icon="chevron-right"
onClick={() => setImmediate(() => {
try {
const result = new Function('return (' + code + ')')();
if (result && result.then) {
logger.log('Promise');
result.then(logger.log);
}
else {
logger.log(result);
}
}
catch (err) {
logger.log(err);
}
})}>
Evaluate
</Button>
)}>
<Box
as="textarea"
width="100%"
height="10em"
onChange={e => setCode(e.target.value)}>
{code}
</Box>
</Section>
</Fragment>
);
};
const KitchenSinkThemes = (props, context) => {
const [theme, setTheme] = useLocalState(context, 'kitchenSinkTheme');
return (
<Section>
<LabeledList>
<LabeledList.Item label="Use theme">
<Input
placeholder="theme_name"
value={theme}
onInput={(e, value) => setTheme(value)} />
</LabeledList.Item>
</LabeledList>
</Section>
);
};
const KitchenSinkStorage = (props, context) => {
if (!window.localStorage) {
return (
<NoticeBox>
Local storage is not available.
</NoticeBox>
);
}
return (
<Section
title="Local Storage"
buttons={(
<Button
icon="recycle"
onClick={() => {
localStorage.clear();
}}>
Clear
</Button>
)}>
<LabeledList>
<LabeledList.Item label="Keys in use">
{localStorage.length}
</LabeledList.Item>
<LabeledList.Item label="Remaining space">
{formatSiUnit(localStorage.remainingSpace, 0, 'B')}
</LabeledList.Item>
</LabeledList>
</Section>
);
};
+13 -8
View File
@@ -107,14 +107,24 @@ export const recallWindowGeometry = async (options = {}) => {
logger.log('recalled geometry:', geometry);
}
let pos = geometry?.pos || options.pos;
const size = options.size;
let size = options.size;
// Wait until screen offset gets resolved
await screenOffsetPromise;
const areaAvailable = [
window.screen.availWidth,
window.screen.availHeight,
];
// Set window size
if (size) {
// Constraint size to not exceed available screen area.
size = [
Math.min(areaAvailable[0], size[0]),
Math.min(areaAvailable[1], size[1]),
];
setWindowSize(size);
}
// Set window position
if (pos) {
await screenOffsetPromise;
// Constraint window position if monitor lock was set in preferences.
if (size && options.locked) {
pos = constraintPosition(pos, size)[1];
@@ -123,12 +133,7 @@ export const recallWindowGeometry = async (options = {}) => {
}
// Set window position at the center of the screen.
else if (size) {
await screenOffsetPromise;
const areaAvailable = [
window.screen.availWidth - Math.abs(screenOffset[0]),
window.screen.availHeight - Math.abs(screenOffset[1]),
];
const pos = vecAdd(
pos = vecAdd(
vecScale(areaAvailable, 0.5),
vecScale(size, -0.5),
vecScale(screenOffset, -1.0));
+1
View File
@@ -71,6 +71,7 @@ const setupApp = () => {
setupHotReloading();
module.hot.accept([
'./components',
'./debug',
'./layouts',
'./routes',
], () => {
+1 -3
View File
@@ -14,7 +14,6 @@ export const NtosWindow = (props, context) => {
title,
width = 575,
height = 700,
resizable,
theme = 'ntos',
children,
} = props;
@@ -35,8 +34,7 @@ export const NtosWindow = (props, context) => {
title={title}
width={width}
height={height}
theme={theme}
resizable={resizable}>
theme={theme}>
<div className="NtosWindow">
<div className="NtosWindow__header NtosHeader">
<div className="NtosHeader__left">
+20 -6
View File
@@ -7,7 +7,7 @@
import { classes } from 'common/react';
import { useDispatch } from 'common/redux';
import { decodeHtmlEntities, toTitleCase } from 'common/string';
import { Component, Fragment } from 'inferno';
import { Component } from 'inferno';
import { backendSuspendStart, useBackend } from '../backend';
import { Icon } from '../components';
import { UI_DISABLED, UI_INTERACTIVE, UI_UPDATE } from '../constants';
@@ -23,11 +23,26 @@ const DEFAULT_SIZE = [400, 600];
export class Window extends Component {
componentDidMount() {
const { config, suspended } = useBackend(this.context);
const { suspended } = useBackend(this.context);
if (suspended) {
return;
}
logger.log('mounting');
this.updateGeometry();
}
componentDidUpdate(prevProps) {
const shouldUpdateGeometry = (
this.props.width !== prevProps.width
|| this.props.height !== prevProps.height
);
if (shouldUpdateGeometry) {
this.updateGeometry();
}
}
updateGeometry() {
const { config } = useBackend(this.context);
const options = {
size: DEFAULT_SIZE,
...config.window,
@@ -43,7 +58,6 @@ export class Window extends Component {
render() {
const {
resizable,
noClose,
theme,
title,
@@ -87,15 +101,15 @@ export class Window extends Component {
<div className="Window__dimmer" />
)}
</div>
{fancy && resizable && (
<Fragment>
{fancy && (
<>
<div className="Window__resizeHandle__e"
onMousedown={resizeStartHandler(1, 0)} />
<div className="Window__resizeHandle__s"
onMousedown={resizeStartHandler(0, 1)} />
<div className="Window__resizeHandle__se"
onMousedown={resizeStartHandler(1, 1)} />
</Fragment>
</>
)}
</Layout>
);
+13 -4
View File
@@ -10,12 +10,21 @@
export const captureExternalLinks = () => {
// Subscribe to all document clicks
document.addEventListener('click', e => {
const tagName = String(e.target.tagName).toLowerCase();
const hrefAttr = e.target.getAttribute('href') || '';
// Must be a link
if (tagName !== 'a') {
/** @type {HTMLElement} */
let target = e.target;
// Recurse down the tree to find a valid link
while (target && target !== document.body) {
const tagName = String(target.tagName).toLowerCase();
if (tagName === 'a') {
break;
}
target = target.parentElement;
}
// Not a link, do nothing.
if (!target) {
return;
}
const hrefAttr = target.getAttribute('href') || '';
// Leave BYOND links alone
const isByondLink = hrefAttr.charAt(0) === '?'
|| hrefAttr.startsWith('byond://');
+5 -5
View File
@@ -1,13 +1,13 @@
{
"private": true,
"name": "tgui",
"version": "4.2.0",
"version": "4.3.0",
"dependencies": {
"common": "workspace:*",
"dompurify": "^2.0.12",
"inferno": "^7.4.2",
"inferno-vnode-flags": "^7.4.2",
"marked": "^1.1.1",
"dompurify": "^2.2.6",
"inferno": "^7.4.8",
"inferno-vnode-flags": "^7.4.8",
"marked": "^2.0.0",
"tgui-dev-server": "workspace:*",
"tgui-polyfill": "workspace:*"
}
+2 -2
View File
@@ -12,7 +12,7 @@ const requireInterface = require.context('./interfaces', false, /\.js$/);
const routingError = (type, name) => () => {
return (
<Window resizable>
<Window>
<Window.Content scrollable>
{type === 'notFound' && (
<div>Interface <b>{name}</b> was not found.</div>
@@ -27,7 +27,7 @@ const routingError = (type, name) => () => {
const SuspendedWindow = () => {
return (
<Window resizable>
<Window>
<Window.Content scrollable />
</Window>
);
+20 -4
View File
@@ -6,10 +6,26 @@ import DOMPurify from 'dompurify';
// Default values
let defTag = [
'br', 'code', 'li', 'p', 'pre',
'span', 'table', 'td', 'tr', 'i',
'th', 'ul', 'ol', 'menu', 'font', 'b',
'center', 'table', 'tr', 'th', 'hr',
'b',
'br',
'center',
'code',
'div',
'font',
'hr',
'i',
'li',
'menu',
'ol',
'p',
'pre',
'span',
'table',
'td',
'th',
'tr',
'u',
'ul',
];
let defAttr = ['class', 'style'];
@@ -0,0 +1,22 @@
/**
* @file
* @copyright 2021 Aleksej Komarov
* @license MIT
*/
import { Blink, Section } from '../components';
export const meta = {
title: 'Blink',
render: () => <Story />,
};
const Story = (props, context) => {
return (
<Section>
<Blink>
Blink
</Blink>
</Section>
);
};
@@ -0,0 +1,23 @@
/**
* @file
* @copyright 2021 Aleksej Komarov
* @license MIT
*/
import { BlockQuote, Section } from '../components';
import { BoxWithSampleText } from './common';
export const meta = {
title: 'BlockQuote',
render: () => <Story />,
};
const Story = (props, context) => {
return (
<Section>
<BlockQuote>
<BoxWithSampleText />
</BlockQuote>
</Section>
);
};
+43
View File
@@ -0,0 +1,43 @@
/**
* @file
* @copyright 2021 Aleksej Komarov
* @license MIT
*/
import { Box, Section } from '../components';
export const meta = {
title: 'Box',
render: () => <Story />,
};
const Story = (props, context) => {
return (
<Section>
<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>
</Section>
);
};
@@ -0,0 +1,82 @@
/**
* @file
* @copyright 2021 Aleksej Komarov
* @license MIT
*/
import { Box, Button, Section } from '../components';
export const meta = {
title: 'Button',
render: () => <Story />,
};
const COLORS_SPECTRUM = [
'red',
'orange',
'yellow',
'olive',
'green',
'teal',
'blue',
'violet',
'purple',
'pink',
'brown',
'grey',
];
const COLORS_STATES = [
'good',
'average',
'bad',
'black',
'white',
];
const Story = (props, context) => {
return (
<Section>
<Box mb={1}>
<Button content="Simple" />
<Button selected content="Selected" />
<Button altSelected content="Alt Selected" />
<Button disabled content="Disabled" />
<Button color="transparent" content="Transparent" />
<Button icon="cog" content="Icon" />
<Button icon="power-off" />
<Button fluid content="Fluid" />
<Button
my={1}
lineHeight={2}
minWidth={15}
textAlign="center"
content="With Box props" />
</Box>
<Box mb={1}>
{COLORS_STATES.map(color => (
<Button
key={color}
color={color}
content={color} />
))}
<br />
{COLORS_SPECTRUM.map(color => (
<Button
key={color}
color={color}
content={color} />
))}
<br />
{COLORS_SPECTRUM.map(color => (
<Box inline
mx="7px"
key={color}
color={color}>
{color}
</Box>
))}
</Box>
</Section>
);
};
@@ -0,0 +1,62 @@
/**
* @file
* @copyright 2021 Aleksej Komarov
* @license MIT
*/
import { useLocalState } from '../backend';
import { Box, Button, ByondUi, Section } from '../components';
import { logger } from '../logging';
export const meta = {
title: 'ByondUi',
render: () => <Story />,
};
const Story = (props, context) => {
const [code, setCode] = useLocalState(context,
'byondUiEvalCode',
`Byond.winset('${window.__windowId__}', {\n 'is-visible': true,\n})`);
return (
<>
<Section title="Button">
<ByondUi
params={{
type: 'button',
text: 'Button',
}} />
</Section>
<Section
title="Make BYOND calls"
buttons={(
<Button
icon="chevron-right"
onClick={() => setImmediate(() => {
try {
const result = new Function('return (' + code + ')')();
if (result && result.then) {
logger.log('Promise');
result.then(logger.log);
}
else {
logger.log(result);
}
}
catch (err) {
logger.log(err);
}
})}>
Evaluate
</Button>
)}>
<Box
as="textarea"
width="100%"
height="10em"
onChange={e => setCode(e.target.value)}>
{code}
</Box>
</Section>
</>
);
};
@@ -0,0 +1,27 @@
/**
* @file
* @copyright 2021 Aleksej Komarov
* @license MIT
*/
import { Button, Collapsible, Section } from '../components';
import { BoxWithSampleText } from './common';
export const meta = {
title: 'Collapsible',
render: () => <Story />,
};
const Story = (props, context) => {
return (
<Section>
<Collapsible
title="Collapsible Demo"
buttons={(
<Button icon="cog" />
)}>
<BoxWithSampleText />
</Collapsible>
</Section>
);
};
@@ -0,0 +1,80 @@
/**
* @file
* @copyright 2021 Aleksej Komarov
* @license MIT
*/
import { useLocalState } from '../backend';
import { Button, Flex, Section } from '../components';
export const meta = {
title: 'Flex & Sections',
render: () => <Story />,
};
const Story = (props, context) => {
const [grow, setGrow] = useLocalState(
context, 'fs_grow', 1);
const [direction, setDirection] = useLocalState(
context, 'fs_direction', 'column');
const [fill, setFill] = useLocalState(
context, 'fs_fill', true);
const [hasTitle, setHasTitle] = useLocalState(
context, 'fs_title', true);
return (
<Flex
height="100%"
direction="column">
<Flex.Item mb={1}>
<Section>
<Button
fluid
onClick={() => setDirection(
direction === 'column' ? 'row' : 'column'
)}>
{`Flex direction="${direction}"`}
</Button>
<Button
fluid
onClick={() => setGrow(Number(!grow))}>
{`Flex.Item grow={${grow}}`}
</Button>
<Button
fluid
onClick={() => setFill(!fill)}>
{`Section fill={${String(fill)}}`}
</Button>
<Button
fluid
selected={hasTitle}
onClick={() => setHasTitle(!hasTitle)}>
{`Section title`}
</Button>
</Section>
</Flex.Item>
<Flex.Item grow={1}>
<Flex
height="100%"
direction={direction}>
<Flex.Item
mr={direction === 'row' && 1}
mb={direction === 'column' && 1}
grow={grow}>
<Section
title={hasTitle && 'Section 1'}
fill={fill}>
Content
</Section>
</Flex.Item>
<Flex.Item grow={grow}>
<Section
title={hasTitle && 'Section 2'}
fill={fill}>
Content
</Section>
</Flex.Item>
</Flex>
</Flex.Item>
</Flex>
);
};
+116
View File
@@ -0,0 +1,116 @@
/**
* @file
* @copyright 2021 Aleksej Komarov
* @license MIT
*/
import { useLocalState } from '../backend';
import { Box, DraggableControl, Icon, Input, Knob, LabeledList, NumberInput, Section, Slider } from '../components';
export const meta = {
title: 'Input',
render: () => <Story />,
};
const Story = (props, context) => {
const [
number,
setNumber,
] = useLocalState(context, 'number', 0);
const [
text,
setText,
] = useLocalState(context, 'text', "Sample text");
return (
<Section>
<LabeledList>
<LabeledList.Item label="Input (onChange)">
<Input
value={text}
onChange={(e, value) => setText(value)} />
</LabeledList.Item>
<LabeledList.Item label="Input (onInput)">
<Input
value={text}
onInput={(e, value) => setText(value)} />
</LabeledList.Item>
<LabeledList.Item label="NumberInput (onChange)">
<NumberInput
animated
width="40px"
step={1}
stepPixelSize={5}
value={number}
minValue={-100}
maxValue={100}
onChange={(e, value) => setNumber(value)} />
</LabeledList.Item>
<LabeledList.Item label="NumberInput (onDrag)">
<NumberInput
animated
width="40px"
step={1}
stepPixelSize={5}
value={number}
minValue={-100}
maxValue={100}
onDrag={(e, value) => setNumber(value)} />
</LabeledList.Item>
<LabeledList.Item label="Slider (onDrag)">
<Slider
step={1}
stepPixelSize={5}
value={number}
minValue={-100}
maxValue={100}
onDrag={(e, value) => setNumber(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) => setNumber(value)} />
<Knob
ml={1}
inline
bipolar
size={1}
step={1}
stepPixelSize={2}
value={number}
minValue={-100}
maxValue={100}
onDrag={(e, value) => setNumber(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) => setNumber(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>
</Section>
);
};
@@ -0,0 +1,43 @@
/**
* @file
* @copyright 2021 Aleksej Komarov
* @license MIT
*/
import { useLocalState } from '../backend';
import { Box, Button, ProgressBar, Section } from '../components';
export const meta = {
title: 'ProgressBar',
render: () => <Story />,
};
const Story = (props, context) => {
const [
progress,
setProgress,
] = useLocalState(context, 'progress', 0.5);
return (
<Section>
<ProgressBar
ranges={{
good: [0.5, Infinity],
bad: [-Infinity, 0.1],
average: [0, 0.5],
}}
minValue={-1}
maxValue={1}
value={progress}>
Value: {Number(progress).toFixed(1)}
</ProgressBar>
<Box mt={1}>
<Button
content="-0.1"
onClick={() => setProgress(progress - 0.1)} />
<Button
content="+0.1"
onClick={() => setProgress(progress + 0.1)} />
</Box>
</Section>
);
};
@@ -0,0 +1,55 @@
/**
* @file
* @copyright 2021 Aleksej Komarov
* @license MIT
*/
import { Box, Section, Stack } from '../components';
export const meta = {
title: 'Stack',
render: () => <Story />,
};
const Filler = () => (
<Box inline width={1} height={1}>
A
</Box>
);
const SmallStackItems = () => (
<>
<Stack.Item>
<Filler />
</Stack.Item>
<Stack.Divider />
<Stack.Item>
<Filler />
</Stack.Item>
</>
);
const Story = (props, context) => {
return (
<Section fill>
<Stack fill className="debug-layout">
<SmallStackItems />
<Stack.Item grow={1}>
<Stack fill vertical>
<SmallStackItems />
<Stack.Item>
<Stack fill>
<SmallStackItems />
<Stack.Item grow={1} />
<SmallStackItems />
<SmallStackItems />
</Stack>
</Stack.Item>
<Stack.Item grow={1} />
<SmallStackItems />
</Stack>
</Stack.Item>
</Stack>
</Section>
);
};
@@ -0,0 +1,47 @@
/**
* @file
* @copyright 2021 Aleksej Komarov
* @license MIT
*/
import { storage } from 'common/storage';
import { Button, LabeledList, NoticeBox, Section } from '../components';
import { formatSiUnit } from '../format';
export const meta = {
title: 'Storage',
render: () => <Story />,
};
const Story = (props, context) => {
if (!window.localStorage) {
return (
<NoticeBox>
Local storage is not available.
</NoticeBox>
);
}
return (
<Section
title="Local Storage"
buttons={(
<Button
icon="recycle"
onClick={() => {
localStorage.clear();
storage.clear();
}}>
Clear
</Button>
)}>
<LabeledList>
<LabeledList.Item label="Keys in use">
{localStorage.length}
</LabeledList.Item>
<LabeledList.Item label="Remaining space">
{formatSiUnit(localStorage.remainingSpace, 0, 'B')}
</LabeledList.Item>
</LabeledList>
</Section>
);
};
+124
View File
@@ -0,0 +1,124 @@
/**
* @file
* @copyright 2021 Aleksej Komarov
* @license MIT
*/
import { useLocalState } from '../backend';
import { Box, Button, Divider, Section, Tabs } from '../components';
export const meta = {
title: 'Tabs',
render: () => <Story />,
};
const TAB_RANGE = [
'Tab #1',
'Tab #2',
'Tab #3',
'Tab #4',
];
const Story = (props, context) => {
const [tabProps, setTabProps] = useLocalState(context, 'tabProps', {});
return (
<>
<Section>
<Button.Checkbox
inline
content="vertical"
checked={tabProps.vertical}
onClick={() => setTabProps({
...tabProps,
vertical: !tabProps.vertical,
})} />
<Button.Checkbox
inline
content="leftSlot"
checked={tabProps.leftSlot}
onClick={() => setTabProps({
...tabProps,
leftSlot: !tabProps.leftSlot,
})} />
<Button.Checkbox
inline
content="rightSlot"
checked={tabProps.rightSlot}
onClick={() => setTabProps({
...tabProps,
rightSlot: !tabProps.rightSlot,
})} />
<Button.Checkbox
inline
content="icon"
checked={tabProps.icon}
onClick={() => setTabProps({
...tabProps,
icon: !tabProps.icon,
})} />
<Button.Checkbox
inline
content="fluid"
checked={tabProps.fluid}
onClick={() => setTabProps({
...tabProps,
fluid: !tabProps.fluid,
})} />
<Button.Checkbox
inline
content="centered"
checked={tabProps.centered}
onClick={() => setTabProps({
...tabProps,
centered: !tabProps.centered,
})} />
</Section>
<Section fitted>
<TabsPrefab />
</Section>
<Section title="Normal section">
<TabsPrefab />
Some text
</Section>
<Section>
Section-less tabs appear the same as tabs in a fitted section:
</Section>
<TabsPrefab />
</>
);
};
const TabsPrefab = (props, context) => {
const [tabIndex, setTabIndex] = useLocalState(context, 'tabIndex', 0);
const [tabProps] = useLocalState(context, 'tabProps', {});
return (
<Tabs
vertical={tabProps.vertical}
fluid={tabProps.fluid}
textAlign={tabProps.centered && 'center'}>
{TAB_RANGE.map((text, i) => (
<Tabs.Tab
key={i}
selected={i === tabIndex}
icon={tabProps.icon && 'info-circle'}
leftSlot={tabProps.leftSlot && (
<Button
circular
compact
color="transparent"
icon="times" />
)}
rightSlot={tabProps.rightSlot && (
<Button
circular
compact
color="transparent"
icon="times" />
)}
onClick={() => setTabIndex(i)}>
{text}
</Tabs.Tab>
))}
</Tabs>
);
};
@@ -0,0 +1,29 @@
/**
* @file
* @copyright 2021 Aleksej Komarov
* @license MIT
*/
import { useLocalState } from '../backend';
import { Input, LabeledList, Section } from '../components';
export const meta = {
title: 'Themes',
render: () => <Story />,
};
const Story = (props, context) => {
const [theme, setTheme] = useLocalState(context, 'kitchenSinkTheme');
return (
<Section>
<LabeledList>
<LabeledList.Item label="Use theme">
<Input
placeholder="theme_name"
value={theme}
onInput={(e, value) => setTheme(value)} />
</LabeledList.Item>
</LabeledList>
</Section>
);
};
@@ -0,0 +1,46 @@
/**
* @file
* @copyright 2021 Aleksej Komarov
* @license MIT
*/
import { Box, Button, Section, Tooltip } from '../components';
export const meta = {
title: 'Tooltip',
render: () => <Story />,
};
const Story = props => {
const positions = [
'top',
'left',
'right',
'bottom',
'bottom-left',
'bottom-right',
];
return (
<Section>
<Box>
<Box inline position="relative" mr={1}>
Box (hover me).
<Tooltip content="Tooltip text." />
</Box>
<Button
tooltip="Tooltip text."
content="Button" />
</Box>
<Box mt={1}>
{positions.map(position => (
<Button
key={position}
color="transparent"
tooltip="Tooltip text."
tooltipPosition={position}
content={position} />
))}
</Box>
</Section>
);
};
+22
View File
@@ -0,0 +1,22 @@
/**
* @file
* @copyright 2021 Aleksej Komarov
* @license MIT
*/
import { Box } from '../components';
export const BoxWithSampleText = props => {
return (
<Box {...props}>
<Box italic>
Jackdaws love my big sphinx of quartz.
</Box>
<Box mt={1} bold>
The wide electrification of the southern
provinces will give a powerful impetus to the
growth of agriculture.
</Box>
</Box>
);
};
@@ -21,7 +21,9 @@
.Dropdown__arrow-button {
float: right;
padding-left: 0.5em;
padding-left: 0.35em;
width: 1.2em;
height: base.em(22px);
border-left: base.em(1px) solid #000;
border-left: base.em(1px) solid rgba(0, 0, 0, 0.25);
}
@@ -56,7 +58,7 @@
font-family: Verdana, sans-serif;
font-size: base.em(12px);
line-height: base.em(17px);
transition: background-color 100ms;
transition: background-color 100ms ease-out;
&:hover {
background-color: rgba(255, 255, 255, 0.2);
@@ -68,3 +70,12 @@
top: auto;
bottom: 100%
}
.Dropdown__selected-text {
display: inline-block;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
height: base.em(17px);
width: calc(100% - 1.2em);
}
@@ -50,13 +50,3 @@
.Flex__item--iefix--grow {
width: auto !important;
}
@for $i from 1 through 2 {
.Flex--spacing--#{$i} {
margin: 0 -0.25em * $i;
& > .Flex__item {
margin: 0 0.25em * $i;
}
}
}
@@ -117,7 +117,7 @@ $pi: 3.1416;
stroke-width: 8;
stroke-linecap: round;
stroke-dasharray: 100 * $pi;
transition: stroke 50ms;
transition: stroke 50ms ease-out;
}
@each $color-name, $color-value in $fg-map {
@@ -19,7 +19,7 @@ $bg-map: colors.$bg-map !default;
padding: 0 0.5em;
border-radius: $border-radius;
background-color: $background-color;
transition: border-color 500ms;
transition: border-color 900ms ease-out;
}
.ProgressBar__fill {
@@ -30,7 +30,7 @@ $bg-map: colors.$bg-map !default;
}
.ProgressBar__fill--animated {
transition: background-color 500ms, width 500ms;
transition: background-color 900ms ease-out, width 900ms ease-out;
}
.ProgressBar__content {
@@ -33,7 +33,7 @@ $pi: 3.1416;
stroke: $ring-color;
stroke-width: 10;
stroke-dasharray: 100 * $pi;
transition: stroke 50ms;
transition: stroke 50ms ease-out;
}
.RoundGauge__needle, .RoundGauge__ringFill {
@@ -11,10 +11,6 @@
$title-text-color: base.$color-fg !default;
$background-color: base.$color-bg-section !default;
$separator-color: colors.$primary !default;
$shadow-color: rgba(0, 0, 0, 0.5) !default;
$shadow-type: inset !default;
$shadow-size: 0.5em !default;
$shadow-offset: 0 0 !default;
.Section {
position: relative;
@@ -47,34 +43,48 @@ $shadow-offset: 0 0 !default;
margin-top: base.em(-1px);
}
.Section__rest {
position: relative;
}
.Section__content {
padding: 0.66em 0.5em;
}
.Section--fitted > .Section__rest > .Section__content {
padding: 0;
}
.Section--fill {
display: flex;
flex-direction: column;
height: 100%;
}
.Section--scrollable .Section__content {
overflow-y: scroll;
overflow-x: hidden;
}
.Section--fill .Section__content {
.Section--fill > .Section__rest {
flex-grow: 1;
}
.Section--iefix.Section--fill {
.Section--fill > .Section__rest > .Section__content {
height: 100%;
}
.Section--fill.Section--scrollable > .Section__rest > .Section__content {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.Section--fill.Section--iefix {
display: table !important;
width: 100% !important;
height: 100% !important;
border-collapse: collapse;
border-spacing: 0;
& .Section__content {
& > .Section__rest {
display: table-row !important;
height: 100% !important;
}
@@ -83,24 +93,31 @@ $shadow-offset: 0 0 !default;
.Section--scrollable {
overflow-x: hidden;
overflow-y: hidden;
& > .Section__rest > .Section__content {
overflow-y: scroll;
overflow-x: hidden;
}
}
.Section--level--1 .Section__titleText {
font-size: base.em(14px);
// Nested sections
.Section .Section {
background-color: transparent;
margin-left: -0.5em;
margin-right: -0.5em;
// Remove extra space above the first nested section
&:first-child {
margin-top: -0.5em;
}
}
.Section--level--2 .Section__titleText {
// Level 2 section title
.Section .Section .Section__titleText {
font-size: base.em(13px);
}
.Section--level--3 .Section__titleText {
// Level 3 section title
.Section .Section .Section .Section__titleText {
font-size: base.em(12px);
}
.Section--level--2,
.Section--level--3 {
background-color: transparent;
box-shadow: none;
margin-left: -0.5em;
margin-right: -0.5em;
}

Some files were not shown because too many files have changed in this diff Show More