building pipeline
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -1,6 +1,6 @@
|
||||
/.yarn/**
|
||||
/**/node_modules
|
||||
/**/*.bundle.*
|
||||
/**/*.chunk.*
|
||||
/**/*.hot-update.*
|
||||
/packages/inferno/**
|
||||
/packages/tgui/public/shim-*.js
|
||||
|
||||
@@ -12,3 +12,4 @@ rules:
|
||||
selfClosing: after-props,
|
||||
nonEmpty: after-props,
|
||||
}]
|
||||
react/display-name: error
|
||||
|
||||
+6
-4
@@ -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
@@ -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
@@ -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
Vendored
-55
File diff suppressed because one or more lines are too long
Vendored
+55
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -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";
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eslint",
|
||||
"version": "7.4.0-pnpify",
|
||||
"version": "7.19.0-pnpify",
|
||||
"main": "./lib/api.js",
|
||||
"type": "commonjs"
|
||||
}
|
||||
|
||||
Vendored
+20
@@ -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
@@ -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
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "typescript",
|
||||
"version": "4.1.5-pnpify",
|
||||
"main": "./lib/typescript.js",
|
||||
"type": "commonjs"
|
||||
}
|
||||
+21
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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,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
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
Vendored
+134
-110
@@ -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 {};
|
||||
|
||||
@@ -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
@@ -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,5 +1,5 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "common",
|
||||
"version": "4.2.0"
|
||||
"version": "4.3.0"
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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,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();
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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:*"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)} />
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
|
||||
+25
-4
@@ -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;
|
||||
+28
-20
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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),
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -30,6 +30,11 @@ export const COLORS = {
|
||||
burn: '#e67e22',
|
||||
brute: '#e74c3c',
|
||||
},
|
||||
// reagent / chemistry related colours
|
||||
reagent: {
|
||||
acidicbuffer: "#fbc314",
|
||||
basicbuffer: "#3853a4",
|
||||
},
|
||||
};
|
||||
|
||||
// Colors defined in CSS
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -71,6 +71,7 @@ const setupApp = () => {
|
||||
setupHotReloading();
|
||||
module.hot.accept([
|
||||
'./components',
|
||||
'./debug',
|
||||
'./layouts',
|
||||
'./routes',
|
||||
], () => {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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://');
|
||||
|
||||
@@ -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:*"
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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
Reference in New Issue
Block a user