From 2bbf2dc2ddc506e9e724cad6538ae9bfeacdfb2f Mon Sep 17 00:00:00 2001 From: Drulikar Date: Fri, 26 Apr 2024 05:39:44 -0700 Subject: [PATCH 01/58] Tacmap polish and fixes --- tgui/packages/tgui/interfaces/TacticalMap.tsx | 85 ++++++++++--------- .../tgui/styles/interfaces/TacticalMap.scss | 8 ++ tgui/packages/tgui/styles/themes/crt.scss | 30 ++++++- 3 files changed, 79 insertions(+), 44 deletions(-) diff --git a/tgui/packages/tgui/interfaces/TacticalMap.tsx b/tgui/packages/tgui/interfaces/TacticalMap.tsx index c3a386cc6f92..a7af8f5af2a8 100644 --- a/tgui/packages/tgui/interfaces/TacticalMap.tsx +++ b/tgui/packages/tgui/interfaces/TacticalMap.tsx @@ -1,5 +1,13 @@ import { useBackend, useLocalState } from '../backend'; -import { Button, Dropdown, Section, Stack, ProgressBar, Box, Tabs } from '../components'; +import { + Button, + Dropdown, + Section, + Stack, + ProgressBar, + Box, + Tabs, +} from '../components'; import { Window } from '../layouts'; import { CanvasLayer } from './CanvasLayer'; import { DrawnMap } from './DrawnMap'; @@ -11,10 +19,10 @@ interface TacMapProps { updatedCanvas: boolean; themeId: number; svgData: any; - canViewTacmap: number; - canDraw: number; + canViewTacmap: boolean; + canDraw: boolean; isxeno: boolean; - canViewCanvas: number; + canViewCanvas: boolean; newCanvasFlatImage: string; oldCanvasFlatImage: string; actionQueueChange: number; @@ -26,40 +34,40 @@ interface TacMapProps { canvasCooldownDuration: any; canvasCooldown: any; exportedTacMapImage: any; - tacmapReady: number; + tacmapReady: boolean; } const PAGES = [ { title: 'Live Tacmap', - canOpen: (data) => { - return 1; + canOpen: (data: TacMapProps) => { + return true; }, component: () => ViewMapPanel, icon: 'map', - canAccess: (data) => { + canAccess: (data: TacMapProps) => { return data.canViewTacmap; }, }, { title: 'Map View', - canOpen: (data) => { - return 1; + canOpen: (data: TacMapProps) => { + return true; }, component: () => OldMapPanel, icon: 'eye', - canAccess: (data) => { + canAccess: (data: TacMapProps) => { return data.canViewCanvas; }, }, { title: 'Canvas', - canOpen: (data) => { + canOpen: (data: TacMapProps) => { return data.tacmapReady; }, component: () => DrawMapPanel, icon: 'paintbrush', - canAccess: (data) => { + canAccess: (data: TacMapProps) => { return data.canDraw; }, }, @@ -93,7 +101,7 @@ export const TacticalMap = (props) => { ); const PageComponent = PAGES[pageIndex].component(); - const handleTacmapOnClick = (i, pageTitle) => { + const handleTacmapOnClick = (i: number, pageTitle: string) => { setPageIndex(i); act('menuSelect', { selection: pageTitle, @@ -111,13 +119,12 @@ export const TacticalMap = (props) => { width="688px" fontSize="20px" textAlign="center" - title="Tactical Map Options" - justify="space-evenly"> + title="Tactical Map Options"> {PAGES.map((page, i) => { - if (page.canAccess(data) === 0) { + if (!page.canAccess(data)) { return; } return ( @@ -125,10 +132,13 @@ export const TacticalMap = (props) => { key={i} color={data.isxeno ? 'purple' : 'blue'} selected={i === pageIndex} - disabled={page.canOpen(data) === 0} icon={page.icon} - onClick={() => handleTacmapOnClick(i, page.title)}> - {page.canOpen(data) === 0 ? 'loading' : page.title} + onClick={() => + page.canOpen(data) + ? handleTacmapOnClick(i, page.title) + : null + }> + {page.canOpen(data) ? page.title : 'loading'} ); })} @@ -146,7 +156,7 @@ const ViewMapPanel = (props) => { const { data } = useBackend(); // byond ui can't resist trying to render - if (data.canViewTacmap === 0 || data.mapRef === null) { + if (!data.canViewTacmap || data.mapRef === null) { return ; } @@ -167,12 +177,7 @@ const ViewMapPanel = (props) => { const OldMapPanel = (props) => { const { data } = useBackend(); return ( -
+
{data.canViewCanvas ? ( { data.exportedTacMapImage = image; }; - const handleColorSelection = (dataSelection) => { + const handleColorSelection = (dataSelection: string) => { if (colors[dataSelection] !== null && colors[dataSelection] !== undefined) { return colors[dataSelection]; } else { @@ -222,8 +227,7 @@ const DrawMapPanel = (props) => { {(!data.updatedCanvas && (
-
+
+ onUndo={(value: string) => act('selectColor', { color: findColorValue(value) }) } onDraw={() => act('onDraw')} diff --git a/tgui/packages/tgui/styles/interfaces/TacticalMap.scss b/tgui/packages/tgui/styles/interfaces/TacticalMap.scss index a4ab13451772..84f81d62a681 100644 --- a/tgui/packages/tgui/styles/interfaces/TacticalMap.scss +++ b/tgui/packages/tgui/styles/interfaces/TacticalMap.scss @@ -13,3 +13,11 @@ .progress-stack { margin-top: 15px; } + +.TacticalMapColorPicker { + height: 21px; + width: 13rem; + span { + color: white !important; + } +} diff --git a/tgui/packages/tgui/styles/themes/crt.scss b/tgui/packages/tgui/styles/themes/crt.scss index bffc754404e3..9f9436f275a3 100644 --- a/tgui/packages/tgui/styles/themes/crt.scss +++ b/tgui/packages/tgui/styles/themes/crt.scss @@ -1,5 +1,5 @@ @use 'sass:color'; -@use "sass:math"; +@use 'sass:math'; @use 'sass:meta'; $font-family: monospace; @@ -139,7 +139,9 @@ $background-radial-opacity: 0.2 !default; rgba(0, 0, 255, 0.06) ); z-index: 2; - background-size: 100% 2px, 3px 100%; + background-size: + 100% 2px, + 3px 100%; pointer-events: none; } @@ -182,6 +184,30 @@ $background-radial-opacity: 0.2 !default; } } + .Dropdown__control { + &:hover, + &:focus, + &:visited, + &:target { + .Dropdown__selected-text, + .Dropdown__arrow-button { + color: base.$color-bg; + } + } + } + + .Dropdown__control.Button--disabled { + &:hover, + &:focus, + &:visited, + &:target { + .Dropdown__selected-text, + .Dropdown__arrow-button { + color: base.$color-fg; + } + } + } + hr { color: base.$color-fg; border: 1px solid base.$color-fg; From e958883f6f2442956d4835de1e0f51f80ac0690b Mon Sep 17 00:00:00 2001 From: Drulikar Date: Fri, 26 Apr 2024 05:41:01 -0700 Subject: [PATCH 02/58] Fix runtimes when tgui say thinking ends --- code/modules/tgui/tgui-say/modal.dm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/modules/tgui/tgui-say/modal.dm b/code/modules/tgui/tgui-say/modal.dm index f1e87e001cef..17b77c2767ff 100644 --- a/code/modules/tgui/tgui-say/modal.dm +++ b/code/modules/tgui/tgui-say/modal.dm @@ -110,10 +110,10 @@ close() return TRUE if (type == "thinking") - if(payload["mode"] == TRUE) + if(payload == TRUE) // visible start_thinking() return TRUE - if(payload["mode"] == FALSE) + if(payload == FALSE) stop_thinking() return TRUE return FALSE From a31d82813cce6137daf26be7af073766edf5f16d Mon Sep 17 00:00:00 2001 From: Drulikar Date: Fri, 26 Apr 2024 05:41:27 -0700 Subject: [PATCH 03/58] Various TGUI ports from upstream --- .vscode/settings.json | 2 +- node_modules/.yarn-integrity | 10 + tgui/.eslintrc-sonar.yml | 27 +- tgui/.eslintrc.yml | 2 +- tgui/.yarn/sdks/prettier/bin/prettier.cjs | 20 + tgui/.yarn/sdks/prettier/index.cjs | 20 + tgui/.yarn/sdks/prettier/index.js | 20 - tgui/.yarn/sdks/prettier/package.json | 7 +- tgui/.yarn/sdks/typescript/bin/tsc | 8 +- tgui/.yarn/sdks/typescript/bin/tsserver | 8 +- tgui/.yarn/sdks/typescript/lib/tsc.js | 8 +- tgui/.yarn/sdks/typescript/lib/tsserver.js | 191 +- .../sdks/typescript/lib/tsserverlibrary.js | 191 +- tgui/.yarn/sdks/typescript/lib/typescript.js | 14 +- tgui/.yarn/sdks/typescript/package.json | 8 +- tgui/README.md | 8 +- tgui/docs/component-reference.md | 71 +- tgui/global.d.ts | 30 - tgui/package.json | 8 +- tgui/packages/common/color.js | 21 +- .../common/{keycodes.js => keycodes.ts} | 0 tgui/packages/common/react.ts | 12 +- tgui/packages/common/redux.ts | 20 +- tgui/packages/common/timer.ts | 6 +- tgui/packages/tgfont/package.json | 2 +- tgui/packages/tgui-dev-server/dreamseeker.js | 3 +- tgui/packages/tgui-dev-server/index.js | 2 +- tgui/packages/tgui-dev-server/link/retrace.js | 3 +- tgui/packages/tgui-dev-server/reloader.js | 7 +- tgui/packages/tgui-dev-server/util.js | 1 + tgui/packages/tgui-dev-server/winreg.js | 4 +- tgui/packages/tgui-panel/chat/renderer.js | 25 +- tgui/packages/tgui-panel/package.json | 7 +- tgui/packages/tgui-panel/panelFocus.js | 2 +- .../tgui-panel/settings/SettingsPanel.jsx | 31 +- tgui/packages/tgui-say/index.tsx | 21 +- tgui/packages/tgui-say/package.json | 4 +- tgui/packages/tgui-say/timers.ts | 4 +- tgui/packages/tgui/assets.ts | 4 +- tgui/packages/tgui/backend.ts | 22 +- tgui/packages/tgui/backend.ts.bak | 385 +++ tgui/packages/tgui/components/Autofocus.tsx | 18 +- tgui/packages/tgui/components/Blink.jsx | 11 +- .../{BlockQuote.jsx => BlockQuote.tsx} | 8 +- tgui/packages/tgui/components/Box.tsx | 129 +- tgui/packages/tgui/components/Button.jsx | 367 --- tgui/packages/tgui/components/Button.tsx | 414 +++ tgui/packages/tgui/components/ByondUi.jsx | 15 +- tgui/packages/tgui/components/Chart.jsx | 127 - tgui/packages/tgui/components/Chart.tsx | 160 + tgui/packages/tgui/components/Collapsible.jsx | 45 - tgui/packages/tgui/components/Collapsible.tsx | 45 + tgui/packages/tgui/components/ColorBox.jsx | 31 - tgui/packages/tgui/components/ColorBox.tsx | 30 + .../components/{Dimmer.jsx => Dimmer.tsx} | 10 +- .../components/{Divider.jsx => Divider.tsx} | 12 +- .../tgui/components/DraggableControl.jsx | 11 +- tgui/packages/tgui/components/Dropdown.tsx | 9 +- tgui/packages/tgui/components/Flex.tsx | 8 +- tgui/packages/tgui/components/Flex.tsx.bak | 110 + tgui/packages/tgui/components/Icon.jsx | 71 - tgui/packages/tgui/components/Icon.tsx | 91 + tgui/packages/tgui/components/Image.tsx | 63 + .../tgui/components/InfinitePlane.jsx | 44 +- tgui/packages/tgui/components/Input.tsx | 181 ++ .../tgui/components/{Knob.jsx => Knob.tsx} | 107 +- ...abeledControls.jsx => LabeledControls.tsx} | 22 +- .../tgui/components/{Modal.jsx => Modal.tsx} | 11 +- tgui/packages/tgui/components/NoticeBox.jsx | 27 - tgui/packages/tgui/components/NoticeBox.tsx | 48 + tgui/packages/tgui/components/Popper.tsx | 34 +- tgui/packages/tgui/components/ProgressBar.tsx | 2 +- .../tgui/components/RestrictedInput.jsx | 151 +- .../{RoundGauge.jsx => RoundGauge.tsx} | 127 +- tgui/packages/tgui/components/Section.tsx | 159 +- .../components/{Slider.jsx => Slider.tsx} | 95 +- tgui/packages/tgui/components/Stack.tsx | 40 +- tgui/packages/tgui/components/Table.jsx | 64 - tgui/packages/tgui/components/Table.tsx | 90 + .../tgui/components/{Tabs.jsx => Tabs.tsx} | 39 +- tgui/packages/tgui/components/TimeDisplay.jsx | 3 +- tgui/packages/tgui/components/index.jsx | 1 + tgui/packages/tgui/debug/KitchenSink.jsx | 10 +- .../tgui/debug/{index.js => index.ts} | 0 tgui/packages/tgui/debug/middleware.js | 7 +- tgui/packages/tgui/events.ts | 21 +- .../{AresAdmin.js => AresAdmin.jsx} | 0 .../{IcMedalsPanel.js => IcMedalsPanel.jsx} | 0 .../tgui/interfaces/TextInputModal.tsx | 33 +- .../tgui/interfaces/common/LoadingToolbox.tsx | 32 + .../tgui/layouts/{Layout.jsx => Layout.tsx} | 54 +- .../{NtosWindow.jsx => NtosWindow.tsx} | 92 +- .../tgui/layouts/{Pane.jsx => Pane.tsx} | 32 +- tgui/packages/tgui/layouts/Window.tsx | 39 +- .../tgui/layouts/{index.js => index.ts} | 0 tgui/packages/tgui/package.json | 18 +- tgui/packages/tgui/routes.tsx | 35 +- tgui/packages/tgui/routes.tsx.bak | 99 + tgui/packages/tgui/sanitize.ts | 2 +- .../packages/tgui/stories/ByondUi.stories.jsx | 19 +- tgui/packages/tgui/stories/Flex.stories.jsx | 20 +- .../tgui/stories/LabeledList.stories.jsx | 6 +- ...{Popper.stories.jsx => Popper.stories.tsx} | 22 +- .../tgui/stories/ProgressBar.stories.jsx | 21 +- tgui/packages/tgui/stories/Tabs.stories.jsx | 15 +- tgui/public/tgui.html | 52 +- tgui/yarn.lock | 2843 +++++++++-------- yarn.lock | 4 + 108 files changed, 4766 insertions(+), 2979 deletions(-) create mode 100644 node_modules/.yarn-integrity create mode 100644 tgui/.yarn/sdks/prettier/bin/prettier.cjs create mode 100644 tgui/.yarn/sdks/prettier/index.cjs delete mode 100644 tgui/.yarn/sdks/prettier/index.js rename tgui/packages/common/{keycodes.js => keycodes.ts} (100%) create mode 100644 tgui/packages/tgui/backend.ts.bak rename tgui/packages/tgui/components/{BlockQuote.jsx => BlockQuote.tsx} (71%) delete mode 100644 tgui/packages/tgui/components/Button.jsx create mode 100644 tgui/packages/tgui/components/Button.tsx delete mode 100644 tgui/packages/tgui/components/Chart.jsx create mode 100644 tgui/packages/tgui/components/Chart.tsx delete mode 100644 tgui/packages/tgui/components/Collapsible.jsx create mode 100644 tgui/packages/tgui/components/Collapsible.tsx delete mode 100644 tgui/packages/tgui/components/ColorBox.jsx create mode 100644 tgui/packages/tgui/components/ColorBox.tsx rename tgui/packages/tgui/components/{Dimmer.jsx => Dimmer.tsx} (61%) rename tgui/packages/tgui/components/{Divider.jsx => Divider.tsx} (66%) create mode 100644 tgui/packages/tgui/components/Flex.tsx.bak delete mode 100644 tgui/packages/tgui/components/Icon.jsx create mode 100644 tgui/packages/tgui/components/Icon.tsx create mode 100644 tgui/packages/tgui/components/Image.tsx create mode 100644 tgui/packages/tgui/components/Input.tsx rename tgui/packages/tgui/components/{Knob.jsx => Knob.tsx} (53%) rename tgui/packages/tgui/components/{LabeledControls.jsx => LabeledControls.tsx} (73%) rename tgui/packages/tgui/components/{Modal.jsx => Modal.tsx} (69%) delete mode 100644 tgui/packages/tgui/components/NoticeBox.jsx create mode 100644 tgui/packages/tgui/components/NoticeBox.tsx rename tgui/packages/tgui/components/{RoundGauge.jsx => RoundGauge.tsx} (52%) rename tgui/packages/tgui/components/{Slider.jsx => Slider.tsx} (53%) delete mode 100644 tgui/packages/tgui/components/Table.jsx create mode 100644 tgui/packages/tgui/components/Table.tsx rename tgui/packages/tgui/components/{Tabs.jsx => Tabs.tsx} (63%) rename tgui/packages/tgui/debug/{index.js => index.ts} (100%) rename tgui/packages/tgui/interfaces/{AresAdmin.js => AresAdmin.jsx} (100%) rename tgui/packages/tgui/interfaces/{IcMedalsPanel.js => IcMedalsPanel.jsx} (100%) create mode 100644 tgui/packages/tgui/interfaces/common/LoadingToolbox.tsx rename tgui/packages/tgui/layouts/{Layout.jsx => Layout.tsx} (50%) rename tgui/packages/tgui/layouts/{NtosWindow.jsx => NtosWindow.tsx} (61%) rename tgui/packages/tgui/layouts/{Pane.jsx => Pane.tsx} (68%) rename tgui/packages/tgui/layouts/{index.js => index.ts} (100%) create mode 100644 tgui/packages/tgui/routes.tsx.bak rename tgui/packages/tgui/stories/{Popper.stories.jsx => Popper.stories.tsx} (81%) create mode 100644 yarn.lock diff --git a/.vscode/settings.json b/.vscode/settings.json index c7b218b77591..5b5a40bb329d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,7 @@ { "eslint.nodePath": "./tgui/.yarn/sdks", "eslint.workingDirectories": ["./tgui"], - "prettier.prettierPath": "./tgui/.yarn/sdks/prettier/index.js", + "prettier.prettierPath": "./tgui/.yarn/sdks/prettier/index.cjs", "typescript.tsdk": "./tgui/.yarn/sdks/typescript/lib", "typescript.enablePromptUseWorkspaceTsdk": true, "search.exclude": { diff --git a/node_modules/.yarn-integrity b/node_modules/.yarn-integrity new file mode 100644 index 000000000000..6b671344d4af --- /dev/null +++ b/node_modules/.yarn-integrity @@ -0,0 +1,10 @@ +{ + "systemParams": "win32-x64-93", + "modulesFolders": [], + "flags": [], + "linkedModules": [], + "topLevelPatterns": [], + "lockfileEntries": {}, + "files": [], + "artifacts": {} +} \ No newline at end of file diff --git a/tgui/.eslintrc-sonar.yml b/tgui/.eslintrc-sonar.yml index 3cdd49f889e2..ebc57f72c008 100644 --- a/tgui/.eslintrc-sonar.yml +++ b/tgui/.eslintrc-sonar.yml @@ -1,26 +1 @@ -rules: - # radar/cognitive-complexity: error - radar/max-switch-cases: error - radar/no-all-duplicated-branches: error - radar/no-collapsible-if: error - radar/no-collection-size-mischeck: error - radar/no-duplicate-string: error - radar/no-duplicated-branches: error - radar/no-element-overwrite: error - radar/no-extra-arguments: error - radar/no-identical-conditions: error - radar/no-identical-expressions: error - radar/no-identical-functions: error - radar/no-inverted-boolean-check: error - radar/no-one-iteration-loop: error - radar/no-redundant-boolean: error - radar/no-redundant-jump: error - radar/no-same-line-conditional: error - radar/no-small-switch: error - radar/no-unused-collection: error - radar/no-use-of-empty-return-value: error - radar/no-useless-catch: error - radar/prefer-immediate-return: error - radar/prefer-object-literal: error - radar/prefer-single-boolean-return: error - radar/prefer-while: error +extends: 'plugin:sonarjs/recommended' diff --git a/tgui/.eslintrc.yml b/tgui/.eslintrc.yml index fc7385b68b33..5ad24497c831 100644 --- a/tgui/.eslintrc.yml +++ b/tgui/.eslintrc.yml @@ -11,7 +11,7 @@ env: browser: true node: true plugins: - - radar + - sonarjs - react - unused-imports settings: diff --git a/tgui/.yarn/sdks/prettier/bin/prettier.cjs b/tgui/.yarn/sdks/prettier/bin/prettier.cjs new file mode 100644 index 000000000000..5efad688e739 --- /dev/null +++ b/tgui/.yarn/sdks/prettier/bin/prettier.cjs @@ -0,0 +1,20 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire} = require(`module`); +const {resolve} = require(`path`); + +const relPnpApiPath = "../../../../.pnp.cjs"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absRequire = createRequire(absPnpApiPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require prettier/bin/prettier.cjs + require(absPnpApiPath).setup(); + } +} + +// Defer to the real prettier/bin/prettier.cjs your application uses +module.exports = absRequire(`prettier/bin/prettier.cjs`); diff --git a/tgui/.yarn/sdks/prettier/index.cjs b/tgui/.yarn/sdks/prettier/index.cjs new file mode 100644 index 000000000000..8758e367a725 --- /dev/null +++ b/tgui/.yarn/sdks/prettier/index.cjs @@ -0,0 +1,20 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire} = require(`module`); +const {resolve} = require(`path`); + +const relPnpApiPath = "../../../.pnp.cjs"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absRequire = createRequire(absPnpApiPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require prettier + require(absPnpApiPath).setup(); + } +} + +// Defer to the real prettier your application uses +module.exports = absRequire(`prettier`); diff --git a/tgui/.yarn/sdks/prettier/index.js b/tgui/.yarn/sdks/prettier/index.js deleted file mode 100644 index 873c77bda9d4..000000000000 --- a/tgui/.yarn/sdks/prettier/index.js +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env node - -const { existsSync } = require(`fs`); -const { createRequire } = require(`module`); -const { resolve } = require(`path`); - -const relPnpApiPath = '../../../.pnp.cjs'; - -const absPnpApiPath = resolve(__dirname, relPnpApiPath); -const absRequire = createRequire(absPnpApiPath); - -if (existsSync(absPnpApiPath)) { - if (!process.versions.pnp) { - // Setup the environment to be able to require prettier/index.js - require(absPnpApiPath).setup(); - } -} - -// Defer to the real prettier/index.js your application uses -module.exports = absRequire(`prettier/index.js`); diff --git a/tgui/.yarn/sdks/prettier/package.json b/tgui/.yarn/sdks/prettier/package.json index 0cbd71ff32d5..6e68f38b6716 100644 --- a/tgui/.yarn/sdks/prettier/package.json +++ b/tgui/.yarn/sdks/prettier/package.json @@ -1,6 +1,7 @@ { "name": "prettier", - "version": "0.19.0-sdk", - "main": "./index.js", - "type": "commonjs" + "version": "3.2.5-sdk", + "main": "./index.cjs", + "type": "commonjs", + "bin": "./bin/prettier.cjs" } diff --git a/tgui/.yarn/sdks/typescript/bin/tsc b/tgui/.yarn/sdks/typescript/bin/tsc index 815aedd29d3e..454b950b7e8f 100644 --- a/tgui/.yarn/sdks/typescript/bin/tsc +++ b/tgui/.yarn/sdks/typescript/bin/tsc @@ -1,10 +1,10 @@ #!/usr/bin/env node -const { existsSync } = require(`fs`); -const { createRequire } = require(`module`); -const { resolve } = require(`path`); +const {existsSync} = require(`fs`); +const {createRequire} = require(`module`); +const {resolve} = require(`path`); -const relPnpApiPath = '../../../../.pnp.cjs'; +const relPnpApiPath = "../../../../.pnp.cjs"; const absPnpApiPath = resolve(__dirname, relPnpApiPath); const absRequire = createRequire(absPnpApiPath); diff --git a/tgui/.yarn/sdks/typescript/bin/tsserver b/tgui/.yarn/sdks/typescript/bin/tsserver index 7d9de6801622..d7a605684df9 100644 --- a/tgui/.yarn/sdks/typescript/bin/tsserver +++ b/tgui/.yarn/sdks/typescript/bin/tsserver @@ -1,10 +1,10 @@ #!/usr/bin/env node -const { existsSync } = require(`fs`); -const { createRequire } = require(`module`); -const { resolve } = require(`path`); +const {existsSync} = require(`fs`); +const {createRequire} = require(`module`); +const {resolve} = require(`path`); -const relPnpApiPath = '../../../../.pnp.cjs'; +const relPnpApiPath = "../../../../.pnp.cjs"; const absPnpApiPath = resolve(__dirname, relPnpApiPath); const absRequire = createRequire(absPnpApiPath); diff --git a/tgui/.yarn/sdks/typescript/lib/tsc.js b/tgui/.yarn/sdks/typescript/lib/tsc.js index 1d3e2a83cf2d..2f62fc96c0a0 100644 --- a/tgui/.yarn/sdks/typescript/lib/tsc.js +++ b/tgui/.yarn/sdks/typescript/lib/tsc.js @@ -1,10 +1,10 @@ #!/usr/bin/env node -const { existsSync } = require(`fs`); -const { createRequire } = require(`module`); -const { resolve } = require(`path`); +const {existsSync} = require(`fs`); +const {createRequire} = require(`module`); +const {resolve} = require(`path`); -const relPnpApiPath = '../../../../.pnp.cjs'; +const relPnpApiPath = "../../../../.pnp.cjs"; const absPnpApiPath = resolve(__dirname, relPnpApiPath); const absRequire = createRequire(absPnpApiPath); diff --git a/tgui/.yarn/sdks/typescript/lib/tsserver.js b/tgui/.yarn/sdks/typescript/lib/tsserver.js index 023031ded46d..bbb1e46501b5 100644 --- a/tgui/.yarn/sdks/typescript/lib/tsserver.js +++ b/tgui/.yarn/sdks/typescript/lib/tsserver.js @@ -1,31 +1,29 @@ #!/usr/bin/env node -const { existsSync } = require(`fs`); -const { createRequire } = require(`module`); -const { resolve } = require(`path`); +const {existsSync} = require(`fs`); +const {createRequire} = require(`module`); +const {resolve} = require(`path`); -const relPnpApiPath = '../../../../.pnp.cjs'; +const relPnpApiPath = "../../../../.pnp.cjs"; const absPnpApiPath = resolve(__dirname, relPnpApiPath); const absRequire = createRequire(absPnpApiPath); -const moduleWrapper = (tsserver) => { +const moduleWrapper = tsserver => { if (!process.versions.pnp) { return tsserver; } - const { isAbsolute } = require(`path`); + const {isAbsolute} = require(`path`); const pnpApi = require(`pnpapi`); - const isVirtual = (str) => str.match(/\/(\$\$virtual|__virtual__)\//); - const isPortal = (str) => str.startsWith('portal:/'); - const normalize = (str) => str.replace(/\\/g, `/`).replace(/^\/?/, `/`); + const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//); + const isPortal = str => str.startsWith("portal:/"); + const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`); - const dependencyTreeRoots = new Set( - pnpApi.getDependencyTreeRoots().map((locator) => { - return `${locator.name}@${locator.reference}`; - }) - ); + const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => { + return `${locator.name}@${locator.reference}`; + })); // VSCode sends the zip paths to TS using the "zip://" prefix, that TS // doesn't understand. This layer makes sure to remove the protocol @@ -33,11 +31,7 @@ const moduleWrapper = (tsserver) => { function toEditorPath(str) { // We add the `zip:` prefix to both `.zip/` paths and virtual paths - if ( - isAbsolute(str) && - !str.match(/^\^?(zip:|\/zip\/)/) && - (str.match(/\.zip\//) || isVirtual(str)) - ) { + if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) { // We also take the opportunity to turn virtual paths into physical ones; // this makes it much easier to work with workspaces that list peer // dependencies, since otherwise Ctrl+Click would bring us to the virtual @@ -51,11 +45,7 @@ const moduleWrapper = (tsserver) => { const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str; if (resolved) { const locator = pnpApi.findPackageLocator(resolved); - if ( - locator && - (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || - isPortal(locator.reference)) - ) { + if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) { str = resolved; } } @@ -83,56 +73,44 @@ const moduleWrapper = (tsserver) => { // Before | ^/zip/c:/foo/bar.zip/package.json // After | ^/zip//c:/foo/bar.zip/package.json // - case `vscode <1.61`: - { - str = `^zip:${str}`; - } - break; + case `vscode <1.61`: { + str = `^zip:${str}`; + } break; - case `vscode <1.66`: - { - str = `^/zip/${str}`; - } - break; + case `vscode <1.66`: { + str = `^/zip/${str}`; + } break; - case `vscode <1.68`: - { - str = `^/zip${str}`; - } - break; + case `vscode <1.68`: { + str = `^/zip${str}`; + } break; - case `vscode`: - { - str = `^/zip/${str}`; - } - break; + case `vscode`: { + str = `^/zip/${str}`; + } break; // To make "go to definition" work, // We have to resolve the actual file system path from virtual path // and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip) - case `coc-nvim`: - { - str = normalize(resolved).replace(/\.zip\//, `.zip::`); - str = resolve(`zipfile:${str}`); - } - break; + case `coc-nvim`: { + str = normalize(resolved).replace(/\.zip\//, `.zip::`); + str = resolve(`zipfile:${str}`); + } break; // Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server) // We have to resolve the actual file system path from virtual path, // everything else is up to neovim - case `neovim`: - { - str = normalize(resolved).replace(/\.zip\//, `.zip::`); - str = `zipfile://${str}`; - } - break; - - default: - { - str = `zip:${str}`; - } - break; + case `neovim`: { + str = normalize(resolved).replace(/\.zip\//, `.zip::`); + str = `zipfile://${str}`; + } break; + + default: { + str = `zip:${str}`; + } break; } + } else { + str = str.replace(/^\/?/, process.platform === `win32` ? `` : `/`); } } @@ -141,35 +119,26 @@ const moduleWrapper = (tsserver) => { function fromEditorPath(str) { switch (hostInfo) { - case `coc-nvim`: - { - str = str.replace(/\.zip::/, `.zip/`); - // The path for coc-nvim is in format of //zipfile://.yarn/... - // So in order to convert it back, we use .* to match all the thing - // before `zipfile:` - return process.platform === `win32` - ? str.replace(/^.*zipfile:\//, ``) - : str.replace(/^.*zipfile:/, ``); - } - break; - - case `neovim`: - { - str = str.replace(/\.zip::/, `.zip/`); - // The path for neovim is in format of zipfile:////.yarn/... - return str.replace(/^zipfile:\/\//, ``); - } - break; + case `coc-nvim`: { + str = str.replace(/\.zip::/, `.zip/`); + // The path for coc-nvim is in format of //zipfile://.yarn/... + // So in order to convert it back, we use .* to match all the thing + // before `zipfile:` + return process.platform === `win32` + ? str.replace(/^.*zipfile:\//, ``) + : str.replace(/^.*zipfile:/, ``); + } break; + + case `neovim`: { + str = str.replace(/\.zip::/, `.zip/`); + // The path for neovim is in format of zipfile:////.yarn/... + return str.replace(/^zipfile:\/\//, ``); + } break; case `vscode`: - default: - { - return str.replace( - /^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, - process.platform === `win32` ? `` : `/` - ); - } - break; + default: { + return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`) + } break; } } @@ -181,9 +150,8 @@ const moduleWrapper = (tsserver) => { // TypeScript already does local loads and if this code is running the user trusts the workspace // https://github.com/microsoft/vscode/issues/45856 const ConfiguredProject = tsserver.server.ConfiguredProject; - const { enablePluginsWithOptions: originalEnablePluginsWithOptions } = - ConfiguredProject.prototype; - ConfiguredProject.prototype.enablePluginsWithOptions = function () { + const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype; + ConfiguredProject.prototype.enablePluginsWithOptions = function() { this.projectService.allowLocalPluginLoads = true; return originalEnablePluginsWithOptions.apply(this, arguments); }; @@ -193,8 +161,7 @@ const moduleWrapper = (tsserver) => { // like an absolute path of ours and normalize it. const Session = tsserver.server.Session; - const { onMessage: originalOnMessage, send: originalSend } = - Session.prototype; + const {onMessage: originalOnMessage, send: originalSend} = Session.prototype; let hostInfo = `unknown`; Object.assign(Session.prototype, { @@ -210,12 +177,10 @@ const moduleWrapper = (tsserver) => { ) { hostInfo = parsedMessage.arguments.hostInfo; if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) { - const [, major, minor] = ( - process.env.VSCODE_IPC_HOOK.match( - // The RegExp from https://semver.org/ but without the caret at the start - /(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ - ) ?? [] - ).map(Number); + const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match( + // The RegExp from https://semver.org/ but without the caret at the start + /(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ + ) ?? []).map(Number) if (major === 1) { if (minor < 61) { @@ -229,31 +194,21 @@ const moduleWrapper = (tsserver) => { } } - const processedMessageJSON = JSON.stringify( - parsedMessage, - (key, value) => { - return typeof value === 'string' ? fromEditorPath(value) : value; - } - ); + const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => { + return typeof value === 'string' ? fromEditorPath(value) : value; + }); return originalOnMessage.call( this, - isStringMessage - ? processedMessageJSON - : JSON.parse(processedMessageJSON) + isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON) ); }, send(/** @type {any} */ msg) { - return originalSend.call( - this, - JSON.parse( - JSON.stringify(msg, (key, value) => { - return typeof value === `string` ? toEditorPath(value) : value; - }) - ) - ); - }, + return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => { + return typeof value === `string` ? toEditorPath(value) : value; + }))); + } }); return tsserver; diff --git a/tgui/.yarn/sdks/typescript/lib/tsserverlibrary.js b/tgui/.yarn/sdks/typescript/lib/tsserverlibrary.js index f545a14220c7..a68f028fe197 100644 --- a/tgui/.yarn/sdks/typescript/lib/tsserverlibrary.js +++ b/tgui/.yarn/sdks/typescript/lib/tsserverlibrary.js @@ -1,31 +1,29 @@ #!/usr/bin/env node -const { existsSync } = require(`fs`); -const { createRequire } = require(`module`); -const { resolve } = require(`path`); +const {existsSync} = require(`fs`); +const {createRequire} = require(`module`); +const {resolve} = require(`path`); -const relPnpApiPath = '../../../../.pnp.cjs'; +const relPnpApiPath = "../../../../.pnp.cjs"; const absPnpApiPath = resolve(__dirname, relPnpApiPath); const absRequire = createRequire(absPnpApiPath); -const moduleWrapper = (tsserver) => { +const moduleWrapper = tsserver => { if (!process.versions.pnp) { return tsserver; } - const { isAbsolute } = require(`path`); + const {isAbsolute} = require(`path`); const pnpApi = require(`pnpapi`); - const isVirtual = (str) => str.match(/\/(\$\$virtual|__virtual__)\//); - const isPortal = (str) => str.startsWith('portal:/'); - const normalize = (str) => str.replace(/\\/g, `/`).replace(/^\/?/, `/`); + const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//); + const isPortal = str => str.startsWith("portal:/"); + const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`); - const dependencyTreeRoots = new Set( - pnpApi.getDependencyTreeRoots().map((locator) => { - return `${locator.name}@${locator.reference}`; - }) - ); + const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => { + return `${locator.name}@${locator.reference}`; + })); // VSCode sends the zip paths to TS using the "zip://" prefix, that TS // doesn't understand. This layer makes sure to remove the protocol @@ -33,11 +31,7 @@ const moduleWrapper = (tsserver) => { function toEditorPath(str) { // We add the `zip:` prefix to both `.zip/` paths and virtual paths - if ( - isAbsolute(str) && - !str.match(/^\^?(zip:|\/zip\/)/) && - (str.match(/\.zip\//) || isVirtual(str)) - ) { + if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) { // We also take the opportunity to turn virtual paths into physical ones; // this makes it much easier to work with workspaces that list peer // dependencies, since otherwise Ctrl+Click would bring us to the virtual @@ -51,11 +45,7 @@ const moduleWrapper = (tsserver) => { const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str; if (resolved) { const locator = pnpApi.findPackageLocator(resolved); - if ( - locator && - (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || - isPortal(locator.reference)) - ) { + if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) { str = resolved; } } @@ -83,56 +73,44 @@ const moduleWrapper = (tsserver) => { // Before | ^/zip/c:/foo/bar.zip/package.json // After | ^/zip//c:/foo/bar.zip/package.json // - case `vscode <1.61`: - { - str = `^zip:${str}`; - } - break; + case `vscode <1.61`: { + str = `^zip:${str}`; + } break; - case `vscode <1.66`: - { - str = `^/zip/${str}`; - } - break; + case `vscode <1.66`: { + str = `^/zip/${str}`; + } break; - case `vscode <1.68`: - { - str = `^/zip${str}`; - } - break; + case `vscode <1.68`: { + str = `^/zip${str}`; + } break; - case `vscode`: - { - str = `^/zip/${str}`; - } - break; + case `vscode`: { + str = `^/zip/${str}`; + } break; // To make "go to definition" work, // We have to resolve the actual file system path from virtual path // and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip) - case `coc-nvim`: - { - str = normalize(resolved).replace(/\.zip\//, `.zip::`); - str = resolve(`zipfile:${str}`); - } - break; + case `coc-nvim`: { + str = normalize(resolved).replace(/\.zip\//, `.zip::`); + str = resolve(`zipfile:${str}`); + } break; // Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server) // We have to resolve the actual file system path from virtual path, // everything else is up to neovim - case `neovim`: - { - str = normalize(resolved).replace(/\.zip\//, `.zip::`); - str = `zipfile://${str}`; - } - break; - - default: - { - str = `zip:${str}`; - } - break; + case `neovim`: { + str = normalize(resolved).replace(/\.zip\//, `.zip::`); + str = `zipfile://${str}`; + } break; + + default: { + str = `zip:${str}`; + } break; } + } else { + str = str.replace(/^\/?/, process.platform === `win32` ? `` : `/`); } } @@ -141,35 +119,26 @@ const moduleWrapper = (tsserver) => { function fromEditorPath(str) { switch (hostInfo) { - case `coc-nvim`: - { - str = str.replace(/\.zip::/, `.zip/`); - // The path for coc-nvim is in format of //zipfile://.yarn/... - // So in order to convert it back, we use .* to match all the thing - // before `zipfile:` - return process.platform === `win32` - ? str.replace(/^.*zipfile:\//, ``) - : str.replace(/^.*zipfile:/, ``); - } - break; - - case `neovim`: - { - str = str.replace(/\.zip::/, `.zip/`); - // The path for neovim is in format of zipfile:////.yarn/... - return str.replace(/^zipfile:\/\//, ``); - } - break; + case `coc-nvim`: { + str = str.replace(/\.zip::/, `.zip/`); + // The path for coc-nvim is in format of //zipfile://.yarn/... + // So in order to convert it back, we use .* to match all the thing + // before `zipfile:` + return process.platform === `win32` + ? str.replace(/^.*zipfile:\//, ``) + : str.replace(/^.*zipfile:/, ``); + } break; + + case `neovim`: { + str = str.replace(/\.zip::/, `.zip/`); + // The path for neovim is in format of zipfile:////.yarn/... + return str.replace(/^zipfile:\/\//, ``); + } break; case `vscode`: - default: - { - return str.replace( - /^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, - process.platform === `win32` ? `` : `/` - ); - } - break; + default: { + return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`) + } break; } } @@ -181,9 +150,8 @@ const moduleWrapper = (tsserver) => { // TypeScript already does local loads and if this code is running the user trusts the workspace // https://github.com/microsoft/vscode/issues/45856 const ConfiguredProject = tsserver.server.ConfiguredProject; - const { enablePluginsWithOptions: originalEnablePluginsWithOptions } = - ConfiguredProject.prototype; - ConfiguredProject.prototype.enablePluginsWithOptions = function () { + const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype; + ConfiguredProject.prototype.enablePluginsWithOptions = function() { this.projectService.allowLocalPluginLoads = true; return originalEnablePluginsWithOptions.apply(this, arguments); }; @@ -193,8 +161,7 @@ const moduleWrapper = (tsserver) => { // like an absolute path of ours and normalize it. const Session = tsserver.server.Session; - const { onMessage: originalOnMessage, send: originalSend } = - Session.prototype; + const {onMessage: originalOnMessage, send: originalSend} = Session.prototype; let hostInfo = `unknown`; Object.assign(Session.prototype, { @@ -210,12 +177,10 @@ const moduleWrapper = (tsserver) => { ) { hostInfo = parsedMessage.arguments.hostInfo; if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) { - const [, major, minor] = ( - process.env.VSCODE_IPC_HOOK.match( - // The RegExp from https://semver.org/ but without the caret at the start - /(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ - ) ?? [] - ).map(Number); + const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match( + // The RegExp from https://semver.org/ but without the caret at the start + /(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ + ) ?? []).map(Number) if (major === 1) { if (minor < 61) { @@ -229,31 +194,21 @@ const moduleWrapper = (tsserver) => { } } - const processedMessageJSON = JSON.stringify( - parsedMessage, - (key, value) => { - return typeof value === 'string' ? fromEditorPath(value) : value; - } - ); + const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => { + return typeof value === 'string' ? fromEditorPath(value) : value; + }); return originalOnMessage.call( this, - isStringMessage - ? processedMessageJSON - : JSON.parse(processedMessageJSON) + isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON) ); }, send(/** @type {any} */ msg) { - return originalSend.call( - this, - JSON.parse( - JSON.stringify(msg, (key, value) => { - return typeof value === `string` ? toEditorPath(value) : value; - }) - ) - ); - }, + return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => { + return typeof value === `string` ? toEditorPath(value) : value; + }))); + } }); return tsserver; diff --git a/tgui/.yarn/sdks/typescript/lib/typescript.js b/tgui/.yarn/sdks/typescript/lib/typescript.js index 3e69e60dc6a5..b5f4db25bee6 100644 --- a/tgui/.yarn/sdks/typescript/lib/typescript.js +++ b/tgui/.yarn/sdks/typescript/lib/typescript.js @@ -1,20 +1,20 @@ #!/usr/bin/env node -const { existsSync } = require(`fs`); -const { createRequire } = require(`module`); -const { resolve } = require(`path`); +const {existsSync} = require(`fs`); +const {createRequire} = require(`module`); +const {resolve} = require(`path`); -const relPnpApiPath = '../../../../.pnp.cjs'; +const relPnpApiPath = "../../../../.pnp.cjs"; const absPnpApiPath = resolve(__dirname, relPnpApiPath); const absRequire = createRequire(absPnpApiPath); if (existsSync(absPnpApiPath)) { if (!process.versions.pnp) { - // Setup the environment to be able to require typescript/lib/typescript.js + // Setup the environment to be able to require typescript require(absPnpApiPath).setup(); } } -// Defer to the real typescript/lib/typescript.js your application uses -module.exports = absRequire(`typescript/lib/typescript.js`); +// Defer to the real typescript your application uses +module.exports = absRequire(`typescript`); diff --git a/tgui/.yarn/sdks/typescript/package.json b/tgui/.yarn/sdks/typescript/package.json index 6aac31b18401..bedc36522526 100644 --- a/tgui/.yarn/sdks/typescript/package.json +++ b/tgui/.yarn/sdks/typescript/package.json @@ -1,6 +1,10 @@ { "name": "typescript", - "version": "4.9.4-sdk", + "version": "5.4.3-sdk", "main": "./lib/typescript.js", - "type": "commonjs" + "type": "commonjs", + "bin": { + "tsc": "./bin/tsc", + "tsserver": "./bin/tsserver" + } } diff --git a/tgui/README.md b/tgui/README.md index e87130243429..1bae91fd1325 100644 --- a/tgui/README.md +++ b/tgui/README.md @@ -16,10 +16,9 @@ If you are completely new to frontend and prefer to **learn by doing**, start wi ### Guides -This project uses **Inferno** - a very fast UI rendering engine with a similar API to React. Take your time to read these guides: +This project uses React. Take your time to read the guide: -- [React guide](https://reactjs.org/docs/hello-world.html) -- [Inferno documentation](https://infernojs.org/docs/guides/components) - highlights differences with React. +- [React guide](https://react.dev/learn) If you were already familiar with an older, Ractive-based tgui, and want to translate concepts between old and new tgui, read this [interface conversion guide](docs/converting-old-tgui-interfaces.md). @@ -71,6 +70,7 @@ However, if you want finer control over the installation or build process, you w - `tools/build/build tgui-clean` - Clean up tgui folder. > With Juke Build, you can run multiple targets together, e.g.: +> > ``` > tools/build/build tgui tgui-lint tgui-tsc tgui-test > ``` @@ -137,7 +137,7 @@ Press `F12` or click the green bug to open the KitchenSink interface. This inter playground to test various tgui components. **Layout Debugger.** -Press `F11` to toggle the *layout debugger*. It will show outlines of +Press `F11` to toggle the _layout debugger_. It will show outlines of all tgui elements, which makes it easy to understand how everything comes together, and can reveal certain layout bugs which are not normally visible. diff --git a/tgui/docs/component-reference.md b/tgui/docs/component-reference.md index 3789016447cc..7fd8b2dc1912 100644 --- a/tgui/docs/component-reference.md +++ b/tgui/docs/component-reference.md @@ -65,17 +65,13 @@ it is used a lot in this framework. **Event handlers.** Event handlers are callbacks that you can attack to various element to -listen for browser events. Inferno supports camelcase (`onClick`) and -lowercase (`onclick`) event names. +listen for browser events. React supports camelcase (`onClick`) event names. -- Camel case names are what's called *synthetic* events, and are the +- Camel case names are what's called _synthetic_ events, and are the **preferred way** of handling events in React, for efficiency and performance reasons. Please read -[Inferno Event Handling](https://infernojs.org/docs/guides/event-handling) + [React Event Handling](https://react.dev/learn/responding-to-events) to understand what this is about. -- Lower case names are native browser events and should be used sparingly, -for example when you need an explicit IE8 support. **DO NOT** use -lowercase event handlers unless you really know what you are doing. ## `tgui/components` @@ -212,6 +208,7 @@ Buttons allow users to take actions, and make choices, with a single click. - See inherited props: [Box](#box) - `fluid: boolean` - Fill all available horizontal space. - `icon: string` - Adds an icon to the button. +- `iconPosition?: string` - Set to `'right'` to align the icon to the right of the children - `color: string` - Button color, as defined in `variables.scss`. - There is also a special color `transparent` - makes the button transparent and slightly dim when inactive. @@ -363,15 +360,13 @@ and displays selected entry. - See inherited props: [Box](#box) - See inherited props: [Icon](#icon) -- `options: string[]` - An array of strings which will be displayed in the -dropdown when open -- `selected: string` - Currently selected entry -- `width: number` - Width of dropdown button and resulting menu +- `options: string[] | DropdownEntry[]` - An array of strings which will be displayed in the + dropdown when open. See Dropdown.tsx for more advanced usage with DropdownEntry +- `selected: any` - Currently selected entry - `over: boolean` - Dropdown renders over instead of below - `color: string` - Color of dropdown button -- `nochevron: boolean` - Whether or not the arrow on the right hand side of the dropdown button is visible -- `noscroll: boolean` - Whether or not the dropdown menu should have a scroll bar -- `displayText: string` - Text to always display in place of the selected text +- `noChevron: boolean` - Whether or not the arrow on the right hand side of the dropdown button is visible +- `displayText: ReactNode` - Text to always display in place of the selected text - `onClick: (e) => void` - Called when dropdown button is clicked - `onSelected: (value) => void` - Called when a value is picked from the list, `value` is the value that was picked @@ -560,15 +555,18 @@ A basic text input, which allow users to enter text into a UI. **Props:** - See inherited props: [Box](#box) -- `value: string` - Value of an input. +- `value: string` - The initial value displayed on the input. - `placeholder: string` - Text placed into Input box when it's empty, otherwise nothing. Clears automatically when focused. - `fluid: boolean` - Fill all available horizontal space. - `selfClear: boolean` - Clear after hitting enter, as well as remain focused when this happens. Useful for things like chat inputs. -- `onChange: (e, value) => void` - An event, which fires when you commit -the text by either unfocusing the input box, or by pressing the Enter key. -- `onInput: (e, value) => void` - An event, which fires on every keypress. +- `onChange: (e, value) => void` - Fires when the user clicks out or presses enter. +- `onEnter: (e, value) => void` - Fires when the user hits enter. +- `onEscape: (e) => void` - Fires when the user hits escape. +- `onInput: (e, value) => void` - Fires when the user types into the input. +- `expensive: boolean` - Introduces a delay before updating the input. Useful for large filters, + where you don't want to update on every keystroke. ### `Knob` @@ -666,7 +664,7 @@ to perform some sort of action), there is a way to do that: **Props:** - `className: string` - Applies a CSS class to the element. -- `label: string|InfernoNode` - Item label. +- `label: string|ReactNode` - Item label. - `labelWrap: boolean` - Lets the label wrap and makes it not take the minimum width. - `labelColor: string` - Sets the color of the label. - `color: string` - Sets the color of the content text. @@ -732,8 +730,9 @@ to fine tune the value, or single click it to manually type a number. **Props:** - `animated: boolean` - Animates the value if it was changed externally. +- `disabled: boolean` - Makes the input field uneditable & non draggable to prevent user changes - `fluid: boolean` - Fill all available horizontal space. -- `value: number` - Value itself. +- `value: string|number` - Value itself. - `unit: string` - Unit to display to the right of value. - `minValue: number` - Lowest possible value. - `maxValue: number` - Highest possible value. @@ -741,18 +740,15 @@ to fine tune the value, or single click it to manually type a number. dragging the input. - `stepPixelSize: number` (default: 1) - Screen distance mouse needs to travel to adjust value by one `step`. -- `width: string|number` - Width of the element, in `Box` units or pixels. -- `height: string|numer` - Height of the element, in `Box` units or pixels. -- `lineHeight: string|number` - lineHeight of the element, in `Box` units or pixels. -- `fontSize: string|number` - fontSize of the element, in `Box` units or pixels. -- `format: value => value` - Format value using this function before +- `width: string` - Width of the element, in `Box` units or pixels. +- `height: string` - Height of the element, in `Box` units or pixels. +- `lineHeight: string` - lineHeight of the element, in `Box` units or pixels. +- `fontSize: string` - fontSize of the element, in `Box` units or pixels. +- `format: (value: number) => string` - Format value using this function before displaying it. -- `suppressFlicker: number` - A number in milliseconds, for which the input -will hold off from updating while events propagate through the backend. -Default is about 250ms, increase it if you still see flickering. -- `onChange: (e, value) => void` - An event, which fires when you release +- `onChange: (value: number) => void` - An event, which fires when you release the input, or successfully enter a number. -- `onDrag: (e, value) => void` - An event, which fires about every 500ms +- `onDrag: (value: number) => void` - An event, which fires about every 500ms when you drag the input up and down, on release and on manual editing. ### `Popper` @@ -761,9 +757,10 @@ Popper lets you position elements so that they don't go out of the bounds of the **Props:** -- `popperContent: InfernoNode` - The content that will be put inside the popper. -- `options?: { ... }` - An object of options to pass to `createPopper`. See [https://popper.js.org/docs/v2/constructors/#options], but the one you want most is `placement`. Valid placements are "bottom", "top", "left", and "right". You can affix "-start" and "-end" to achieve something like top left or top right respectively. You can also use "auto" (with an optional "-start" or "-end"), where a best fit will be chosen. -- `additionalStyles: { ... }` - A map of CSS styles to add to the element that will contain the popper. +- `content: ReactNode` - The content that will be put inside the popper. +- `isOpen: boolean` - Whether or not the popper is open. +- `onClickOutside?: (e) => void` - A function that will be called when the user clicks outside of the popper. +- `placement?: string` - The placement of the popper. See [https://popper.js.org/docs/v2/constructors/#placement] ### `ProgressBar` @@ -811,11 +808,11 @@ The RoundGauge component provides a visual representation of a single metric, as value={tankPressure} minValue={0} maxValue={pressureLimit} - alertAfter={pressureLimit * 0.70} + alertAfter={pressureLimit * 0.7} ranges={{ - "good": [0, pressureLimit * 0.70], - "average": [pressureLimit * 0.70, pressureLimit * 0.85], - "bad": [pressureLimit * 0.85, pressureLimit], + good: [0, pressureLimit * 0.7], + average: [pressureLimit * 0.7, pressureLimit * 0.85], + bad: [pressureLimit * 0.85, pressureLimit], }} format={formatPressure} /> ``` diff --git a/tgui/global.d.ts b/tgui/global.d.ts index 542788717ab6..9cb8e37c2e8c 100644 --- a/tgui/global.d.ts +++ b/tgui/global.d.ts @@ -41,36 +41,6 @@ type ByondType = { */ windowId: string; - /** - * True if javascript is running in BYOND. - */ - IS_BYOND: boolean; - - /** - * Version of Trident engine of Internet Explorer. Null if N/A. - */ - TRIDENT: number | null; - - /** - * 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; - /** * If `true`, unhandled errors and common mistakes result in a blue screen * of death, which stops this window from handling incoming messages and diff --git a/tgui/package.json b/tgui/package.json index 77ff3be9b01f..b4a1c15df4a0 100644 --- a/tgui/package.json +++ b/tgui/package.json @@ -12,8 +12,8 @@ "tgui:build": "BROWSERSLIST_IGNORE_OLD_DATA=true webpack", "tgui:dev": "node --experimental-modules packages/tgui-dev-server/index.js", "tgui:lint": "eslint packages --ext .js,.cjs,.ts,.tsx", - "tgui:prettier": "prettierx --check .", - "tgui:sonar": "eslint packages --ext .js,.cjs,.ts,.tsx -c .eslintrc-sonar.yml", + "tgui:prettier": "prettier --check .", + "tgui:sonar": "eslint packages -c .eslintrc-sonar.yml", "tgui:test": "jest --watch", "tgui:test-simple": "CI=true jest --color", "tgui:test-ci": "CI=true jest --color --collect-coverage", @@ -40,15 +40,15 @@ "css-loader": "^5.2.7", "eslint": "^7.32.0", "eslint-config-prettier": "^8.5.0", - "eslint-plugin-radar": "^0.2.1", "eslint-plugin-react": "^7.24.0", + "eslint-plugin-sonarjs": "^0.25.0", "eslint-plugin-unused-imports": "^1.1.4", "file-loader": "^6.2.0", "jest": "^27.0.6", "jest-circus": "^27.0.6", "jsdom": "^16.7.0", "mini-css-extract-plugin": "^1.6.2", - "prettier": "npm:prettierx@0.19.0", + "prettier": "^3.2.5", "sass": "^1.37.5", "sass-loader": "^11.1.1", "style-loader": "^2.0.0", diff --git a/tgui/packages/common/color.js b/tgui/packages/common/color.js index 672fce529b61..59935931d82b 100644 --- a/tgui/packages/common/color.js +++ b/tgui/packages/common/color.js @@ -22,6 +22,23 @@ export class Color { } return `rgba(${this.r | 0}, ${this.g | 0}, ${this.b | 0}, ${alpha})`; } + + // Darkens a color by a given percent. Returns a color, which can have toString called to get it's rgba() css value. + darken(percent) { + percent /= 100; + return new Color( + this.r - this.r * percent, + this.g - this.g * percent, + this.b - this.b * percent, + this.a, + ); + } + + // Brightens a color by a given percent. Returns a color, which can have toString called to get it's rgba() css value. + lighten(percent) { + // No point in rewriting code we already have. + return this.darken(-percent); + } } /** @@ -31,7 +48,7 @@ Color.fromHex = (hex) => new Color( parseInt(hex.substr(1, 2), 16), parseInt(hex.substr(3, 2), 16), - parseInt(hex.substr(5, 2), 16) + parseInt(hex.substr(5, 2), 16), ); /** @@ -42,7 +59,7 @@ Color.lerp = (c1, c2, n) => (c2.r - c1.r) * n + c1.r, (c2.g - c1.g) * n + c1.g, (c2.b - c1.b) * n + c1.b, - (c2.a - c1.a) * n + c1.a + (c2.a - c1.a) * n + c1.a, ); /** diff --git a/tgui/packages/common/keycodes.js b/tgui/packages/common/keycodes.ts similarity index 100% rename from tgui/packages/common/keycodes.js rename to tgui/packages/common/keycodes.ts diff --git a/tgui/packages/common/react.ts b/tgui/packages/common/react.ts index dd6430953445..5260ff6ae128 100644 --- a/tgui/packages/common/react.ts +++ b/tgui/packages/common/react.ts @@ -51,6 +51,12 @@ export const shallowDiffers = (a: object, b: object) => { return false; }; +/** + * 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; + /** * A helper to determine whether the object is renderable by React. */ @@ -60,9 +66,3 @@ export const canRender = (value: unknown) => { && 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; diff --git a/tgui/packages/common/redux.ts b/tgui/packages/common/redux.ts index 7b4999d93b1a..c8eb268f5d44 100644 --- a/tgui/packages/common/redux.ts +++ b/tgui/packages/common/redux.ts @@ -6,7 +6,7 @@ export type Reducer = ( state: State | undefined, - action: ActionType + action: ActionType, ) => State; export type Store = { @@ -21,7 +21,7 @@ type MiddlewareAPI = { }; export type Middleware = ( - storeApi: MiddlewareAPI + storeApi: MiddlewareAPI, ) => (next: Dispatch) => Dispatch; export type Action = { @@ -33,7 +33,7 @@ export type AnyAction = Action & { }; export type Dispatch = ( - action: ActionType + action: ActionType, ) => void; type StoreEnhancer = (createStoreFunction: Function) => Function; @@ -48,7 +48,7 @@ type PreparedAction = { */ export const createStore = ( reducer: Reducer, - enhancer?: StoreEnhancer + enhancer?: StoreEnhancer, ): Store => { // Apply a store enhancer (applyMiddleware is one of them). if (enhancer) { @@ -90,14 +90,14 @@ export const applyMiddleware = ( ...middlewares: Middleware[] ): StoreEnhancer => { return ( - createStoreFunction: (reducer: Reducer, enhancer?: StoreEnhancer) => Store + createStoreFunction: (reducer: Reducer, enhancer?: StoreEnhancer) => Store, ) => { return (reducer, ...args): Store => { const store = createStoreFunction(reducer, ...args); - let dispatch: Dispatch = () => { + let dispatch: Dispatch = (action, ...args) => { throw new Error( - 'Dispatching while constructing your middleware is not allowed.' + 'Dispatching while constructing your middleware is not allowed.', ); }; @@ -109,7 +109,7 @@ export const applyMiddleware = ( const chain = middlewares.map((middleware) => middleware(storeApi)); dispatch = chain.reduceRight( (next, middleware) => middleware(next), - store.dispatch + store.dispatch, ); return { @@ -129,7 +129,7 @@ export const applyMiddleware = ( * is also more flexible than the redux counterpart. */ export const combineReducers = ( - reducersObj: Record + reducersObj: Record, ): Reducer => { const keys = Object.keys(reducersObj); @@ -170,7 +170,7 @@ export const combineReducers = ( */ export const createAction = ( type: TAction, - prepare?: (...args: any[]) => PreparedAction + prepare?: (...args: any[]) => PreparedAction, ) => { const actionCreator = (...args: any[]) => { let action: Action & PreparedAction = { type }; diff --git a/tgui/packages/common/timer.ts b/tgui/packages/common/timer.ts index 49d36484200b..1fc3e11fd30e 100644 --- a/tgui/packages/common/timer.ts +++ b/tgui/packages/common/timer.ts @@ -13,7 +13,7 @@ export const debounce = any>( fn: F, time: number, - immediate = false + immediate = false, ): ((...args: Parameters) => void) => { let timeout: ReturnType | null; return (...args: Parameters) => { @@ -38,7 +38,7 @@ export const debounce = any>( */ export const throttle = any>( fn: F, - time: number + time: number, ): ((...args: Parameters) => void) => { let previouslyRun: number | null, queuedToRun: ReturnType | null; @@ -53,7 +53,7 @@ export const throttle = any>( } else { queuedToRun = setTimeout( () => invokeFn(...args), - time - (now - (previouslyRun ?? 0)) + time - (now - (previouslyRun ?? 0)), ); } }; diff --git a/tgui/packages/tgfont/package.json b/tgui/packages/tgfont/package.json index 9459aa129696..56bb38bf68e8 100644 --- a/tgui/packages/tgfont/package.json +++ b/tgui/packages/tgfont/package.json @@ -6,6 +6,6 @@ "tgfont:build": "node mkdist.cjs && fantasticon --config config.cjs" }, "dependencies": { - "fantasticon": "^1.2.2" + "fantasticon": "^1.2.3" } } diff --git a/tgui/packages/tgui-dev-server/dreamseeker.js b/tgui/packages/tgui-dev-server/dreamseeker.js index 2b25b155ae0c..d1ca2a9ac539 100644 --- a/tgui/packages/tgui-dev-server/dreamseeker.js +++ b/tgui/packages/tgui-dev-server/dreamseeker.js @@ -6,6 +6,7 @@ import { exec } from 'child_process'; import { promisify } from 'util'; + import { createLogger } from './logging.js'; import { require } from './require.js'; @@ -30,7 +31,7 @@ export class DreamSeeker { + '=' + encodeURIComponent(params[key])) .join('&'); logger.log( - `topic call at ${this.client.defaults.baseURL + '/dummy?' + query}` + `topic call at ${this.client.defaults.baseURL + '/dummy?' + query}`, ); return this.client.get('/dummy?' + query); } diff --git a/tgui/packages/tgui-dev-server/index.js b/tgui/packages/tgui-dev-server/index.js index 199e93d83632..85489ebb0499 100644 --- a/tgui/packages/tgui-dev-server/index.js +++ b/tgui/packages/tgui-dev-server/index.js @@ -4,8 +4,8 @@ * @license MIT */ -import { createCompiler } from './webpack.js'; import { reloadByondCache } from './reloader.js'; +import { createCompiler } from './webpack.js'; const noHot = process.argv.includes('--no-hot'); const noTmp = process.argv.includes('--no-tmp'); diff --git a/tgui/packages/tgui-dev-server/link/retrace.js b/tgui/packages/tgui-dev-server/link/retrace.js index 842de228fdfd..083ddb37d1c1 100644 --- a/tgui/packages/tgui-dev-server/link/retrace.js +++ b/tgui/packages/tgui-dev-server/link/retrace.js @@ -6,6 +6,7 @@ import fs from 'fs'; import { basename } from 'path'; + import { createLogger } from '../logging.js'; import { require } from '../require.js'; import { resolveGlob } from '../util.js'; @@ -30,7 +31,7 @@ export const loadSourceMaps = async (bundleDir) => { try { const file = basename(path).replace('.map', ''); const consumer = await new SourceMapConsumer( - JSON.parse(fs.readFileSync(path, 'utf8')) + JSON.parse(fs.readFileSync(path, 'utf8')), ); sourceMaps.push({ file, consumer }); } catch (err) { diff --git a/tgui/packages/tgui-dev-server/reloader.js b/tgui/packages/tgui-dev-server/reloader.js index c13a8afdfcfc..cb477a6523b0 100644 --- a/tgui/packages/tgui-dev-server/reloader.js +++ b/tgui/packages/tgui-dev-server/reloader.js @@ -7,6 +7,7 @@ import fs from 'fs'; import os from 'os'; import { basename } from 'path'; + import { DreamSeeker } from './dreamseeker.js'; import { createLogger } from './logging.js'; import { resolveGlob, resolvePath } from './util.js'; @@ -83,19 +84,19 @@ export const reloadByondCache = async (bundleDir) => { } // Get dreamseeker instances const pids = cacheDirs.map((cacheDir) => - parseInt(cacheDir.split('/cache/tmp').pop(), 10) + parseInt(cacheDir.split('/cache/tmp').pop(), 10), ); const dssPromise = DreamSeeker.getInstancesByPids(pids); // Copy assets const assets = await resolveGlob( bundleDir, - './*.+(bundle|chunk|hot-update).*' + './*.+(bundle|chunk|hot-update).*', ); for (let cacheDir of cacheDirs) { // Clear garbage const garbage = await resolveGlob( cacheDir, - './*.+(bundle|chunk|hot-update).*' + './*.+(bundle|chunk|hot-update).*', ); try { // Plant a dummy browser window file, we'll be using this to avoid world topic. For byond 515. diff --git a/tgui/packages/tgui-dev-server/util.js b/tgui/packages/tgui-dev-server/util.js index 9d07b96c71a0..54d806a629df 100644 --- a/tgui/packages/tgui-dev-server/util.js +++ b/tgui/packages/tgui-dev-server/util.js @@ -19,6 +19,7 @@ export const resolveGlob = (...sections) => { const unsafePaths = globPkg.sync(path.resolve(...sections), { strict: false, silent: true, + windowsPathsNoEscape: true, }); const safePaths = []; for (let path of unsafePaths) { diff --git a/tgui/packages/tgui-dev-server/winreg.js b/tgui/packages/tgui-dev-server/winreg.js index b61fddc1a255..9b5b3c89a8bb 100644 --- a/tgui/packages/tgui-dev-server/winreg.js +++ b/tgui/packages/tgui-dev-server/winreg.js @@ -35,8 +35,8 @@ export const regQuery = async (path, key) => { logger.error('could not find the start of the key value'); return null; } - const value = stdout.substring(indexOfValue + 4, indexOfEol); - return value; + + return stdout.substring(indexOfValue + 4, indexOfEol); } catch (err) { logger.error(err); return null; diff --git a/tgui/packages/tgui-panel/chat/renderer.js b/tgui/packages/tgui-panel/chat/renderer.js index 8f5d6600c47d..f14650ee2fa9 100644 --- a/tgui/packages/tgui-panel/chat/renderer.js +++ b/tgui/packages/tgui-panel/chat/renderer.js @@ -8,7 +8,7 @@ import { EventEmitter } from 'common/events'; import { classes } from 'common/react'; import { createLogger } from 'tgui/logging'; import { COMBINE_MAX_MESSAGES, COMBINE_MAX_TIME_WINDOW, IMAGE_RETRY_DELAY, IMAGE_RETRY_LIMIT, IMAGE_RETRY_MESSAGE_AGE, MAX_PERSISTED_MESSAGES, MAX_VISIBLE_MESSAGES, MESSAGE_PRUNE_INTERVAL, MESSAGE_TYPES, MESSAGE_TYPE_INTERNAL, MESSAGE_TYPE_UNKNOWN } from './constants'; -import { render } from 'react-dom'; +import { createRoot } from 'react-dom/client'; import { canPageAcceptType, createMessage, isSameMessage } from './model'; import { highlightNode, linkifyNode } from './replaceInTextNode'; import { Tooltip } from '../../tgui/components'; @@ -155,7 +155,7 @@ class ChatRenderer { // Find scrollable parent this.scrollNode = findNearestScrollableParent(this.rootNode); this.scrollNode.addEventListener('scroll', this.handleScroll); - setImmediate(() => { + setTimeout(() => { this.scrollToBottom(); }); // Flush the queue @@ -401,8 +401,11 @@ class ChatRenderer { childNode.removeChild(childNode.firstChild); } const Element = TGUI_CHAT_COMPONENTS[targetName]; + + const reactRoot = createRoot(childNode); + /* eslint-disable react/no-danger */ - render( + reactRoot.render( , @@ -443,13 +446,9 @@ class ChatRenderer { message.node = node; // Query all possible selectors to find out the message type if (!message.type) { - // IE8: Does not support querySelector on elements that - // are not yet in the document. - // prettier-ignore - const typeDef = !Byond.IS_LTE_IE8 && MESSAGE_TYPES - .find(typeDef => ( - typeDef.selector && node.querySelector(typeDef.selector) - )); + const typeDef = MESSAGE_TYPES.find( + (typeDef) => typeDef.selector && node.querySelector(typeDef.selector), + ); message.type = typeDef?.type || MESSAGE_TYPE_UNKNOWN; } updateMessageBadge(message); @@ -472,7 +471,7 @@ class ChatRenderer { this.rootNode.appendChild(fragment); } if (this.scrollTracking) { - setImmediate(() => this.scrollToBottom()); + setTimeout(() => this.scrollToBottom()); } } // Notify listeners that we have processed the batch @@ -549,10 +548,6 @@ class ChatRenderer { } saveToDisk() { - // Allow only on IE11 - if (Byond.IS_LTE_IE10) { - return; - } // Compile currently loaded stylesheets as CSS text let cssText = ''; const styleSheets = document.styleSheets; diff --git a/tgui/packages/tgui-panel/package.json b/tgui/packages/tgui-panel/package.json index e4c87ad6e823..d435245e3fe3 100644 --- a/tgui/packages/tgui-panel/package.json +++ b/tgui/packages/tgui-panel/package.json @@ -1,11 +1,12 @@ { "private": true, "name": "tgui-panel", - "version": "4.3.1", + "version": "5.0.1", "dependencies": { - "@types/react": "^18.2.39", + "@types/node": "^20.12.3", + "@types/react": "^18.2.74", "common": "workspace:*", - "dompurify": "^2.3.1", + "dompurify": "^2.4.9", "react": "^18.2.0", "react-dom": "^18.2.0", "tgui": "workspace:*", diff --git a/tgui/packages/tgui-panel/panelFocus.js b/tgui/packages/tgui-panel/panelFocus.js index b7cea2293149..8cff4a361b3e 100644 --- a/tgui/packages/tgui-panel/panelFocus.js +++ b/tgui/packages/tgui-panel/panelFocus.js @@ -15,7 +15,7 @@ import { focusMap } from 'tgui/focus'; // text you can select with the mouse. const MIN_SELECTION_DISTANCE = 10; -const deferredFocusMap = () => setImmediate(() => focusMap()); +const deferredFocusMap = () => setTimeout(() => focusMap()); export const setupPanelFocusHacks = () => { let focusStolen = false; diff --git a/tgui/packages/tgui-panel/settings/SettingsPanel.jsx b/tgui/packages/tgui-panel/settings/SettingsPanel.jsx index d0f249c77aff..3fd6921cb93d 100644 --- a/tgui/packages/tgui-panel/settings/SettingsPanel.jsx +++ b/tgui/packages/tgui-panel/settings/SettingsPanel.jsx @@ -7,13 +7,38 @@ import { toFixed } from 'common/math'; import { useLocalState } from 'tgui/backend'; import { useDispatch, useSelector } from 'tgui/backend'; -import { Box, Button, ColorBox, Divider, Dropdown, Flex, Input, LabeledList, NumberInput, Section, Stack, 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, addHighlightSetting, removeHighlightSetting, updateHighlightSetting } from './actions'; +import { + changeSettingsTab, + updateSettings, + addHighlightSetting, + removeHighlightSetting, + updateHighlightSetting, +} from './actions'; import { SETTINGS_TABS, FONTS, MAX_HIGHLIGHT_SETTINGS } from './constants'; -import { selectActiveTab, selectSettings, selectHighlightSettings, selectHighlightSettingById } from './selectors'; +import { + selectActiveTab, + selectSettings, + selectHighlightSettings, + selectHighlightSettingById, +} from './selectors'; export const SettingsPanel = (props) => { const activeTab = useSelector(selectActiveTab); diff --git a/tgui/packages/tgui-say/index.tsx b/tgui/packages/tgui-say/index.tsx index 41956294f46c..81b7b311e46e 100644 --- a/tgui/packages/tgui-say/index.tsx +++ b/tgui/packages/tgui-say/index.tsx @@ -1,19 +1,16 @@ import './styles/main.scss'; -import { createRenderer } from 'tgui/renderer'; import { TguiSay } from './interfaces/TguiSay'; +import { createRoot, Root } from 'react-dom/client'; -const renderApp = createRenderer(() => { - return ; -}); +let reactRoot: Root | null = null; -const setupApp = () => { - // Delay setup - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', setupApp); - return; +document.onreadystatechange = function () { + if (document.readyState !== 'complete') return; + + if (!reactRoot) { + const root = document.getElementById('react-root'); + reactRoot = createRoot(root!); } - renderApp(); + reactRoot.render(); }; - -setupApp(); diff --git a/tgui/packages/tgui-say/package.json b/tgui/packages/tgui-say/package.json index 6fc66ade82f1..55410500e586 100644 --- a/tgui/packages/tgui-say/package.json +++ b/tgui/packages/tgui-say/package.json @@ -3,9 +3,11 @@ "name": "tgui-say", "version": "1.0.0", "dependencies": { - "@types/react": "^18.2.39", + "@types/react": "^18.2.74", + "@types/react-dom": "^18.2.24", "common": "workspace:*", "react": "^18.2.0", + "react-dom": "^18.2.0", "tgui": "workspace:*", "tgui-polyfill": "workspace:*" } diff --git a/tgui/packages/tgui-say/timers.ts b/tgui/packages/tgui-say/timers.ts index 7b9fffc87fc2..85c58f7424ae 100644 --- a/tgui/packages/tgui-say/timers.ts +++ b/tgui/packages/tgui-say/timers.ts @@ -7,12 +7,12 @@ export const byondMessages = { // Debounce: Prevents spamming the server channelIncrementMsg: debounce( (visible: boolean) => Byond.sendMessage('thinking', { visible }), - 0.4 * SECONDS + 0.4 * SECONDS, ), forceSayMsg: debounce( (entry: string) => Byond.sendMessage('force', { entry, channel: 'Say' }), 1 * SECONDS, - true + true, ), // Throttle: Prevents spamming the server typingMsg: throttle(() => Byond.sendMessage('typing'), 4 * SECONDS), diff --git a/tgui/packages/tgui/assets.ts b/tgui/packages/tgui/assets.ts index e519d0c2f76c..f62bf652deae 100644 --- a/tgui/packages/tgui/assets.ts +++ b/tgui/packages/tgui/assets.ts @@ -4,10 +4,10 @@ * @license MIT */ -import { Action, AnyAction, Middleware } from '../common/redux'; - import { Dispatch } from 'common/redux'; +import { Action, AnyAction, Middleware } from '../common/redux'; + const EXCLUDED_PATTERNS = [/v4shim/i]; const loadedMappings: Record = {}; diff --git a/tgui/packages/tgui/backend.ts b/tgui/packages/tgui/backend.ts index 746e511a5027..9190d74dbe23 100644 --- a/tgui/packages/tgui/backend.ts +++ b/tgui/packages/tgui/backend.ts @@ -13,6 +13,7 @@ import { perf } from 'common/perf'; import { createAction } from 'common/redux'; + import { setupDrag } from './drag'; import { globalEvents } from './events'; import { focusMap } from './focus'; @@ -176,7 +177,7 @@ export const backendMiddleware = (store) => { Byond.winset(Byond.windowId, { 'is-visible': false, }); - setImmediate(() => focusMap()); + setTimeout(() => focusMap()); } if (type === 'backend/update') { @@ -206,7 +207,7 @@ export const backendMiddleware = (store) => { setupDrag(); // We schedule this for the next tick here because resizing and unhiding // during the same tick will flash with a white background. - setImmediate(() => { + setTimeout(() => { perf.mark('resume/start'); // Doublecheck if we are not re-suspended. const { suspended } = selectBackend(store.getState()); @@ -220,7 +221,7 @@ export const backendMiddleware = (store) => { if (process.env.NODE_ENV !== 'production') { logger.log( 'visible in', - perf.measure('render/finish', 'resume/finish') + perf.measure('render/finish', 'resume/finish'), ); } }); @@ -273,10 +274,6 @@ type BackendState = { shared: Record; suspending: boolean; suspended: boolean; - debug?: { - debugLayout: boolean; - kitchenSink: boolean; - }; }; /** @@ -316,10 +313,11 @@ type StateWithSetter = [T, (nextState: T) => void]; * @param context React context. * @param key Key which uniquely identifies this state in Redux store. * @param initialState Initializes your global variable with this value. + * @deprecated Use useState and useEffect when you can. Pass the state as a prop. */ export const useLocalState = ( key: string, - initialState: T + initialState: T, ): StateWithSetter => { const state = globalStore?.getState()?.backend; const sharedStates = state?.shared ?? {}; @@ -334,7 +332,7 @@ export const useLocalState = ( typeof nextState === 'function' ? nextState(sharedState) : nextState, - }) + }), ); }, ]; @@ -356,7 +354,7 @@ export const useLocalState = ( */ export const useSharedState = ( key: string, - initialState: T + initialState: T, ): StateWithSetter => { const state = globalStore?.getState()?.backend; const sharedStates = state?.shared ?? {}; @@ -369,7 +367,9 @@ export const useSharedState = ( key, value: JSON.stringify( - typeof nextState === 'function' ? nextState(sharedState) : nextState + typeof nextState === 'function' + ? nextState(sharedState) + : nextState, ) || '', }); }, diff --git a/tgui/packages/tgui/backend.ts.bak b/tgui/packages/tgui/backend.ts.bak new file mode 100644 index 000000000000..746e511a5027 --- /dev/null +++ b/tgui/packages/tgui/backend.ts.bak @@ -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 { globalEvents } from './events'; +import { focusMap } from './focus'; +import { createLogger } from './logging'; +import { resumeRenderer, suspendRenderer } from './renderer'; + +const logger = createLogger('backend'); + +export let globalStore; + +export const setGlobalStore = (store) => { + globalStore = store; +}; + +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') { + Byond.sendMessage('ping/reply'); + return; + } + + if (type === 'byond/mousedown') { + globalEvents.emit('byond/mousedown'); + } + + if (type === 'byond/mouseup') { + globalEvents.emit('byond/mouseup'); + } + + if (type === 'byond/ctrldown') { + globalEvents.emit('byond/ctrldown'); + } + + if (type === 'byond/ctrlup') { + globalEvents.emit('byond/ctrlup'); + } + + if (type === 'backend/suspendStart' && !suspendInterval) { + logger.log(`suspending (${Byond.windowId})`); + // Keep sending suspend messages until it succeeds. + // It may fail multiple times due to topic rate limiting. + const suspendFn = () => Byond.sendMessage('suspend'); + suspendFn(); + suspendInterval = setInterval(suspendFn, 2000); + } + + if (type === 'backend/suspendSuccess') { + suspendRenderer(); + clearInterval(suspendInterval); + suspendInterval = undefined; + Byond.winset(Byond.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(Byond.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(Byond.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 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 + // prettier-ignore + 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; + } + Byond.sendMessage('act/' + action, payload); +}; + +type BackendState = { + config: { + title: string; + status: number; + interface: string; + refreshing: boolean; + 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; + suspending: boolean; + suspended: boolean; + debug?: { + debugLayout: boolean; + kitchenSink: boolean; + }; +}; + +/** + * Selects a backend-related slice of Redux state + */ +export const selectBackend = (state: any): BackendState => + state.backend || {}; + +/** + * Get data from tgui backend. + * + * Includes the `act` function for performing DM actions. + */ +export const useBackend = () => { + const state: BackendState = globalStore?.getState()?.backend; + + return { + ...state, + act: sendAct, + }; +}; + +/** + * A tuple that contains the state and a setter function for it. + */ +type StateWithSetter = [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 = ( + key: string, + initialState: T +): StateWithSetter => { + const state = globalStore?.getState()?.backend; + const sharedStates = state?.shared ?? {}; + const sharedState = key in sharedStates ? sharedStates[key] : initialState; + return [ + sharedState, + (nextState) => { + globalStore.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 = ( + key: string, + initialState: T +): StateWithSetter => { + const state = globalStore?.getState()?.backend; + const sharedStates = state?.shared ?? {}; + const sharedState = key in sharedStates ? sharedStates[key] : initialState; + return [ + sharedState, + (nextState) => { + Byond.sendMessage({ + type: 'setSharedState', + key, + value: + JSON.stringify( + typeof nextState === 'function' ? nextState(sharedState) : nextState + ) || '', + }); + }, + ]; +}; + +export const useDispatch = () => { + return globalStore.dispatch; +}; + +export const useSelector = (selector: (state: any) => any) => { + return selector(globalStore?.getState()); +}; diff --git a/tgui/packages/tgui/components/Autofocus.tsx b/tgui/packages/tgui/components/Autofocus.tsx index a0b3f6f76590..403dbe2e9653 100644 --- a/tgui/packages/tgui/components/Autofocus.tsx +++ b/tgui/packages/tgui/components/Autofocus.tsx @@ -1,17 +1,23 @@ -import { createRef, PropsWithChildren, useEffect } from 'react'; +import { PropsWithChildren, useEffect, useRef } from 'react'; -export const Autofocus = (props: PropsWithChildren) => { - const ref = createRef(); +/** Used to force the window to steal focus on load. Children optional */ +export function Autofocus(props: PropsWithChildren) { + const { children } = props; + const ref = useRef(null); useEffect(() => { - setTimeout(() => { + const timer = setTimeout(() => { ref.current?.focus(); }, 1); + + return () => { + clearTimeout(timer); + }; }, []); return (
- {props.children} + {children}
); -}; +} diff --git a/tgui/packages/tgui/components/Blink.jsx b/tgui/packages/tgui/components/Blink.jsx index 70d2f1393eb2..dc12f84346ef 100644 --- a/tgui/packages/tgui/components/Blink.jsx +++ b/tgui/packages/tgui/components/Blink.jsx @@ -4,8 +4,8 @@ const DEFAULT_BLINKING_INTERVAL = 1000; const DEFAULT_BLINKING_TIME = 1000; export class Blink extends Component { - constructor() { - super(); + constructor(props) { + super(props); this.state = { hidden: false, }; @@ -55,13 +55,14 @@ export class Blink extends Component { clearTimeout(this.timer); } - render(props) { + render() { return ( - {props.children} + }} + > + {this.props.children} ); } diff --git a/tgui/packages/tgui/components/BlockQuote.jsx b/tgui/packages/tgui/components/BlockQuote.tsx similarity index 71% rename from tgui/packages/tgui/components/BlockQuote.jsx rename to tgui/packages/tgui/components/BlockQuote.tsx index 8c0f53a46a1e..3c627d3fac24 100644 --- a/tgui/packages/tgui/components/BlockQuote.jsx +++ b/tgui/packages/tgui/components/BlockQuote.tsx @@ -5,9 +5,11 @@ */ import { classes } from 'common/react'; -import { Box } from './Box'; -export const BlockQuote = (props) => { +import { Box, BoxProps } from './Box'; + +export function BlockQuote(props: BoxProps) { const { className, ...rest } = props; + return ; -}; +} diff --git a/tgui/packages/tgui/components/Box.tsx b/tgui/packages/tgui/components/Box.tsx index 8da3235553fa..e5808277914a 100644 --- a/tgui/packages/tgui/components/Box.tsx +++ b/tgui/packages/tgui/components/Box.tsx @@ -5,63 +5,56 @@ */ import { BooleanLike, classes } from 'common/react'; -import { createElement, ReactNode } from 'react'; +import { + createElement, + KeyboardEventHandler, + MouseEventHandler, + ReactNode, + UIEventHandler, +} from 'react'; + import { CSS_COLORS } from '../constants'; +import { logger } from '../logging'; + +type BooleanProps = Partial>; +type StringProps = Partial< + Record +>; + +export type EventHandlers = Partial<{ + onClick: MouseEventHandler; + onContextMenu: MouseEventHandler; + onDoubleClick: MouseEventHandler; + onKeyDown: KeyboardEventHandler; + onKeyUp: KeyboardEventHandler; + onMouseDown: MouseEventHandler; + onMouseMove: MouseEventHandler; + onMouseOver: MouseEventHandler; + onMouseUp: MouseEventHandler; + onScroll: UIEventHandler; +}>; + +export type BoxProps = Partial<{ + as: string; + children: ReactNode; + className: string | BooleanLike; + style: Partial; +}> & + BooleanProps & + StringProps & + EventHandlers; -export type BoxProps = { - [key: string]: any; - readonly as?: string; - readonly className?: string | BooleanLike; - readonly children?: ReactNode; - readonly position?: string | BooleanLike; - readonly overflow?: string | BooleanLike; - readonly overflowX?: string | BooleanLike; - readonly overflowY?: string | BooleanLike; - readonly top?: string | BooleanLike; - readonly bottom?: string | BooleanLike; - readonly left?: string | BooleanLike; - readonly right?: string | BooleanLike; - readonly width?: string | BooleanLike; - readonly minWidth?: string | BooleanLike; - readonly maxWidth?: string | BooleanLike; - readonly height?: string | BooleanLike; - readonly minHeight?: string | BooleanLike; - readonly maxHeight?: string | BooleanLike; - readonly fontSize?: string | BooleanLike; - readonly fontFamily?: string; - readonly lineHeight?: string | BooleanLike; - readonly opacity?: number; - readonly textAlign?: string | BooleanLike; - readonly verticalAlign?: string | BooleanLike; - readonly inline?: BooleanLike; - readonly bold?: BooleanLike; - readonly italic?: BooleanLike; - readonly nowrap?: BooleanLike; - readonly preserveWhitespace?: BooleanLike; - readonly m?: string | BooleanLike; - readonly mx?: string | BooleanLike; - readonly my?: string | BooleanLike; - readonly mt?: string | BooleanLike; - readonly mb?: string | BooleanLike; - readonly ml?: string | BooleanLike; - readonly mr?: string | BooleanLike; - readonly p?: string | BooleanLike; - readonly px?: string | BooleanLike; - readonly py?: string | BooleanLike; - readonly pt?: string | BooleanLike; - readonly pb?: string | BooleanLike; - readonly pl?: string | BooleanLike; - readonly pr?: string | BooleanLike; - readonly color?: string | BooleanLike; - readonly textColor?: string | BooleanLike; - readonly backgroundColor?: string | BooleanLike; - readonly fillPositionedParent?: boolean; +// Don't you dare put this elsewhere +type DangerDoNotUse = { + dangerouslySetInnerHTML?: { + __html: any; + }; }; /** * Coverts our rem-like spacing unit into a CSS unit. */ -export const unit = (value: unknown): string | undefined => { +export const unit = (value: unknown) => { if (typeof value === 'string') { // Transparently convert pixels into rem units if (value.endsWith('px')) { @@ -77,7 +70,7 @@ export const unit = (value: unknown): string | undefined => { /** * Same as `unit`, but half the size for integers numbers. */ -export const halfUnit = (value: unknown): string | undefined => { +export const halfUnit = (value: unknown) => { if (typeof value === 'string') { return unit(value); } @@ -89,7 +82,7 @@ export const halfUnit = (value: unknown): string | undefined => { const isColorCode = (str: unknown) => !isColorClass(str); const isColorClass = (str: unknown): boolean => { - return typeof str === 'string' && CSS_COLORS.includes(str); + return typeof str === 'string' && CSS_COLORS.includes(str as any); }; const mapRawPropTo = (attrName) => (style, value) => { @@ -126,9 +119,11 @@ const mapColorPropTo = (attrName) => (style, value) => { // String / number props const stringStyleMap = { + align: mapRawPropTo('textAlign'), bottom: mapUnitPropTo('bottom', unit), fontFamily: mapRawPropTo('fontFamily'), fontSize: mapUnitPropTo('fontSize', unit), + fontWeight: mapRawPropTo('fontWeight'), height: mapUnitPropTo('height', unit), left: mapUnitPropTo('left', unit), maxHeight: mapUnitPropTo('maxHeight', unit), @@ -160,12 +155,12 @@ const stringStyleMap = { 'Left', 'Right', ]), - mx: mapDirectionalUnitPropTo('margin', halfUnit, ['Left', 'Right']), - my: mapDirectionalUnitPropTo('margin', halfUnit, ['Top', 'Bottom']), - mt: mapUnitPropTo('marginTop', halfUnit), mb: mapUnitPropTo('marginBottom', halfUnit), ml: mapUnitPropTo('marginLeft', halfUnit), mr: mapUnitPropTo('marginRight', halfUnit), + mt: mapUnitPropTo('marginTop', halfUnit), + mx: mapDirectionalUnitPropTo('margin', halfUnit, ['Left', 'Right']), + my: mapDirectionalUnitPropTo('margin', halfUnit, ['Top', 'Bottom']), // Padding p: mapDirectionalUnitPropTo('padding', halfUnit, [ 'Top', @@ -173,18 +168,21 @@ const stringStyleMap = { 'Left', 'Right', ]), - px: mapDirectionalUnitPropTo('padding', halfUnit, ['Left', 'Right']), - py: mapDirectionalUnitPropTo('padding', halfUnit, ['Top', 'Bottom']), - pt: mapUnitPropTo('paddingTop', halfUnit), pb: mapUnitPropTo('paddingBottom', halfUnit), pl: mapUnitPropTo('paddingLeft', halfUnit), pr: mapUnitPropTo('paddingRight', halfUnit), + pt: mapUnitPropTo('paddingTop', halfUnit), + px: mapDirectionalUnitPropTo('padding', halfUnit, ['Left', 'Right']), + py: mapDirectionalUnitPropTo('padding', halfUnit, ['Top', 'Bottom']), // Color props color: mapColorPropTo('color'), textColor: mapColorPropTo('color'), backgroundColor: mapColorPropTo('backgroundColor'), +} as const; - // Utility props +// Boolean props +const booleanStyleMap = { + bold: mapBooleanPropTo('fontWeight', 'bold'), fillPositionedParent: (style, value) => { if (value) { style['position'] = 'absolute'; @@ -194,11 +192,6 @@ const stringStyleMap = { style['right'] = 0; } }, -} as const; - -// Boolean props -const booleanStyleMap = { - bold: mapBooleanPropTo('fontWeight', 'bold'), inline: mapBooleanPropTo('display', 'inline-block'), italic: mapBooleanPropTo('fontStyle', 'italic'), nowrap: mapBooleanPropTo('whiteSpace', 'nowrap'), @@ -242,7 +235,7 @@ export const computeBoxClassName = (props: BoxProps) => { ]); }; -export const Box = (props: BoxProps) => { +export const Box = (props: BoxProps & DangerDoNotUse) => { const { as = 'div', className, children, ...rest } = props; // Compute class name and styles @@ -252,7 +245,9 @@ export const Box = (props: BoxProps) => { const computedProps = computeBoxProps(rest); if (as === 'img') { - computedProps.style['-ms-interpolation-mode'] = 'nearest-neighbor'; + logger.error( + 'Box component cannot be used as an image. Use Image component instead.', + ); } // Render the component @@ -262,6 +257,6 @@ export const Box = (props: BoxProps) => { ...computedProps, className: computedClassName, }, - children + children, ); }; diff --git a/tgui/packages/tgui/components/Button.jsx b/tgui/packages/tgui/components/Button.jsx deleted file mode 100644 index dbffb6a72c95..000000000000 --- a/tgui/packages/tgui/components/Button.jsx +++ /dev/null @@ -1,367 +0,0 @@ -/** - * @file - * @copyright 2020 Aleksej Komarov - * @license MIT - */ - -import { KEY_ENTER, KEY_ESCAPE, KEY_SPACE } from 'common/keycodes'; -import { classes, pureComponentHooks } from 'common/react'; -import { Component, createRef } from 'react'; -import { createLogger } from '../logging'; -import { Box, computeBoxClassName, computeBoxProps } from './Box'; -import { Icon } from './Icon'; -import { Tooltip } from './Tooltip'; - -const logger = createLogger('Button'); - -export const Button = (props) => { - const { - className, - fluid, - icon, - iconRotation, - iconSpin, - iconColor, - iconPosition, - color, - disabled, - selected, - tooltip, - tooltipPosition, - ellipsis, - compact, - circular, - content, - children, - onclick, - onClick, - verticalAlignContent, - ...rest - } = props; - const hasContent = !!(content || children); - // A warning about the lowercase onclick - if (onclick) { - logger.warn( - `Lowercase 'onclick' is not supported on Button and lowercase` + - ` prop names are discouraged in general. Please use a camelCase` + - `'onClick' instead and read: ` + - `https://infernojs.org/docs/guides/event-handling` - ); - } - rest.onClick = (e) => { - if (!disabled && onClick) { - onClick(e); - } - }; - // IE8: Use "unselectable" because "user-select" doesn't work. - if (Byond.IS_LTE_IE8) { - rest.unselectable = true; - } - let buttonContent = ( -
{ - if (props.captureKeys === false) { - return; - } - const keyCode = window.event ? e.which : e.keyCode; - // Simulate a click when pressing space or enter. - if (keyCode === KEY_SPACE || keyCode === KEY_ENTER) { - e.preventDefault(); - if (!disabled && onClick) { - onClick(e); - } - return; - } - // Refocus layout on pressing escape. - if (keyCode === KEY_ESCAPE) { - e.preventDefault(); - return; - } - }} - {...computeBoxProps(rest)}> -
- {icon && iconPosition !== 'right' && ( - - )} - {content} - {children} - {icon && iconPosition === 'right' && ( - - )} -
-
- ); - - if (tooltip) { - buttonContent = ( - - {buttonContent} - - ); - } - - return buttonContent; -}; - -Button.defaultHooks = pureComponentHooks; - -export const ButtonCheckbox = (props) => { - const { checked, ...rest } = props; - return ( - + ); +}; + +Button.Confirm = ButtonConfirm; + +type InputProps = Partial<{ + currentValue: string; + defaultValue: string; + fluid: boolean; + maxLength: number; + onCommit: (e: any, value: string) => void; + placeholder: string; +}> & + Props; + +/** Accepts and handles user input. */ +const ButtonInput = (props: InputProps) => { + const { + children, + color = 'default', + content, + currentValue, + defaultValue, + disabled, + fluid, + icon, + iconRotation, + iconSpin, + maxLength, + onCommit = () => null, + placeholder, + tooltip, + tooltipPosition, + ...rest + } = props; + const [inInput, setInInput] = useState(false); + const inputRef = createRef(); + + const toDisplay = content || children; + + const commitResult = (e) => { + const input = inputRef.current; + if (!input) return; + + const hasValue = input.value !== ''; + if (hasValue) { + onCommit(e, input.value); + } else { + if (defaultValue) { + onCommit(e, defaultValue); + } + } + }; + + useEffect(() => { + const input = inputRef.current; + if (!input) return; + + if (inInput) { + input.value = currentValue || ''; + try { + input.focus(); + input.select(); + } catch {} + } + }, [inInput, currentValue]); + + let buttonContent = ( + setInInput(true)}> + {icon && } +
{toDisplay}
+ { + if (!inInput) { + return; + } + setInInput(false); + commitResult(event); + }} + onKeyDown={(event) => { + if (event.key === KEY.Enter) { + setInInput(false); + commitResult(event); + return; + } + if (event.key === KEY.Escape) { + setInInput(false); + } + }} + /> +
+ ); + + if (tooltip) { + buttonContent = ( + + {buttonContent} + + ); + } + + return buttonContent; +}; + +Button.Input = ButtonInput; + +type FileProps = { + readonly accept: string; + readonly multiple?: boolean; + readonly onSelectFiles: (files: string | string[]) => void; +} & Props; + +/** Accepts file input */ +// eslint-disable-next-line func-style +function ButtonFile(props: FileProps) { + const { accept, multiple, onSelectFiles, ...rest } = props; + + const inputRef = useRef(null); + + // eslint-disable-next-line func-style + async function read(files: FileList) { + const promises = Array.from(files).map((file) => { + const reader = new FileReader(); + + return new Promise((resolve) => { + reader.onload = () => resolve(reader.result as string); + reader.readAsText(file); + }); + }); + + return await Promise.all(promises); + } + + // eslint-disable-next-line func-style + async function handleChange(event: ChangeEvent) { + const files = event.target.files; + if (files?.length) { + const readFiles = await read(files); + onSelectFiles(multiple ? readFiles : readFiles[0]); + } + } + + return ( + <> + - - {buttons && ( -
{buttons}
- )} - - {open && {children}} -
- ); - } -} diff --git a/tgui/packages/tgui/components/Collapsible.tsx b/tgui/packages/tgui/components/Collapsible.tsx new file mode 100644 index 000000000000..9f5f944b0ec0 --- /dev/null +++ b/tgui/packages/tgui/components/Collapsible.tsx @@ -0,0 +1,45 @@ +/** + * @file + * @copyright 2020 Aleksej Komarov + * @license MIT + */ + +import { ReactNode, useState } from 'react'; + +import { Box, BoxProps } from './Box'; +import { Button } from './Button'; + +type Props = Partial<{ + buttons: ReactNode; + open: boolean; + title: ReactNode; + icon: string; +}> & + BoxProps; + +export function Collapsible(props: Props) { + const { children, color, title, buttons, icon, ...rest } = props; + const [open, setOpen] = useState(props.open); + + return ( + +
+
+ +
+ {buttons && ( +
{buttons}
+ )} +
+ {open && {children}} +
+ ); +} diff --git a/tgui/packages/tgui/components/ColorBox.jsx b/tgui/packages/tgui/components/ColorBox.jsx deleted file mode 100644 index a6203ca4694c..000000000000 --- a/tgui/packages/tgui/components/ColorBox.jsx +++ /dev/null @@ -1,31 +0,0 @@ -/** - * @file - * @copyright 2020 Aleksej Komarov - * @license MIT - */ - -import { classes, pureComponentHooks } from 'common/react'; -import { computeBoxClassName, computeBoxProps } from './Box'; - -export const ColorBox = (props) => { - // prettier-ignore - const { - content, - children, - className, - color, - backgroundColor, - ...rest - } = props; - rest.color = content ? null : 'transparent'; - rest.backgroundColor = color || backgroundColor; - return ( -
- {content || '.'} -
- ); -}; - -ColorBox.defaultHooks = pureComponentHooks; diff --git a/tgui/packages/tgui/components/ColorBox.tsx b/tgui/packages/tgui/components/ColorBox.tsx new file mode 100644 index 000000000000..b04cdd9241ae --- /dev/null +++ b/tgui/packages/tgui/components/ColorBox.tsx @@ -0,0 +1,30 @@ +/** + * @file + * @copyright 2020 Aleksej Komarov + * @license MIT + */ + +import { classes } from 'common/react'; +import { ReactNode } from 'react'; + +import { BoxProps, computeBoxClassName, computeBoxProps } from './Box'; + +type Props = { + content?: ReactNode; +} & BoxProps; + +export function ColorBox(props: Props) { + const { content, children, className, ...rest } = props; + + rest.color = content ? null : 'default'; + rest.backgroundColor = props.color || 'default'; + + return ( +
+ {content || '.'} +
+ ); +} diff --git a/tgui/packages/tgui/components/Dimmer.jsx b/tgui/packages/tgui/components/Dimmer.tsx similarity index 61% rename from tgui/packages/tgui/components/Dimmer.jsx rename to tgui/packages/tgui/components/Dimmer.tsx index 85e046ca0dbd..d1ddb64c4e0c 100644 --- a/tgui/packages/tgui/components/Dimmer.jsx +++ b/tgui/packages/tgui/components/Dimmer.tsx @@ -5,13 +5,15 @@ */ import { classes } from 'common/react'; -import { Box } from './Box'; -export const Dimmer = (props) => { +import { Box, BoxProps } from './Box'; + +export function Dimmer(props: BoxProps) { const { className, children, ...rest } = props; + return ( - +
{children}
); -}; +} diff --git a/tgui/packages/tgui/components/Divider.jsx b/tgui/packages/tgui/components/Divider.tsx similarity index 66% rename from tgui/packages/tgui/components/Divider.jsx rename to tgui/packages/tgui/components/Divider.tsx index 8cbf9b77d7d0..1582aeadfd44 100644 --- a/tgui/packages/tgui/components/Divider.jsx +++ b/tgui/packages/tgui/components/Divider.tsx @@ -6,8 +6,14 @@ import { classes } from 'common/react'; -export const Divider = (props) => { - const { vertical, hidden } = props; +type Props = Partial<{ + hidden: boolean; + vertical: boolean; +}>; + +export function Divider(props: Props) { + const { hidden, vertical } = props; + return (
{ ])} /> ); -}; +} diff --git a/tgui/packages/tgui/components/DraggableControl.jsx b/tgui/packages/tgui/components/DraggableControl.jsx index 018118dd7966..bb55287b02a4 100644 --- a/tgui/packages/tgui/components/DraggableControl.jsx +++ b/tgui/packages/tgui/components/DraggableControl.jsx @@ -5,8 +5,8 @@ */ import { clamp } from 'common/math'; -import { pureComponentHooks } from 'common/react'; import { Component, createRef } from 'react'; + import { AnimatedNumber } from './AnimatedNumber'; const DEFAULT_UPDATE_RATE = 400; @@ -97,13 +97,13 @@ export class DraggableControl extends Component { state.internalValue = clamp( state.internalValue + (offset * step) / stepPixelSize, minValue - step, - maxValue + step + maxValue + step, ); // Clamp the final value state.value = clamp( state.internalValue - (state.internalValue % step) + stepOffset, minValue, - maxValue + maxValue, ); state.origin = getScalarScreenOffset(e, dragMatrix); } else if (Math.abs(offset) > 4) { @@ -196,8 +196,8 @@ export class DraggableControl extends Component { style={{ display: !editing ? 'none' : undefined, height: height, - 'line-height': lineHeight, - 'font-size': fontSize, + lineHeight: lineHeight, + fontsize: fontSize, }} onBlur={(e) => { if (!editing) { @@ -276,7 +276,6 @@ export class DraggableControl extends Component { } } -DraggableControl.defaultHooks = pureComponentHooks; DraggableControl.defaultProps = { minValue: -Infinity, maxValue: +Infinity, diff --git a/tgui/packages/tgui/components/Dropdown.tsx b/tgui/packages/tgui/components/Dropdown.tsx index b7491c84dca6..934bbcc231e4 100644 --- a/tgui/packages/tgui/components/Dropdown.tsx +++ b/tgui/packages/tgui/components/Dropdown.tsx @@ -40,6 +40,8 @@ type Props = { menuWidth: string; /** Whether or not the arrow on the right hand side of the dropdown button is visible */ noChevron: boolean; + /** Whether or not the scrollbar on the right hand side of the dropdown button is visible */ + noScroll: boolean; /** Called when dropdown button is clicked */ onClick: (event) => void; /** Dropdown renders over instead of below */ @@ -66,6 +68,7 @@ export const Dropdown = (props: Props) => { iconSpin, menuWidth = '15rem', noChevron, + noScroll, onClick, onSelected, options = [], @@ -121,7 +124,11 @@ export const Dropdown = (props: Props) => { placement={over ? 'top-start' : 'bottom-start'} content={
{options.length === 0 && ( diff --git a/tgui/packages/tgui/components/Flex.tsx b/tgui/packages/tgui/components/Flex.tsx index da063f0296f5..cb0cf602dbf9 100644 --- a/tgui/packages/tgui/components/Flex.tsx +++ b/tgui/packages/tgui/components/Flex.tsx @@ -22,8 +22,6 @@ export const computeFlexClassName = (props: FlexProps) => { return classes([ 'Flex', props.inline && 'Flex--inline', - Byond.IS_LTE_IE10 && 'Flex--iefix', - Byond.IS_LTE_IE10 && props.direction === 'column' && 'Flex--iefix--column', computeBoxClassName(props), ]); }; @@ -64,11 +62,7 @@ export type FlexItemProps = BoxProps & }>; export const computeFlexItemClassName = (props: FlexItemProps) => { - return classes([ - 'Flex__item', - Byond.IS_LTE_IE10 && 'Flex__item--iefix', - computeBoxClassName(props), - ]); + return classes(['Flex__item', computeBoxClassName(props)]); }; export const computeFlexItemProps = (props: FlexItemProps) => { diff --git a/tgui/packages/tgui/components/Flex.tsx.bak b/tgui/packages/tgui/components/Flex.tsx.bak new file mode 100644 index 000000000000..da063f0296f5 --- /dev/null +++ b/tgui/packages/tgui/components/Flex.tsx.bak @@ -0,0 +1,110 @@ +/** + * @file + * @copyright 2020 Aleksej Komarov + * @license MIT + */ + +import { classes } from 'common/react'; +import { BoxProps, computeBoxClassName, computeBoxProps, unit } from './Box'; + +export type FlexProps = Partial<{ + align: string | boolean; + direction: string; + inline: boolean; + justify: string; + scrollable: boolean; + style: Partial; + wrap: string | boolean; +}> & + BoxProps; + +export const computeFlexClassName = (props: FlexProps) => { + return classes([ + 'Flex', + props.inline && 'Flex--inline', + Byond.IS_LTE_IE10 && 'Flex--iefix', + Byond.IS_LTE_IE10 && props.direction === 'column' && 'Flex--iefix--column', + computeBoxClassName(props), + ]); +}; + +export const computeFlexProps = (props: FlexProps) => { + const { className, direction, wrap, align, justify, inline, ...rest } = props; + + return computeBoxProps({ + style: { + ...rest.style, + flexDirection: direction, + flexWrap: wrap === true ? 'wrap' : wrap, + alignItems: align, + justifyContent: justify, + }, + ...rest, + }); +}; + +export const Flex = (props) => { + const { className, ...rest } = props; + return ( +
+ ); +}; + +export type FlexItemProps = BoxProps & + Partial<{ + grow: number | boolean; + order: number; + shrink: number | boolean; + basis: string | number; + align: string | boolean; + style: Partial; + }>; + +export const computeFlexItemClassName = (props: FlexItemProps) => { + return classes([ + 'Flex__item', + Byond.IS_LTE_IE10 && 'Flex__item--iefix', + computeBoxClassName(props), + ]); +}; + +export const computeFlexItemProps = (props: FlexItemProps) => { + const { className, style, grow, order, shrink, basis, align, ...rest } = + props; + + const computedBasis = + basis ?? + // IE11: Set basis to specified width if it's known, which fixes certain + // bugs when rendering tables inside the flex. + props.width ?? + // If grow is used, basis should be set to 0 to be consistent with + // flex css shorthand `flex: 1`. + (grow !== undefined ? 0 : undefined); + + return computeBoxProps({ + style: { + ...style, + flexGrow: grow !== undefined && Number(grow), + flexShrink: shrink !== undefined && Number(shrink), + flexBasis: unit(computedBasis), + order: order, + alignSelf: align, + }, + ...rest, + }); +}; + +const FlexItem = (props) => { + const { className, ...rest } = props; + return ( +
+ ); +}; + +Flex.Item = FlexItem; diff --git a/tgui/packages/tgui/components/Icon.jsx b/tgui/packages/tgui/components/Icon.jsx deleted file mode 100644 index e9a4d7fd7ba9..000000000000 --- a/tgui/packages/tgui/components/Icon.jsx +++ /dev/null @@ -1,71 +0,0 @@ -/** - * @file - * @copyright 2020 Aleksej Komarov - * @author Original Aleksej Komarov - * @author Changes ThePotato97 - * @license MIT - */ - -import { classes, pureComponentHooks } from 'common/react'; -import { computeBoxClassName, computeBoxProps } from './Box'; - -const FA_OUTLINE_REGEX = /-o$/; - -export const Icon = (props) => { - const { name, size, spin, className, rotation, inverse, ...rest } = props; - - if (size) { - if (!rest.style) { - rest.style = {}; - } - rest.style['font-size'] = size * 100 + '%'; - } - if (typeof rotation === 'number') { - if (!rest.style) { - rest.style = {}; - } - rest.style['transform'] = `rotate(${rotation}deg)`; - } - - const boxProps = computeBoxProps(rest); - - let iconClass = ''; - if (name.startsWith('tg-')) { - // tgfont icon - iconClass = name; - } else { - // font awesome icon - const faRegular = FA_OUTLINE_REGEX.test(name); - const faName = name.replace(FA_OUTLINE_REGEX, ''); - // prettier-ignore - iconClass = (faRegular ? 'far ' : 'fas ') - + 'fa-' + faName - + (spin ? ' fa-spin' : ''); - } - return ( - - ); -}; - -Icon.defaultHooks = pureComponentHooks; - -export const IconStack = (props) => { - const { className, children, ...rest } = props; - return ( - - {children} - - ); -}; - -Icon.Stack = IconStack; diff --git a/tgui/packages/tgui/components/Icon.tsx b/tgui/packages/tgui/components/Icon.tsx new file mode 100644 index 000000000000..cf0b55dfa8cb --- /dev/null +++ b/tgui/packages/tgui/components/Icon.tsx @@ -0,0 +1,91 @@ +/** + * @file + * @copyright 2020 Aleksej Komarov + * @author Original Aleksej Komarov + * @author Changes ThePotato97 + * @license MIT + */ + +import { BooleanLike, classes } from 'common/react'; +import { ReactNode } from 'react'; + +import { BoxProps, computeBoxClassName, computeBoxProps } from './Box'; + +const FA_OUTLINE_REGEX = /-o$/; + +type IconPropsUnique = { name: string } & Partial<{ + size: number; + spin: BooleanLike; + className: string; + rotation: number; + style: Partial; +}>; + +export type IconProps = IconPropsUnique & BoxProps; + +export const Icon = (props: IconProps) => { + const { name, size, spin, className, rotation, ...rest } = props; + + const customStyle = rest.style || {}; + if (size) { + customStyle.fontSize = size * 100 + '%'; + } + if (rotation) { + customStyle.transform = `rotate(${rotation}deg)`; + } + rest.style = customStyle; + + const boxProps = computeBoxProps(rest); + + let iconClass = ''; + if (name.startsWith('tg-')) { + // tgfont icon + iconClass = name; + } else { + // font awesome icon + const faRegular = FA_OUTLINE_REGEX.test(name); + const faName = name.replace(FA_OUTLINE_REGEX, ''); + const preprendFa = !faName.startsWith('fa-'); + + iconClass = faRegular ? 'far ' : 'fas '; + if (preprendFa) { + iconClass += 'fa-'; + } + iconClass += faName; + if (spin) { + iconClass += ' fa-spin'; + } + } + return ( + + ); +}; + +type IconStackUnique = { + children: ReactNode; + className?: string; +}; + +export type IconStackProps = IconStackUnique & BoxProps; + +export const IconStack = (props: IconStackProps) => { + const { className, children, ...rest } = props; + return ( + + {children} + + ); +}; + +Icon.Stack = IconStack; diff --git a/tgui/packages/tgui/components/Image.tsx b/tgui/packages/tgui/components/Image.tsx new file mode 100644 index 000000000000..5d3a943feb0f --- /dev/null +++ b/tgui/packages/tgui/components/Image.tsx @@ -0,0 +1,63 @@ +import { useRef } from 'react'; + +import { BoxProps, computeBoxProps } from './Box'; + +type Props = Partial<{ + /** True is default, this fixes an ie thing */ + fixBlur: boolean; + /** False by default. Good if you're fetching images on UIs that do not auto update. This will attempt to fix the 'x' icon 5 times. */ + fixErrors: boolean; + /** Fill is default. */ + objectFit: 'contain' | 'cover'; +}> & + IconUnion & + BoxProps; + +// at least one of these is required +type IconUnion = + | { + className?: string; + src: string; + } + | { + className: string; + src?: string; + }; + +const maxAttempts = 5; + +/** Image component. Use this instead of Box as="img". */ +export function Image(props: Props) { + const { + fixBlur = true, + fixErrors = false, + objectFit = 'fill', + src, + ...rest + } = props; + const attempts = useRef(0); + + const computedProps = computeBoxProps(rest); + computedProps['style'] = { + ...computedProps.style, + '-ms-interpolation-mode': fixBlur ? 'nearest-neighbor' : 'auto', + objectFit, + }; + + return ( + { + if (fixErrors && attempts.current < maxAttempts) { + const imgElement = event.currentTarget; + + setTimeout(() => { + imgElement.src = `${src}?attempt=${attempts.current}`; + attempts.current++; + }, 1000); + } + }} + src={src} + {...computedProps} + /> + ); +} diff --git a/tgui/packages/tgui/components/InfinitePlane.jsx b/tgui/packages/tgui/components/InfinitePlane.jsx index e298537b46df..5277f2ad7968 100644 --- a/tgui/packages/tgui/components/InfinitePlane.jsx +++ b/tgui/packages/tgui/components/InfinitePlane.jsx @@ -1,8 +1,9 @@ +import { Component } from 'react'; + import { computeBoxProps } from './Box'; -import { Stack } from './Stack'; -import { ProgressBar } from './ProgressBar'; import { Button } from './Button'; -import { Component } from 'react'; +import { ProgressBar } from './ProgressBar'; +import { Stack } from './Stack'; const ZOOM_MIN_VAL = 0.5; const ZOOM_MAX_VAL = 1.5; @@ -10,8 +11,8 @@ const ZOOM_MAX_VAL = 1.5; const ZOOM_INCREMENT = 0.1; export class InfinitePlane extends Component { - constructor() { - super(); + constructor(props) { + super(props); this.state = { mouseDown: false, @@ -139,30 +140,32 @@ export class InfinitePlane extends Component { overflow: 'hidden', position: 'relative', }, - })}> + })} + >
+ position: 'fixed', + transform: `translate(${finalLeft}px, ${finalTop}px) scale(${zoom})`, + transformOrigin: 'top left', + height: '100%', + width: '100%', + }} + > {children}
@@ -174,7 +177,8 @@ export class InfinitePlane extends Component { + maxValue={ZOOM_MAX_VAL} + > {zoom}x diff --git a/tgui/packages/tgui/components/Input.tsx b/tgui/packages/tgui/components/Input.tsx new file mode 100644 index 000000000000..36d928ce2151 --- /dev/null +++ b/tgui/packages/tgui/components/Input.tsx @@ -0,0 +1,181 @@ +/** + * @file + * @copyright 2020 Aleksej Komarov + * @license MIT + */ + +import { KEY } from 'common/keys'; +import { classes } from 'common/react'; +import { debounce } from 'common/timer'; +import { KeyboardEvent, SyntheticEvent, useEffect, useRef } from 'react'; + +import { Box, BoxProps } from './Box'; + +type ConditionalProps = + | { + /** + * Mark this if you want to debounce onInput. + * + * This is useful for expensive filters, large lists etc. + * + * Requires `onInput` to be set. + */ + expensive?: boolean; + /** + * Fires on each key press / value change. Used for searching. + * + * If it's a large list, consider using `expensive` prop. + */ + onInput: (event: SyntheticEvent, value: string) => void; + } + | { + /** This prop requires onInput to be set */ + expensive?: never; + onInput?: never; + }; + +type OptionalProps = Partial<{ + /** Automatically focuses the input on mount */ + autoFocus: boolean; + /** Automatically selects the input value on focus */ + autoSelect: boolean; + /** The class name of the input */ + className: string; + /** Disables the input */ + disabled: boolean; + /** Mark this if you want the input to be as wide as possible */ + fluid: boolean; + /** The maximum length of the input value */ + maxLength: number; + /** Mark this if you want to use a monospace font */ + monospace: boolean; + /** Fires when user is 'done typing': Clicked out, blur, enter key */ + onChange: (event: SyntheticEvent, value: string) => void; + /** Fires once the enter key is pressed */ + onEnter?: (event: SyntheticEvent, value: string) => void; + /** Fires once the escape key is pressed */ + onEscape: (event: SyntheticEvent) => void; + /** The placeholder text when everything is cleared */ + placeholder: string; + /** Clears the input value on enter */ + selfClear: boolean; + /** The state variable of the input. */ + value: string | number; +}>; + +type Props = OptionalProps & ConditionalProps & BoxProps; + +export function toInputValue(value: string | number | undefined) { + return typeof value !== 'number' && typeof value !== 'string' + ? '' + : String(value); +} + +const inputDebounce = debounce((onInput: () => void) => onInput(), 250); + +/** + * ### Input + * A basic text input which allow users to enter text into a UI. + * > Input does not support custom font size and height due to the way + * > it's implemented in CSS. Eventually, this needs to be fixed. + */ +export function Input(props: Props) { + const { + autoFocus, + autoSelect, + className, + disabled, + expensive, + fluid, + maxLength, + monospace, + onChange, + onEnter, + onEscape, + onInput, + placeholder, + selfClear, + value, + ...rest + } = props; + + // The ref to the input field + const inputRef = useRef(null); + + function handleInput(event: SyntheticEvent) { + if (!onInput) return; + + const value = event.currentTarget?.value; + + if (expensive) { + inputDebounce(() => onInput(event, value)); + } else { + onInput(event, value); + } + } + + function handleKeyDown(event: KeyboardEvent) { + if (event.key === KEY.Enter) { + onEnter?.(event, event.currentTarget.value); + if (selfClear) { + event.currentTarget.value = ''; + } else { + event.currentTarget.blur(); + onChange?.(event, event.currentTarget.value); + } + + return; + } + + if (event.key === KEY.Escape) { + onEscape?.(event); + + event.currentTarget.value = toInputValue(value); + event.currentTarget.blur(); + } + } + + /** Focuses the input on mount */ + useEffect(() => { + const input = inputRef.current; + if (!input) return; + + const newValue = toInputValue(value); + + if (input.value !== newValue) input.value = newValue; + + if (!autoFocus && !autoSelect) return; + + setTimeout(() => { + input.focus(); + + if (autoSelect) { + input.select(); + } + }, 1); + }, []); + + return ( + +
.
+ onChange?.(event, event.target.value)} + onChange={handleInput} + onKeyDown={handleKeyDown} + placeholder={placeholder} + ref={inputRef} + /> +
+ ); +} diff --git a/tgui/packages/tgui/components/Knob.jsx b/tgui/packages/tgui/components/Knob.tsx similarity index 53% rename from tgui/packages/tgui/components/Knob.jsx rename to tgui/packages/tgui/components/Knob.tsx index 1d40ff1ed231..d571569b0020 100644 --- a/tgui/packages/tgui/components/Knob.jsx +++ b/tgui/packages/tgui/components/Knob.tsx @@ -5,42 +5,84 @@ */ import { keyOfMatchingRange, scale } from 'common/math'; -import { classes } from 'common/react'; -import { computeBoxClassName, computeBoxProps } from './Box'; +import { BooleanLike, classes } from 'common/react'; + +import { BoxProps, computeBoxClassName, computeBoxProps } from './Box'; import { DraggableControl } from './DraggableControl'; -import { NumberInput } from './NumberInput'; -export const Knob = (props) => { - // IE8: I don't want to support a yet another component on IE8. - // IE8: It also can't handle SVG. - if (Byond.IS_LTE_IE8) { - return ; - } +type Props = { + /** Value itself, controls the position of the cursor. */ + value: number; +} & Partial<{ + /** Animates the value if it was changed externally. */ + animated: boolean; + /** Knob can be bipolar or unipolar. */ + bipolar: boolean; + /** Color of the outer ring around the knob. */ + color: string | BooleanLike; + /** If set, this value will be used to set the fill percentage of the outer ring independently of the main value. */ + fillValue: number; + /** Format value using this function before displaying it. */ + format: (value: number) => string; + /** Highest possible value. */ + maxValue: number; + /** Lowest possible value. */ + minValue: number; + /** Adjust value by this amount when dragging the input. */ + onChange: (event: Event, value: number) => void; + /** An event, which fires about every 500ms when you drag the input up and down, on release and on manual editing. */ + onDrag: (event: Event, value: number) => void; + /** Applies a `color` to the outer ring around the knob based on whether the value lands in the range between `from` and `to`. */ + ranges: Record; + /** Relative size of the knob. `1` is normal size, `2` is two times bigger. Fractional numbers are supported. */ + size: number; + /** Adjust value by this amount when dragging the input. */ + step: number; + /** Screen distance mouse needs to travel to adjust value by one `step`. */ + stepPixelSize: number; + /** A number in milliseconds, for which the input will hold off from updating while events propagate through the backend. Default is about 250ms, increase it if you still see flickering. */ + suppressFlicker: boolean; + /** Unit to display to the right of value. */ + unit: string; + /** Whether to clamp the value to the range. */ + unclamped: boolean; +}> & + BoxProps; + +/** + * ## Knob + * A radial control which allows dialing in precise values by dragging it + * up and down. + * + * Single click opens an input box to manually type in a number. + */ +export function Knob(props: Props) { const { // Draggable props (passthrough) animated, format, maxValue, minValue, - unclamped, onChange, onDrag, step, stepPixelSize, suppressFlicker, + unclamped, unit, value, // Own props + bipolar, + children, className, - style, - fillValue, color, + fillValue, ranges = {}, size = 1, - bipolar, - children, + style, ...rest } = props; + return ( { format, maxValue, minValue, - unclamped, onChange, onDrag, step, stepPixelSize, suppressFlicker, + unclamped, unit, value, - }}> + }} + > {(control) => { const { - dragging, - editing, - value, - displayValue, displayElement, - inputElement, + displayValue, + dragging, handleDragStart, + inputElement, + value, } = control; const scaledFillValue = scale( fillValue ?? displayValue, minValue, - maxValue + maxValue, ); const scaledDisplayValue = scale(displayValue, minValue, maxValue); const effectiveColor = color || keyOfMatchingRange(fillValue ?? value, ranges) || 'default'; const rotation = Math.min((scaledDisplayValue - 0.5) * 270, 225); + return (
{ ])} {...computeBoxProps({ style: { - 'font-size': size + 'em', + fontSize: size + 'em', ...style, }, ...rest, })} - onMouseDown={handleDragStart}> + onMouseDown={handleDragStart} + >
+ }} + >
@@ -108,20 +153,22 @@ export const Knob = (props) => { )} + viewBox="0 0 100 100" + > + viewBox="0 0 100 100" + > { }} ); -}; +} diff --git a/tgui/packages/tgui/components/LabeledControls.jsx b/tgui/packages/tgui/components/LabeledControls.tsx similarity index 73% rename from tgui/packages/tgui/components/LabeledControls.jsx rename to tgui/packages/tgui/components/LabeledControls.tsx index eea3b2c29bd3..7b352633e6be 100644 --- a/tgui/packages/tgui/components/LabeledControls.jsx +++ b/tgui/packages/tgui/components/LabeledControls.tsx @@ -4,24 +4,31 @@ * @license MIT */ -import { Flex } from './Flex'; +import { Flex, FlexProps } from './Flex'; -export const LabeledControls = (props) => { +export function LabeledControls(props: FlexProps) { const { children, wrap, ...rest } = props; + return ( + {...rest} + > {children} ); -}; +} + +type ItemProps = { + label: string; +} & FlexProps; -const LabeledControlsItem = (props) => { +function LabeledControlsItem(props: ItemProps) { const { label, children, mx = 1, ...rest } = props; + return ( { align="center" textAlign="center" justify="space-between" - {...rest}> + {...rest} + > {children} {label} ); -}; +} LabeledControls.Item = LabeledControlsItem; diff --git a/tgui/packages/tgui/components/Modal.jsx b/tgui/packages/tgui/components/Modal.tsx similarity index 69% rename from tgui/packages/tgui/components/Modal.jsx rename to tgui/packages/tgui/components/Modal.tsx index 9cf990c45bb9..30803d0dbf29 100644 --- a/tgui/packages/tgui/components/Modal.jsx +++ b/tgui/packages/tgui/components/Modal.tsx @@ -5,18 +5,21 @@ */ import { classes } from 'common/react'; -import { computeBoxClassName, computeBoxProps } from './Box'; + +import { BoxProps, computeBoxClassName, computeBoxProps } from './Box'; import { Dimmer } from './Dimmer'; -export const Modal = (props) => { +export function Modal(props: BoxProps) { const { className, children, ...rest } = props; + return (
+ {...computeBoxProps(rest)} + > {children}
); -}; +} diff --git a/tgui/packages/tgui/components/NoticeBox.jsx b/tgui/packages/tgui/components/NoticeBox.jsx deleted file mode 100644 index 1c3b49b16a53..000000000000 --- a/tgui/packages/tgui/components/NoticeBox.jsx +++ /dev/null @@ -1,27 +0,0 @@ -/** - * @file - * @copyright 2020 Aleksej Komarov - * @license MIT - */ - -import { classes, pureComponentHooks } from 'common/react'; -import { Box } from './Box'; - -export const NoticeBox = (props) => { - const { className, color, info, warning, success, danger, ...rest } = props; - return ( - - ); -}; - -NoticeBox.defaultHooks = pureComponentHooks; diff --git a/tgui/packages/tgui/components/NoticeBox.tsx b/tgui/packages/tgui/components/NoticeBox.tsx new file mode 100644 index 000000000000..db4f821d6b63 --- /dev/null +++ b/tgui/packages/tgui/components/NoticeBox.tsx @@ -0,0 +1,48 @@ +/** + * @file + * @copyright 2020 Aleksej Komarov + * @license MIT + */ + +import { classes } from 'common/react'; + +import { Box, BoxProps } from './Box'; + +type Props = ExclusiveProps & BoxProps; + +/** You MUST use only one or none */ +type NoticeType = 'info' | 'success' | 'danger'; + +type None = { + [K in NoticeType]?: undefined; +}; + +type ExclusiveProps = + | None + | (Omit & { + info: boolean; + }) + | (Omit & { + success: boolean; + }) + | (Omit & { + danger: boolean; + }); + +export function NoticeBox(props: Props) { + const { className, color, info, success, danger, ...rest } = props; + + return ( + + ); +} diff --git a/tgui/packages/tgui/components/Popper.tsx b/tgui/packages/tgui/components/Popper.tsx index 682fd65a740d..11c848e7eaed 100644 --- a/tgui/packages/tgui/components/Popper.tsx +++ b/tgui/packages/tgui/components/Popper.tsx @@ -1,12 +1,18 @@ import { Placement } from '@popperjs/core'; -import { PropsWithChildren, ReactNode, useEffect, useRef, useState } from 'react'; +import { + PropsWithChildren, + ReactNode, + useEffect, + useRef, + useState, +} from 'react'; import { usePopper } from 'react-popper'; type RequiredProps = { /** The content to display in the popper */ - readonly content: ReactNode; + content: ReactNode; /** Whether the popper is open */ - readonly isOpen: boolean; + isOpen: boolean; }; type OptionalProps = Partial<{ @@ -14,6 +20,10 @@ type OptionalProps = Partial<{ onClickOutside: () => void; /** Where to place the popper relative to the reference element */ placement: Placement; + /** Base z-index of the popper div + * @default 5 + */ + baseZIndex: number; }>; type Props = RequiredProps & OptionalProps; @@ -23,13 +33,13 @@ type Props = RequiredProps & OptionalProps; * Popper lets you position elements so that they don't go out of the bounds of the window. * @url https://popper.js.org/react-popper/ for more information. */ -export const Popper = (props: PropsWithChildren) => { +export function Popper(props: PropsWithChildren) { const { children, content, isOpen, onClickOutside, placement } = props; const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState( - null + null, ); // One would imagine we could just use useref here, but it's against react-popper documentation and causes a positioning bug @@ -42,14 +52,14 @@ export const Popper = (props: PropsWithChildren) => { }); /** Close the popper when the user clicks outside */ - const handleClickOutside = (event: MouseEvent) => { + function handleClickOutside(event: MouseEvent) { if ( !popperRef.current?.contains(event.target as Node) && !parentRef.current?.contains(event.target as Node) ) { onClickOutside?.(); } - }; + } useEffect(() => { if (isOpen) { @@ -69,7 +79,8 @@ export const Popper = (props: PropsWithChildren) => { ref={(node) => { setReferenceElement(node); parentRef.current = node; - }}> + }} + > {children}
{isOpen && ( @@ -78,11 +89,12 @@ export const Popper = (props: PropsWithChildren) => { setPopperElement(node); popperRef.current = node; }} - style={{ ...styles.popper, zIndex: 5 }} - {...attributes.popper}> + style={{ ...styles.popper, zIndex: props.baseZIndex ?? 5 }} + {...attributes.popper} + > {content}
)} ); -}; +} diff --git a/tgui/packages/tgui/components/ProgressBar.tsx b/tgui/packages/tgui/components/ProgressBar.tsx index 929ee232fd1a..ce2f071ba086 100644 --- a/tgui/packages/tgui/components/ProgressBar.tsx +++ b/tgui/packages/tgui/components/ProgressBar.tsx @@ -12,7 +12,7 @@ import { CSS_COLORS } from '../constants'; import { BoxProps, computeBoxClassName, computeBoxProps } from './Box'; type Props = { - readonly value: number; + value: number; } & Partial<{ backgroundColor: string; className: string; diff --git a/tgui/packages/tgui/components/RestrictedInput.jsx b/tgui/packages/tgui/components/RestrictedInput.jsx index f61025039f34..bbacc6c41032 100644 --- a/tgui/packages/tgui/components/RestrictedInput.jsx +++ b/tgui/packages/tgui/components/RestrictedInput.jsx @@ -8,6 +8,119 @@ import { Box } from './Box'; const DEFAULT_MIN = 0; const DEFAULT_MAX = 10000; +/** + * Sanitize a number without interfering with writing negative or floating point numbers. + * Handling dots and minuses in a user friendly way + * @param value {String} + * @param minValue {Number} + * @param maxValue {Number} + * @param allowFloats {Boolean} + * @returns {String} + */ +const softSanitizeNumber = (value, minValue, maxValue, allowFloats) => { + const minimum = minValue || DEFAULT_MIN; + const maximum = maxValue || maxValue === 0 ? maxValue : DEFAULT_MAX; + + let sanitizedString = allowFloats + ? value.replace(/[^\-\d.]/g, '') + : value.replace(/[^\-\d]/g, ''); + + if (allowFloats) { + sanitizedString = maybeLeadWithMin(sanitizedString, minimum); + sanitizedString = keepOnlyFirstOccurrence('.', sanitizedString); + } + if (minValue < 0) { + sanitizedString = maybeMoveMinusSign(sanitizedString); + sanitizedString = keepOnlyFirstOccurrence('-', sanitizedString); + } else { + sanitizedString = sanitizedString.replaceAll('-', ''); + } + if (minimum <= 1 && maximum >= 0) { + return clampGuessedNumber(sanitizedString, minimum, maximum, allowFloats); + } + return sanitizedString; +}; + +/** + * Clamping the input to the restricted range, making the Input smart for min <= 1 and max >= 0 + * @param softSanitizedNumber {String} + * @param allowFloats {Boolean} + * @returns {string} + */ +const clampGuessedNumber = ( + softSanitizedNumber, + minValue, + maxValue, + allowFloats, +) => { + let parsed = allowFloats + ? parseFloat(softSanitizedNumber) + : parseInt(softSanitizedNumber, 10); + if ( + !isNaN(parsed) && + (softSanitizedNumber.slice(-1) !== '.' || parsed < Math.floor(minValue)) + ) { + let clamped = clamp(parsed, minValue, maxValue); + if (parsed !== clamped) { + return String(clamped); + } + } + return softSanitizedNumber; +}; + +/** + * Translate x- to -x and -x- to x + * @param string {String} + * @returns {string} + */ +const maybeMoveMinusSign = (string) => { + let retString = string; + // if minus sign is present but not first + let minusIdx = string.indexOf('-'); + if (minusIdx > 0) { + string = string.replace('-', ''); + retString = '-'.concat(string); + } else if (minusIdx === 0) { + if (string.indexOf('-', minusIdx + 1) > 0) { + retString = string.replaceAll('-', ''); + } + } + return retString; +}; + +/** + * Translate . to min. or .x to mim.x or -. to -min. + * @param string {String} + */ +const maybeLeadWithMin = (string, min) => { + let retString = string; + let cuttedVal = Math.sign(min) * Math.floor(Math.abs(min)); + if (string.indexOf('.') === 0) { + retString = String(cuttedVal).concat(string); + } else if (string.indexOf('-') === 0 && string.indexOf('.') === 1) { + retString = cuttedVal + '.'.concat(string.slice(2)); + } + return retString; +}; + +/** + * Keep only the first occurrence of a string in another string. + * @param needle {String} + * @param haystack {String} + * @returns {string} + */ +const keepOnlyFirstOccurrence = (needle, haystack) => { + const idx = haystack.indexOf(needle); + const len = haystack.length; + let newHaystack = haystack; + if (idx !== -1 && idx < len - 1) { + let trailingString = haystack.slice(idx + 1, len); + trailingString = trailingString.replaceAll(needle, ''); + newHaystack = haystack.slice(0, idx + 1).concat(trailingString); + } + return newHaystack; +}; + /** * Takes a string input and parses integers or floats from it. * If none: Minimum is set. @@ -37,20 +150,29 @@ export class RestrictedInput extends Component { editing: false, }; this.handleBlur = (e) => { - const { maxValue, minValue, allowFloats } = this.props; + const { maxValue, minValue, onBlur, allowFloats } = this.props; const { editing } = this.state; if (editing) { - e.target.value = getClampedNumber( - e.target.value, - minValue, - maxValue, - allowFloats - ); this.setEditing(false); } + const safeNum = getClampedNumber( + e.target.value, + minValue, + maxValue, + allowFloats, + ); + if (onBlur) { + onBlur(e, +safeNum); + } }; this.handleChange = (e) => { - const { onChange } = this.props; + const { maxValue, minValue, onChange, allowFloats } = this.props; + e.target.value = softSanitizeNumber( + e.target.value, + minValue, + maxValue, + allowFloats, + ); if (onChange) { onChange(e, +e.target.value); } @@ -78,7 +200,7 @@ export class RestrictedInput extends Component { e.target.value, minValue, maxValue, - allowFloats + allowFloats, ); this.setEditing(false); if (onChange) { @@ -112,7 +234,7 @@ export class RestrictedInput extends Component { nextValue, minValue, maxValue, - allowFloats + allowFloats, ); } if (this.props.autoFocus || this.props.autoSelect) { @@ -138,7 +260,7 @@ export class RestrictedInput extends Component { nextValue, minValue, maxValue, - allowFloats + allowFloats, ); } } @@ -150,7 +272,7 @@ export class RestrictedInput extends Component { render() { const { props } = this; - const { onChange, onEnter, onInput, value, ...boxProps } = props; + const { onChange, onEnter, onInput, onBlur, value, ...boxProps } = props; const { className, fluid, monospace, ...rest } = boxProps; return ( + {...rest} + >
.
); diff --git a/tgui/packages/tgui/components/RoundGauge.jsx b/tgui/packages/tgui/components/RoundGauge.tsx similarity index 52% rename from tgui/packages/tgui/components/RoundGauge.jsx rename to tgui/packages/tgui/components/RoundGauge.tsx index fb016fa0e4c3..1deea8085b9b 100644 --- a/tgui/packages/tgui/components/RoundGauge.jsx +++ b/tgui/packages/tgui/components/RoundGauge.tsx @@ -6,32 +6,80 @@ import { clamp01, keyOfMatchingRange, scale } from 'common/math'; import { classes } from 'common/react'; + import { AnimatedNumber } from './AnimatedNumber'; -import { Box, computeBoxClassName, computeBoxProps } from './Box'; +import { Box, BoxProps, computeBoxClassName, computeBoxProps } from './Box'; -export const RoundGauge = (props) => { - // Support for IE8 is for losers sorry B) - if (Byond.IS_LTE_IE8) { - return ; - } +type Props = { + /** The current value of the metric. */ + value: number; +} & Partial<{ + /** When provided, will cause an alert symbol on the gauge to begin flashing in the color upon which the needle currently rests, as defined in `ranges`. */ + alertAfter: number; + /** As with alertAfter, but alerts below a value. If both are set, and alertAfter comes earlier, the alert will only flash when the needle is between both values. Otherwise, the alert will flash when on the active side of either threshold. */ + alertBefore: number; + /** CSS style. */ + className: string; + /** When provided, will be used to format the value of the metric for display. */ + format: (value: number) => string; + /** The upper bound of the gauge. */ + maxValue: number; + /** The lower bound of the gauge. */ + minValue: number; + /** Provide regions of the gauge to color between two specified values of the metric. */ + ranges: Record; + /** When provided scales the gauge. */ + size: number; + /** Custom css */ + style: React.CSSProperties; +}> & + BoxProps; +/** + * ## RoundGauge + * The RoundGauge component provides a visual representation of a single metric, as well as being capable of showing + * informational or cautionary boundaries related to that metric. + * + * @example + * ```tsx + * + * ``` + * + * The alert on the gauge is optional, and will only be shown if the `alertAfter` prop is defined. When defined, the alert + * will begin to flash the respective color upon which the needle currently rests, as defined in the `ranges` prop. + * + */ +export function RoundGauge(props: Props) { const { - value, - minValue = 1, - maxValue = 1, - ranges, alertAfter, alertBefore, + className, format, + maxValue = 1, + minValue = 1, + ranges, size = 1, - className, style, + value, ...rest } = props; const scaledValue = scale(value, minValue, maxValue); const clampedValue = clamp01(scaledValue); - const scaledRanges = ranges ? {} : { 'primary': [0, 1] }; + const scaledRanges = ranges ? {} : { primary: [0, 1] }; + if (ranges) { Object.keys(ranges).forEach((x) => { const range = ranges[x]; @@ -42,26 +90,30 @@ export const RoundGauge = (props) => { }); } - const shouldShowAlert = () => { - // If both after and before alert props are set, attempt to interpret both - // in a helpful way. - if (alertAfter && alertBefore && alertAfter < alertBefore) { - // If alertAfter is before alertBefore, only display an alert if - // we're between them. - if (alertAfter < value && alertBefore > value) { - return true; - } - } else if (alertAfter < value || alertBefore > value) { - // Otherwise, we have distint ranges, or only one or neither are set. - // Either way, being on the active side of either is sufficient. + function shouldShowAlert() { + // If both after and before alert props are set, and value is between them + if ( + alertAfter && + alertBefore && + value > alertAfter && + value < alertBefore + ) { return true; } + // If only alertAfter is set and value is greater than alertAfter + else if (alertAfter && value > alertAfter) { + return true; + } + // If only alertBefore is set and value is less than alertBefore + else if (alertBefore && value < alertBefore) { + return true; + } + // If none of the above conditions are met return false; - }; + } - // prettier-ignore - const alertColor = shouldShowAlert() - && keyOfMatchingRange(clampedValue, scaledRanges); + const alertColor = + shouldShowAlert() && keyOfMatchingRange(clampedValue, scaledRanges); return ( @@ -73,18 +125,20 @@ export const RoundGauge = (props) => { ])} {...computeBoxProps({ style: { - 'font-size': size + 'em', + fontSize: size + 'em', ...style, }, ...rest, - })}> + })} + > {(alertAfter || alertBefore) && ( + ])} + > )} @@ -99,9 +153,9 @@ export const RoundGauge = (props) => { className={`RoundGauge__ringFill RoundGauge--color--${x}`} key={i} style={{ - 'stroke-dashoffset': Math.max( + strokeDashoffset: Math.max( (2.0 - (col_ranges[1] - col_ranges[0])) * Math.PI * 50, - 0 + 0, ), }} transform={`rotate(${180 + 180 * col_ranges[0]} 50 50)`} @@ -114,7 +168,8 @@ export const RoundGauge = (props) => { + transform={`rotate(${clampedValue * 180 - 90} 50 50)`} + > {
- + ); -}; +} diff --git a/tgui/packages/tgui/components/Section.tsx b/tgui/packages/tgui/components/Section.tsx index 9b5170e3774e..7718299b92e6 100644 --- a/tgui/packages/tgui/components/Section.tsx +++ b/tgui/packages/tgui/components/Section.tsx @@ -4,79 +4,122 @@ * @license MIT */ -import { BoxProps, computeBoxClassName, computeBoxProps } from './Box'; -import { ReactNode, RefObject, createRef, useEffect } from 'react'; -import { addScrollableNode, removeScrollableNode } from '../events'; import { canRender, classes } from 'common/react'; +import { forwardRef, ReactNode, RefObject, useEffect } from 'react'; + +import { addScrollableNode, removeScrollableNode } from '../events'; +import { BoxProps, computeBoxClassName, computeBoxProps } from './Box'; -export type SectionProps = Partial<{ +type Props = Partial<{ + /** Buttons to render aside the section title. */ buttons: ReactNode; + /** If true, fills all available vertical space. */ fill: boolean; + /** If true, removes all section padding. */ fitted: boolean; + /** Shows or hides the scrollbar. */ scrollable: boolean; + /** Shows or hides the horizontal scrollbar. */ scrollableHorizontal: boolean; + /** Title of the section. */ title: ReactNode; - /** @member Allows external control of scrolling. */ - scrollableRef: RefObject; + /** id to assosiate with the parent div element used by this section, for uses with procs like getElementByID */ + container_id: string; /** @member Callback function for the `scroll` event */ onScroll: ((this: GlobalEventHandlers, ev: Event) => any) | null; }> & BoxProps; -export const Section = (props: SectionProps) => { - const { - className, - title, - buttons, - fill, - fitted, - scrollable, - scrollableHorizontal, - children, - onScroll, - ...rest - } = props; +/** + * ## Section + * Section is a surface that displays content and actions on a single topic. + * + * They should be easy to scan for relevant and actionable information. + * Elements, like text and images, should be placed in them in a way that + * clearly indicates hierarchy. + * + * 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. + * + * Section can also be titled to clearly define its purpose. + * + * ```tsx + *
Here you can order supply crates.
+ * ``` + * + * If you want to have a button on the right side of an section title + * (for example, to perform some sort of action), there is a way to do that: + * + * ```tsx + *
Send shuttle}> + * Here you can order supply crates. + *
+ * ``` + */ +export const Section = forwardRef( + (props: Props, forwardedRef: RefObject) => { + const { + buttons, + children, + className, + fill, + fitted, + onScroll, + scrollable, + scrollableHorizontal, + title, + container_id, + ...rest + } = props; - const scrollableRef = props.scrollableRef || createRef(); - const hasTitle = canRender(title) || canRender(buttons); + const hasTitle = canRender(title) || canRender(buttons); - useEffect(() => { - if (scrollable || scrollableHorizontal) { - addScrollableNode(scrollableRef.current as HTMLElement); - if (onScroll && scrollableRef.current) { - scrollableRef.current.onscroll = onScroll; - } - } - return () => { - if (scrollable || scrollableHorizontal) { - removeScrollableNode(scrollableRef.current as HTMLElement); - } - }; - }, []); + /** We want to be able to scroll on hover, but using focus will steal it from inputs */ + useEffect(() => { + if (!forwardedRef?.current) return; + if (!scrollable && !scrollableHorizontal) return; - return ( -
- {hasTitle && ( -
- {title} -
{buttons}
-
- )} -
-
- {children} + addScrollableNode(forwardedRef.current); + + return () => { + if (!forwardedRef?.current) return; + removeScrollableNode(forwardedRef.current!); + }; + }, []); + + return ( +
+ {hasTitle && ( +
+ {title} +
{buttons}
+
+ )} +
+
+ {children} +
-
- ); -}; + ); + }, +); diff --git a/tgui/packages/tgui/components/Slider.jsx b/tgui/packages/tgui/components/Slider.tsx similarity index 53% rename from tgui/packages/tgui/components/Slider.jsx rename to tgui/packages/tgui/components/Slider.tsx index 84ee8421a9a6..255e747bd60a 100644 --- a/tgui/packages/tgui/components/Slider.jsx +++ b/tgui/packages/tgui/components/Slider.tsx @@ -6,15 +6,56 @@ import { clamp01, keyOfMatchingRange, scale } from 'common/math'; import { classes } from 'common/react'; -import { computeBoxClassName, computeBoxProps } from './Box'; +import { PropsWithChildren } from 'react'; + +import { BoxProps, computeBoxClassName, computeBoxProps } from './Box'; import { DraggableControl } from './DraggableControl'; -import { NumberInput } from './NumberInput'; -export const Slider = (props) => { - // IE8: I don't want to support a yet another component on IE8. - if (Byond.IS_LTE_IE8) { - return ; - } +type Props = { + /** Highest possible value. */ + maxValue: number; + /** Lowest possible value. */ + minValue: number; + /** Value itself, controls the position of the cursor. */ + value: number; +} & Partial<{ + /** Animates the value if it was changed externally. */ + animated: boolean; + /** Custom css */ + className: string; + /** Color of the slider. */ + color: string; + /** If set, this value will be used to set the fill percentage of the progress bar filler independently of the main value. */ + fillValue: number; + /** Format value using this function before displaying it. */ + format: (value: number) => string; + /** Adjust value by this amount when dragging the input. */ + onChange: (event: Event, value: number) => void; + /** An event, which fires when you release the input, or successfully enter a number. */ + onDrag: (event: Event, value: number) => void; + /** Applies a `color` to the slider based on whether the value lands in the range between `from` and `to`. */ + ranges: Record; + /** Screen distance mouse needs to travel to adjust value by one `step`. */ + step: number; + /** A number in milliseconds, for which the input will hold off from updating while events propagate through the backend. Default is about 250ms, increase it if you still see flickering. */ + stepPixelSize: number; + /** Adjust value by this amount when dragging the input. */ + suppressFlicker: boolean; + /** Unit to display to the right of value. */ + unit: string; +}> & + BoxProps & + PropsWithChildren; + +/** + * ## Slider + * A horizontal, progressbar-like control which allows dialing + * in precise values by dragging it left and right. + * + * Single click opens an input box to manually type in a number. + * + */ +export function Slider(props: Props) { const { // Draggable props (passthrough) animated, @@ -36,7 +77,9 @@ export const Slider = (props) => { children, ...rest } = props; + const hasContent = children !== undefined; + return ( { suppressFlicker, unit, value, - }}> + }} + > {(control) => { const { - dragging, - editing, - value, - displayValue, displayElement, - inputElement, + displayValue, + dragging, handleDragStart, + inputElement, + value, } = control; + const hasFillValue = fillValue !== undefined && fillValue !== null; - const scaledValue = scale(value, minValue, maxValue); + const scaledFillValue = scale( fillValue ?? displayValue, minValue, - maxValue + maxValue, ); const scaledDisplayValue = scale(displayValue, minValue, maxValue); - // prettier-ignore - const effectiveColor = color - || keyOfMatchingRange(fillValue ?? value, ranges) || 'default'; + + const effectiveColor = + color || keyOfMatchingRange(fillValue ?? value, ranges) || 'default'; + return (
{ computeBoxClassName(rest), ])} {...computeBoxProps(rest)} - onMouseDown={handleDragStart}> + onMouseDown={handleDragStart} + >
{
+ }} + >
{dragging && ( @@ -123,4 +170,4 @@ export const Slider = (props) => { }} ); -}; +} diff --git a/tgui/packages/tgui/components/Stack.tsx b/tgui/packages/tgui/components/Stack.tsx index 3eb31bd32560..3f5cf72123cc 100644 --- a/tgui/packages/tgui/components/Stack.tsx +++ b/tgui/packages/tgui/components/Stack.tsx @@ -6,17 +6,34 @@ import { classes } from 'common/react'; import { RefObject } from 'react'; -import { computeFlexClassName, computeFlexItemClassName, computeFlexItemProps, computeFlexProps, FlexItemProps, FlexProps } from './Flex'; + +import { + computeFlexClassName, + computeFlexItemClassName, + computeFlexItemProps, + computeFlexProps, + FlexItemProps, + FlexProps, +} from './Flex'; type Props = Partial<{ - vertical: boolean; + /** Fills available space. */ fill: boolean; + /** Reverses the stack. */ + reverse: boolean; + /** Flex column */ + vertical: boolean; + /** Adds zebra striping to the stack. */ zebra: boolean; }> & FlexProps; -export const Stack = (props: Props) => { - const { className, vertical, fill, zebra, ...rest } = props; +export function Stack(props: Props) { + const { className, vertical, fill, reverse, zebra, ...rest } = props; + + const directionPrefix = vertical ? 'column' : 'row'; + const directionSuffix = reverse ? '-reverse' : ''; + return (
{ fill && 'Stack--fill', vertical ? 'Stack--vertical' : 'Stack--horizontal', zebra && 'Stack--zebra', + reverse && `Stack--reverse${vertical ? '--vertical' : ''}`, className, computeFlexClassName(props), ])} {...computeFlexProps({ - direction: vertical ? 'column' : 'row', + direction: `${directionPrefix}${directionSuffix}`, ...rest, })} /> ); -}; +} type StackItemProps = FlexItemProps & Partial<{ innerRef: RefObject; }>; -const StackItem = (props: StackItemProps) => { +function StackItem(props: StackItemProps) { const { className, innerRef, ...rest } = props; + return (
{ {...computeFlexItemProps(rest)} /> ); -}; +} Stack.Item = StackItem; @@ -62,8 +81,9 @@ type StackDividerProps = FlexItemProps & hidden: boolean; }>; -const StackDivider = (props: StackDividerProps) => { +function StackDivider(props: StackDividerProps) { const { className, hidden, ...rest } = props; + return (
{ {...computeFlexItemProps(rest)} /> ); -}; +} Stack.Divider = StackDivider; diff --git a/tgui/packages/tgui/components/Table.jsx b/tgui/packages/tgui/components/Table.jsx deleted file mode 100644 index e936b9144808..000000000000 --- a/tgui/packages/tgui/components/Table.jsx +++ /dev/null @@ -1,64 +0,0 @@ -/** - * @file - * @copyright 2020 Aleksej Komarov - * @license MIT - */ - -import { classes, pureComponentHooks } from 'common/react'; -import { computeBoxClassName, computeBoxProps } from './Box'; - -export const Table = (props) => { - const { className, collapsing, children, ...rest } = props; - return ( - - {children} -
- ); -}; - -Table.defaultHooks = pureComponentHooks; - -export const TableRow = (props) => { - const { className, header, ...rest } = props; - return ( - - ); -}; - -TableRow.defaultHooks = pureComponentHooks; - -export const TableCell = (props) => { - const { className, collapsing, header, ...rest } = props; - return ( - - ); -}; - -TableCell.defaultHooks = pureComponentHooks; - -Table.Row = TableRow; -Table.Cell = TableCell; diff --git a/tgui/packages/tgui/components/Table.tsx b/tgui/packages/tgui/components/Table.tsx new file mode 100644 index 000000000000..f4dcf6f3f522 --- /dev/null +++ b/tgui/packages/tgui/components/Table.tsx @@ -0,0 +1,90 @@ +/** + * @file + * @copyright 2020 Aleksej Komarov + * @license MIT + */ + +import { classes } from 'common/react'; + +import { BoxProps, computeBoxClassName, computeBoxProps } from './Box'; + +type Props = Partial<{ + /** Collapses table to the smallest possible size. */ + collapsing: boolean; +}> & + BoxProps; + +export function Table(props: Props) { + const { className, collapsing, children, ...rest } = props; + + return ( + + {children} +
+ ); +} + +type RowProps = Partial<{ + /** Whether this is a header cell. */ + header: boolean; +}> & + BoxProps; + +export function TableRow(props: RowProps) { + const { className, header, ...rest } = props; + + return ( + + ); +} + +Table.Row = TableRow; + +type CellProps = Partial<{ + /** Collapses table cell to the smallest possible size, + and stops any text inside from wrapping. */ + collapsing: boolean; + /** Additional columns for this cell to expand, assuming there is room. */ + colSpan: number; + /** Whether this is a header cell. */ + header: boolean; + /** Rows for this cell to expand, assuming there is room. */ + rowSpan: number; +}> & + BoxProps; + +export function TableCell(props: CellProps) { + const { className, collapsing, colSpan, header, ...rest } = props; + + return ( + + ); +} + +Table.Cell = TableCell; diff --git a/tgui/packages/tgui/components/Tabs.jsx b/tgui/packages/tgui/components/Tabs.tsx similarity index 63% rename from tgui/packages/tgui/components/Tabs.jsx rename to tgui/packages/tgui/components/Tabs.tsx index 46d1b078a85e..84779bcf7482 100644 --- a/tgui/packages/tgui/components/Tabs.jsx +++ b/tgui/packages/tgui/components/Tabs.tsx @@ -5,11 +5,35 @@ */ import { canRender, classes } from 'common/react'; -import { computeBoxClassName, computeBoxProps } from './Box'; +import { PropsWithChildren, ReactNode } from 'react'; + +import { BoxProps, computeBoxClassName, computeBoxProps } from './Box'; import { Icon } from './Icon'; -export const Tabs = (props) => { +type Props = Partial<{ + className: string; + fill: boolean; + fluid: boolean; + vertical: boolean; +}> & + BoxProps & + PropsWithChildren; + +type TabProps = Partial<{ + className: string; + color: string; + icon: string; + leftSlot: ReactNode; + onClick: (e?) => void; + rightSlot: ReactNode; + selected: boolean; +}> & + BoxProps & + PropsWithChildren; + +export const Tabs = (props: Props) => { const { className, vertical, fill, fluid, children, ...rest } = props; + return (
{ className, computeBoxClassName(rest), ])} - {...computeBoxProps(rest)}> + {...computeBoxProps(rest)} + > {children}
); }; -const Tab = (props) => { +const Tab = (props: TabProps) => { const { className, selected, @@ -37,6 +62,7 @@ const Tab = (props) => { children, ...rest } = props; + return (
{ 'Tab--color--' + color, selected && 'Tab--selected', className, - ...computeBoxClassName(rest), + computeBoxClassName(rest), ])} - {...computeBoxProps(rest)}> + {...computeBoxProps(rest)} + > {(canRender(leftSlot) &&
{leftSlot}
) || (!!icon && (
diff --git a/tgui/packages/tgui/components/TimeDisplay.jsx b/tgui/packages/tgui/components/TimeDisplay.jsx index bbdd747701cc..390dbc93af93 100644 --- a/tgui/packages/tgui/components/TimeDisplay.jsx +++ b/tgui/packages/tgui/components/TimeDisplay.jsx @@ -1,6 +1,7 @@ -import { formatTime } from '../format'; import { Component } from 'react'; +import { formatTime } from '../format'; + // AnimatedNumber Copypaste const isSafeNumber = (value) => { return ( diff --git a/tgui/packages/tgui/components/index.jsx b/tgui/packages/tgui/components/index.jsx index 121d2bfa4e21..9dcc9213b320 100644 --- a/tgui/packages/tgui/components/index.jsx +++ b/tgui/packages/tgui/components/index.jsx @@ -22,6 +22,7 @@ export { Flex } from './Flex'; export { FitText } from './FitText'; export { Grid } from './Grid'; export { Icon } from './Icon'; +export { Image } from './Image'; export { InfinitePlane } from './InfinitePlane'; export { Input } from './Input'; export { KeyListener } from './KeyListener'; diff --git a/tgui/packages/tgui/debug/KitchenSink.jsx b/tgui/packages/tgui/debug/KitchenSink.jsx index 23cf96698101..19044ca98d0c 100644 --- a/tgui/packages/tgui/debug/KitchenSink.jsx +++ b/tgui/packages/tgui/debug/KitchenSink.jsx @@ -4,7 +4,8 @@ * @license MIT */ -import { useLocalState } from '../backend'; +import { useState } from 'react'; + import { Flex, Section, Tabs } from '../components'; import { Pane, Window } from '../layouts'; @@ -22,8 +23,8 @@ const getStories = () => r.keys().map((path) => r(path)); export const KitchenSink = (props) => { const { panel } = props; - const [theme] = useLocalState('kitchenSinkTheme'); - const [pageIndex, setPageIndex] = useLocalState('pageIndex', 0); + const [theme] = useState(null); + const [pageIndex, setPageIndex] = useState(0); const stories = getStories(); const story = stories[pageIndex]; const Layout = panel ? Pane : Window; @@ -38,7 +39,8 @@ export const KitchenSink = (props) => { key={i} color="transparent" selected={i === pageIndex} - onClick={() => setPageIndex(i)}> + onClick={() => setPageIndex(i)} + > {story.meta.title} ))} diff --git a/tgui/packages/tgui/debug/index.js b/tgui/packages/tgui/debug/index.ts similarity index 100% rename from tgui/packages/tgui/debug/index.js rename to tgui/packages/tgui/debug/index.ts diff --git a/tgui/packages/tgui/debug/middleware.js b/tgui/packages/tgui/debug/middleware.js index 75687f25417d..3f7414335e8d 100644 --- a/tgui/packages/tgui/debug/middleware.js +++ b/tgui/packages/tgui/debug/middleware.js @@ -5,9 +5,14 @@ */ import { KEY_BACKSPACE, KEY_F10, KEY_F11, KEY_F12 } from 'common/keycodes'; + import { globalEvents } from '../events'; import { acquireHotKey } from '../hotkeys'; -import { openExternalBrowser, toggleDebugLayout, toggleKitchenSink } from './actions'; +import { + openExternalBrowser, + toggleDebugLayout, + toggleKitchenSink, +} from './actions'; // prettier-ignore const relayedTypes = [ diff --git a/tgui/packages/tgui/events.ts b/tgui/packages/tgui/events.ts index 00b0fd9426ab..cc53d31bfabe 100644 --- a/tgui/packages/tgui/events.ts +++ b/tgui/packages/tgui/events.ts @@ -13,7 +13,7 @@ export const globalEvents = new EventEmitter(); let ignoreWindowFocus = false; export const setupGlobalEvents = ( - options: { ignoreWindowFocus?: boolean } = {} + options: { ignoreWindowFocus?: boolean } = {}, ): void => { ignoreWindowFocus = !!options.ignoreWindowFocus; }; @@ -21,7 +21,7 @@ export const setupGlobalEvents = ( // Window focus // -------------------------------------------------------- -let windowFocusTimeout; +let windowFocusTimeout: ReturnType | null; let windowFocused = true; // Pretend to always be in focus. @@ -50,7 +50,7 @@ const setWindowFocus = (value: boolean, delayed?: boolean) => { let focusStolenBy: HTMLElement | null = null; -export const canStealFocus = (node) => { +export const canStealFocus = (node: HTMLElement) => { const tag = String(node.tagName).toLowerCase(); return tag === 'input' || tag === 'textarea'; }; @@ -86,7 +86,7 @@ export const removeScrollableNode = (node: HTMLElement) => { } }; -const focusNearestTrackedParent = (node) => { +const focusNearestTrackedParent = (node: HTMLElement | null) => { if (focusStolenBy || !windowFocused) { return; } @@ -101,7 +101,7 @@ const focusNearestTrackedParent = (node) => { node.focus(); return; } - node = node.parentNode; + node = node.parentElement; } }; @@ -122,7 +122,6 @@ window.addEventListener('focusin', (e) => { setWindowFocus(true); if (canStealFocus(e.target as HTMLElement)) { stealFocus(e.target as HTMLElement); - return; } }); @@ -143,7 +142,7 @@ window.addEventListener('beforeunload', (e) => { // Key events // -------------------------------------------------------- -const keyHeldByCode = {}; +const keyHeldByCode: Record = {}; export class KeyEvent { event: KeyboardEvent; @@ -210,10 +209,10 @@ export class KeyEvent { // IE8: Keydown event is only available on document. document.addEventListener('keydown', (e) => { - if (canStealFocus(e.target)) { + if (canStealFocus(e.target as HTMLElement)) { return; } - const code = e.code; + const code = e.keyCode; const key = new KeyEvent(e, 'keydown', keyHeldByCode[code]); globalEvents.emit('keydown', key); globalEvents.emit('key', key); @@ -221,10 +220,10 @@ document.addEventListener('keydown', (e) => { }); document.addEventListener('keyup', (e) => { - if (canStealFocus(e.target)) { + if (canStealFocus(e.target as HTMLElement)) { return; } - const code = e.code; + const code = e.keyCode; const key = new KeyEvent(e, 'keyup'); globalEvents.emit('keyup', key); globalEvents.emit('key', key); diff --git a/tgui/packages/tgui/interfaces/AresAdmin.js b/tgui/packages/tgui/interfaces/AresAdmin.jsx similarity index 100% rename from tgui/packages/tgui/interfaces/AresAdmin.js rename to tgui/packages/tgui/interfaces/AresAdmin.jsx diff --git a/tgui/packages/tgui/interfaces/IcMedalsPanel.js b/tgui/packages/tgui/interfaces/IcMedalsPanel.jsx similarity index 100% rename from tgui/packages/tgui/interfaces/IcMedalsPanel.js rename to tgui/packages/tgui/interfaces/IcMedalsPanel.jsx diff --git a/tgui/packages/tgui/interfaces/TextInputModal.tsx b/tgui/packages/tgui/interfaces/TextInputModal.tsx index 45dc5c47d14a..b2b993e92bcc 100644 --- a/tgui/packages/tgui/interfaces/TextInputModal.tsx +++ b/tgui/packages/tgui/interfaces/TextInputModal.tsx @@ -1,9 +1,11 @@ -import { Loader } from './common/Loader'; -import { InputButtons } from './common/InputButtons'; -import { useBackend, useLocalState } from '../backend'; -import { KEY_ENTER, KEY_ESCAPE } from '../../common/keycodes'; +import { KEY } from 'common/keys'; +import { KeyboardEvent, useState } from 'react'; + +import { useBackend } from '../backend'; import { Box, Section, Stack, TextArea } from '../components'; import { Window } from '../layouts'; +import { InputButtons } from './common/InputButtons'; +import { Loader } from './common/Loader'; type TextInputData = { large_buttons: boolean; @@ -30,11 +32,12 @@ export const TextInputModal = (props) => { max_length, message = '', multiline, - placeholder, + placeholder = '', timeout, title, } = data; - const [input, setInput] = useLocalState('input', placeholder || ''); + + const [input, setInput] = useState(placeholder || ''); const onType = (value: string) => { if (value === input) { return; @@ -58,11 +61,13 @@ export const TextInputModal = (props) => { {timeout && } { - const keyCode = window.event ? event.which : event.keyCode; - if (keyCode === KEY_ENTER && (!visualMultiline || !event.shiftKey)) { + if ( + event.key === KEY.Enter && + (!visualMultiline || !event.shiftKey) + ) { act('submit', { entry: input }); } - if (keyCode === KEY_ESCAPE) { + if (event.key === KEY.Escape) { act('cancel'); } }}> @@ -72,7 +77,7 @@ export const TextInputModal = (props) => { {message} - + { }; /** Gets the user input and invalidates if there's a constraint. */ -const InputArea = (props) => { +const InputArea = (props: { + readonly input: string; + readonly onType: (value: string) => void; +}) => { const { act, data } = useBackend(); const { max_length, multiline } = data; const { input, onType } = props; @@ -102,13 +110,14 @@ const InputArea = (props) => { height={multiline || input.length >= 30 ? '100%' : '1.8rem'} maxLength={max_length} onEscape={() => act('cancel')} - onEnter={(event) => { + onEnter={(event: KeyboardEvent) => { if (visualMultiline && event.shiftKey) { return; } event.preventDefault(); act('submit', { entry: input }); }} + onChange={(_, value) => onType(value)} onInput={(_, value) => onType(value)} placeholder="Type something..." value={input} diff --git a/tgui/packages/tgui/interfaces/common/LoadingToolbox.tsx b/tgui/packages/tgui/interfaces/common/LoadingToolbox.tsx new file mode 100644 index 000000000000..fcd307110901 --- /dev/null +++ b/tgui/packages/tgui/interfaces/common/LoadingToolbox.tsx @@ -0,0 +1,32 @@ +import { Dimmer, Icon, Stack } from '../../components'; + +/** Spinner that represents loading states. + * + * @usage + * ```tsx + * /// rest of the component + * return ( + * ///... content to overlay + * {!!loading && } + * /// ... content to overlay + * ); + * ``` + * OR + * ```tsx + * return ( + * {loading ? : } + * ) + * ``` + */ +export const LoadingScreen = (props) => { + return ( + + + + + + Please wait... + + + ); +}; diff --git a/tgui/packages/tgui/layouts/Layout.jsx b/tgui/packages/tgui/layouts/Layout.tsx similarity index 50% rename from tgui/packages/tgui/layouts/Layout.jsx rename to tgui/packages/tgui/layouts/Layout.tsx index 37b01f0663ad..173ed1cbb432 100644 --- a/tgui/packages/tgui/layouts/Layout.jsx +++ b/tgui/packages/tgui/layouts/Layout.tsx @@ -5,24 +5,57 @@ */ import { classes } from 'common/react'; -import { computeBoxClassName, computeBoxProps } from '../components/Box'; +import { useEffect, useRef } from 'react'; + +import { + BoxProps, + computeBoxClassName, + computeBoxProps, +} from '../components/Box'; import { addScrollableNode, removeScrollableNode } from '../events'; -export const Layout = (props) => { +type Props = Partial<{ + theme: string; +}> & + BoxProps; + +export function Layout(props: Props) { const { className, theme = 'nanotrasen', children, ...rest } = props; + return (
+ {...computeBoxProps(rest)} + > {children}
); -}; +} + +type ContentProps = Partial<{ + scrollable: boolean; +}> & + BoxProps; -const LayoutContent = (props) => { +function LayoutContent(props: ContentProps) { const { className, scrollable, children, ...rest } = props; + const node = useRef(null); + + useEffect(() => { + const self = node.current; + + if (self && scrollable) { + addScrollableNode(self); + } + return () => { + if (self && scrollable) { + removeScrollableNode(self); + } + }; + }, []); + return (
{ className, computeBoxClassName(rest), ])} - {...computeBoxProps(rest)}> + ref={node} + {...computeBoxProps(rest)} + > {children}
); -}; - -LayoutContent.defaultHooks = { - onComponentDidMount: (node) => addScrollableNode(node), - onComponentWillUnmount: (node) => removeScrollableNode(node), -}; +} Layout.Content = LayoutContent; diff --git a/tgui/packages/tgui/layouts/NtosWindow.jsx b/tgui/packages/tgui/layouts/NtosWindow.tsx similarity index 61% rename from tgui/packages/tgui/layouts/NtosWindow.jsx rename to tgui/packages/tgui/layouts/NtosWindow.tsx index 01c2f6ac18e8..971e82fd40ea 100644 --- a/tgui/packages/tgui/layouts/NtosWindow.jsx +++ b/tgui/packages/tgui/layouts/NtosWindow.tsx @@ -4,36 +4,88 @@ * @license MIT */ +import { BooleanLike } from 'common/react'; + import { resolveAsset } from '../assets'; import { useBackend } from '../backend'; import { Box, Button } from '../components'; import { Window } from './Window'; +export type NTOSData = { + authenticatedUser: string | null; + authIDName: string; + comp_light_color: string; + has_id: BooleanLike; + has_light: BooleanLike; + id_name: string; + light_on: BooleanLike; + login: Login; + pai: string | null; + PC_batteryicon: string | null; + PC_batterypercent: string | null; + PC_device_theme: string; + PC_lowpower_mode: BooleanLike; + PC_ntneticon: string; + PC_programheaders: Program[]; + PC_showexitprogram: BooleanLike; + PC_stationdate: string; + PC_stationtime: string; + programs: Program[]; + proposed_login: Login; + removable_media: string[]; + show_imprint: BooleanLike; +}; + +type Program = { + alert: BooleanLike; + desc: string; + header_program: BooleanLike; + icon: string; + name: string; + running: BooleanLike; +}; + +type Login = { + IDInserted?: BooleanLike; + IDJob: string | null; + IDName: string | null; +}; + export const NtosWindow = (props) => { - const { title, width = 575, height = 700, theme = 'ntos', children } = props; - const { act, data } = useBackend(); + const { title, width = 575, height = 700, children } = props; + const { act, data } = useBackend(); const { PC_device_theme, PC_batteryicon, - PC_showbatteryicon, PC_batterypercent, PC_ntneticon, - PC_apclinkicon, + PC_stationdate, PC_stationtime, PC_programheaders = [], PC_showexitprogram, + PC_lowpower_mode, } = data; + return ( - +
+
@@ -53,28 +105,17 @@ export const NtosWindow = (props) => { /> )} - {!!(PC_showbatteryicon && PC_batteryicon) && ( + {!!PC_batteryicon && ( - {PC_batterypercent && PC_batterypercent} - - )} - {PC_apclinkicon && ( - - + {PC_batterypercent} )} {!!PC_showexitprogram && ( - }> + } + > setCode(e.target.value)}> + onChange={(e) => setCode(e.target.value)} + > {code}
diff --git a/tgui/packages/tgui/stories/Flex.stories.jsx b/tgui/packages/tgui/stories/Flex.stories.jsx index 03e67f5a2300..ede97cdb7579 100644 --- a/tgui/packages/tgui/stories/Flex.stories.jsx +++ b/tgui/packages/tgui/stories/Flex.stories.jsx @@ -4,7 +4,8 @@ * @license MIT */ -import { useLocalState } from '../backend'; +import { useState } from 'react'; + import { Button, Flex, Section } from '../components'; export const meta = { @@ -13,10 +14,10 @@ export const meta = { }; const Story = (props) => { - const [grow, setGrow] = useLocalState('fs_grow', 1); - const [direction, setDirection] = useLocalState('fs_direction', 'column'); - const [fill, setFill] = useLocalState('fs_fill', true); - const [hasTitle, setHasTitle] = useLocalState('fs_title', true); + const [grow, setGrow] = useState(1); + const [direction, setDirection] = useState('column'); + const [fill, setFill] = useState(true); + const [hasTitle, setHasTitle] = useState(true); return ( @@ -25,7 +26,8 @@ const Story = (props) => { fluid onClick={() => setDirection(direction === 'column' ? 'row' : 'column') - }> + } + > {`Flex direction="${direction}"`}
@@ -47,7 +50,8 @@ const Story = (props) => { + grow={grow} + >
Content
diff --git a/tgui/packages/tgui/stories/LabeledList.stories.jsx b/tgui/packages/tgui/stories/LabeledList.stories.jsx index 5c22a128dc1a..85a852a1459d 100644 --- a/tgui/packages/tgui/stories/LabeledList.stories.jsx +++ b/tgui/packages/tgui/stories/LabeledList.stories.jsx @@ -34,13 +34,15 @@ const Story = (props) => { + label="Very very very very very very very very very very very very very long label with labelWrap" + > Entry 7 + label="Very very very very very very very very very very very very very long label with labelWrap and verticalAlign" + > Entry 8 diff --git a/tgui/packages/tgui/stories/Popper.stories.jsx b/tgui/packages/tgui/stories/Popper.stories.tsx similarity index 81% rename from tgui/packages/tgui/stories/Popper.stories.jsx rename to tgui/packages/tgui/stories/Popper.stories.tsx index 08fd430fb275..cb7fd6634d71 100644 --- a/tgui/packages/tgui/stories/Popper.stories.jsx +++ b/tgui/packages/tgui/stories/Popper.stories.tsx @@ -9,18 +9,19 @@ const Story = () => { return ( <> + }} + > Loogatme! } - options={{ - placement: 'bottom', - }}> + placement="bottom" + > { + }} + > I am on the right! } - options={{ - placement: 'right', - }}> + placement="right" + > { const color_data = color ? { color: color } : { - ranges: { - good: [0.5, Infinity], - bad: [-Infinity, 0.1], - average: [0, 0.5], - }, - }; + ranges: { + good: [0.5, Infinity], + bad: [-Infinity, 0.1], + average: [0, 0.5], + }, + }; return (
diff --git a/tgui/packages/tgui/stories/Tabs.stories.jsx b/tgui/packages/tgui/stories/Tabs.stories.jsx index 2d72ab7598bc..d9bc44341f71 100644 --- a/tgui/packages/tgui/stories/Tabs.stories.jsx +++ b/tgui/packages/tgui/stories/Tabs.stories.jsx @@ -4,7 +4,8 @@ * @license MIT */ -import { useLocalState } from '../backend'; +import { useState } from 'react'; + import { Button, Section, Tabs } from '../components'; export const meta = { @@ -15,7 +16,7 @@ export const meta = { const TAB_RANGE = ['Tab #1', 'Tab #2', 'Tab #3', 'Tab #4']; const Story = (props) => { - const [tabProps, setTabProps] = useLocalState('tabProps', {}); + const [tabProps, setTabProps] = useState({}); return ( <>
@@ -102,13 +103,14 @@ const Story = (props) => { }; const TabsPrefab = (props) => { - const [tabIndex, setTabIndex] = useLocalState('tabIndex', 0); - const [tabProps] = useLocalState('tabProps', {}); + const [tabIndex, setTabIndex] = useState(0); + const [tabProps] = useState({}); return ( + textAlign={tabProps.centered && 'center'} + > {TAB_RANGE.map((text, i) => ( { ); @@ -315,7 +318,8 @@ const ButtonInput = (props: InputProps) => { 'Button--color--' + color, ])} {...rest} - onClick={() => setInInput(true)}> + onClick={() => setInInput(true)} + > {icon && }
{toDisplay}
{ ...(typeof this.props.native?.style === 'object' ? this.props.native.style : {}), - }}> + }} + > {this.props.children} ); diff --git a/tgui/packages/tgui/components/Flex.tsx.bak b/tgui/packages/tgui/components/Flex.tsx.bak deleted file mode 100644 index da063f0296f5..000000000000 --- a/tgui/packages/tgui/components/Flex.tsx.bak +++ /dev/null @@ -1,110 +0,0 @@ -/** - * @file - * @copyright 2020 Aleksej Komarov - * @license MIT - */ - -import { classes } from 'common/react'; -import { BoxProps, computeBoxClassName, computeBoxProps, unit } from './Box'; - -export type FlexProps = Partial<{ - align: string | boolean; - direction: string; - inline: boolean; - justify: string; - scrollable: boolean; - style: Partial; - wrap: string | boolean; -}> & - BoxProps; - -export const computeFlexClassName = (props: FlexProps) => { - return classes([ - 'Flex', - props.inline && 'Flex--inline', - Byond.IS_LTE_IE10 && 'Flex--iefix', - Byond.IS_LTE_IE10 && props.direction === 'column' && 'Flex--iefix--column', - computeBoxClassName(props), - ]); -}; - -export const computeFlexProps = (props: FlexProps) => { - const { className, direction, wrap, align, justify, inline, ...rest } = props; - - return computeBoxProps({ - style: { - ...rest.style, - flexDirection: direction, - flexWrap: wrap === true ? 'wrap' : wrap, - alignItems: align, - justifyContent: justify, - }, - ...rest, - }); -}; - -export const Flex = (props) => { - const { className, ...rest } = props; - return ( -
- ); -}; - -export type FlexItemProps = BoxProps & - Partial<{ - grow: number | boolean; - order: number; - shrink: number | boolean; - basis: string | number; - align: string | boolean; - style: Partial; - }>; - -export const computeFlexItemClassName = (props: FlexItemProps) => { - return classes([ - 'Flex__item', - Byond.IS_LTE_IE10 && 'Flex__item--iefix', - computeBoxClassName(props), - ]); -}; - -export const computeFlexItemProps = (props: FlexItemProps) => { - const { className, style, grow, order, shrink, basis, align, ...rest } = - props; - - const computedBasis = - basis ?? - // IE11: Set basis to specified width if it's known, which fixes certain - // bugs when rendering tables inside the flex. - props.width ?? - // If grow is used, basis should be set to 0 to be consistent with - // flex css shorthand `flex: 1`. - (grow !== undefined ? 0 : undefined); - - return computeBoxProps({ - style: { - ...style, - flexGrow: grow !== undefined && Number(grow), - flexShrink: shrink !== undefined && Number(shrink), - flexBasis: unit(computedBasis), - order: order, - alignSelf: align, - }, - ...rest, - }); -}; - -const FlexItem = (props) => { - const { className, ...rest } = props; - return ( -
- ); -}; - -Flex.Item = FlexItem; diff --git a/tgui/packages/tgui/components/Grid.jsx b/tgui/packages/tgui/components/Grid.jsx deleted file mode 100644 index f5593c9e00a5..000000000000 --- a/tgui/packages/tgui/components/Grid.jsx +++ /dev/null @@ -1,33 +0,0 @@ -/** - * @file - * @copyright 2020 Aleksej Komarov - * @license MIT - */ - -import { Table } from './Table'; - -/** @deprecated */ -export const Grid = (props) => { - const { children, ...rest } = props; - return ( - - {children} -
- ); -}; - -/** @deprecated */ -export const GridColumn = (props) => { - const { size = 1, style, ...rest } = props; - return ( - - ); -}; - -Grid.Column = GridColumn; diff --git a/tgui/packages/tgui/components/Grid.tsx b/tgui/packages/tgui/components/Grid.tsx new file mode 100644 index 000000000000..91c9256753bd --- /dev/null +++ b/tgui/packages/tgui/components/Grid.tsx @@ -0,0 +1,44 @@ +/** + * @file + * @copyright 2020 Aleksej Komarov + * @license MIT + */ + +import { PropsWithChildren } from 'react'; + +import { logger } from '../logging'; +import { BoxProps } from './Box'; +import { Table } from './Table'; + +/** @deprecated Do not use. Use stack instead. */ +export function Grid(props: PropsWithChildren) { + const { children, ...rest } = props; + logger.error('Grid component is deprecated. Use a Stack instead.'); + return ( + + {children} +
+ ); +} + +type Props = Partial<{ + /** Width of the column in percentage. */ + size: number; +}> & + BoxProps; + +/** @deprecated Do not use. Use stack instead. */ +export function GridColumn(props: Props) { + const { size = 1, style, ...rest } = props; + return ( + + ); +} + +Grid.Column = GridColumn; diff --git a/tgui/packages/tgui/components/Input.jsx b/tgui/packages/tgui/components/Input.jsx index 0eac3d05ebf1..b6bcbf84936b 100644 --- a/tgui/packages/tgui/components/Input.jsx +++ b/tgui/packages/tgui/components/Input.jsx @@ -17,8 +17,8 @@ export const toInputValue = value => ( ); export class Input extends Component { - constructor() { - super(); + constructor(props) { + super(props); this.inputRef = createRef(); this.state = { editing: false, diff --git a/tgui/packages/tgui/components/LabeledList.tsx b/tgui/packages/tgui/components/LabeledList.tsx index 8bb3ab82fd64..2d5a505eaf1f 100644 --- a/tgui/packages/tgui/components/LabeledList.tsx +++ b/tgui/packages/tgui/components/LabeledList.tsx @@ -59,7 +59,8 @@ const LabeledListItem = (props: LabeledListItemProps) => { as="span" style={{ borderBottom: '2px dotted rgba(255, 255, 255, 0.8)', - }}> + }} + > {innerLabel} @@ -75,7 +76,8 @@ const LabeledListItem = (props: LabeledListItemProps) => { // Kinda flipped because we want nowrap as default. Cleaner CSS this way though. !labelWrap && 'LabeledList__label--nowrap', ])} - verticalAlign={verticalAlign}> + verticalAlign={verticalAlign} + > {innerLabel} ); @@ -88,8 +90,10 @@ const LabeledListItem = (props: LabeledListItemProps) => { color={color} textAlign={textAlign} className={classes(['LabeledList__cell', 'LabeledList__content'])} + // @ts-ignore colSpan={buttons ? undefined : 2} - verticalAlign={verticalAlign}> + verticalAlign={verticalAlign} + > {content} {children} @@ -113,7 +117,8 @@ const LabeledListDivider = (props: LabeledListDividerProps) => { style={{ paddingTop: padding, paddingBottom: padding, - }}> + }} + > diff --git a/tgui/packages/tgui/components/Tooltip.tsx b/tgui/packages/tgui/components/Tooltip.tsx index b565e6fda475..20bba69a906a 100644 --- a/tgui/packages/tgui/components/Tooltip.tsx +++ b/tgui/packages/tgui/components/Tooltip.tsx @@ -1,3 +1,5 @@ +/* eslint-disable react/no-deprecated */ +// TODO: Rewrite as an FC, remove this lint disable import { createPopper, Placement, VirtualElement } from '@popperjs/core'; import { Component, ReactNode } from 'react'; import { findDOMNode, render } from 'react-dom'; @@ -115,7 +117,7 @@ export class Tooltip extends Component { { ...DEFAULT_OPTIONS, placement: this.props.position || 'auto', - } + }, ); Tooltip.singletonPopper = singletonPopper; diff --git a/tgui/packages/tgui/components/index.jsx b/tgui/packages/tgui/components/index.ts similarity index 100% rename from tgui/packages/tgui/components/index.jsx rename to tgui/packages/tgui/components/index.ts index 9dcc9213b320..f1641175246b 100644 --- a/tgui/packages/tgui/components/index.jsx +++ b/tgui/packages/tgui/components/index.ts @@ -18,8 +18,8 @@ export { Dimmer } from './Dimmer'; export { Divider } from './Divider'; export { DraggableControl } from './DraggableControl'; export { Dropdown } from './Dropdown'; -export { Flex } from './Flex'; export { FitText } from './FitText'; +export { Flex } from './Flex'; export { Grid } from './Grid'; export { Icon } from './Icon'; export { Image } from './Image'; @@ -32,8 +32,8 @@ export { LabeledList } from './LabeledList'; export { Modal } from './Modal'; export { NoticeBox } from './NoticeBox'; export { NumberInput } from './NumberInput'; -export { ProgressBar } from './ProgressBar'; export { Popper } from './Popper'; +export { ProgressBar } from './ProgressBar'; export { RestrictedInput } from './RestrictedInput'; export { RoundGauge } from './RoundGauge'; export { Section } from './Section'; @@ -43,5 +43,5 @@ export { Table } from './Table'; export { Tabs } from './Tabs'; export { TextArea } from './TextArea'; export { TimeDisplay } from './TimeDisplay'; -export { TrackOutsideClicks } from './TrackOutsideClicks'; export { Tooltip } from './Tooltip'; +export { TrackOutsideClicks } from './TrackOutsideClicks'; diff --git a/tgui/packages/tgui/constants.test.ts b/tgui/packages/tgui/constants.test.ts index e71a8b6d770e..abc513495638 100644 --- a/tgui/packages/tgui/constants.test.ts +++ b/tgui/packages/tgui/constants.test.ts @@ -1,4 +1,9 @@ -import { getGasColor, getGasFromId, getGasFromPath, getGasLabel } from './constants'; +import { + getGasColor, + getGasFromId, + getGasFromPath, + getGasLabel, +} from './constants'; describe('gas helper functions', () => { it('should get the proper gas label', () => { diff --git a/tgui/packages/tgui/drag.ts b/tgui/packages/tgui/drag.ts index a34c49ac4622..0884b1b0bd78 100644 --- a/tgui/packages/tgui/drag.ts +++ b/tgui/packages/tgui/drag.ts @@ -4,10 +4,10 @@ * @license MIT */ +import { storage } from 'common/storage'; import { vecAdd, vecMultiply, vecScale, vecSubtract } from 'common/vector'; import { createLogger } from './logging'; -import { storage } from 'common/storage'; const logger = createLogger('drag'); const pixelRatio = window.devicePixelRatio ?? 1; @@ -76,7 +76,7 @@ const getScreenSize = (): [number, number] => [ export const touchRecents = ( recents: string[], touchedItem: string, - limit = 50 + limit = 50, ): [string[], string | undefined] => { const nextRecents: string[] = [touchedItem]; let trimmedItem: string | undefined; @@ -105,7 +105,7 @@ const storeWindowGeometry = async () => { // Update the list of stored geometries const [geometries, trimmedKey] = touchRecents( (await storage.get('geometries')) || [], - windowKey + windowKey, ); if (trimmedKey) { storage.remove(trimmedKey); @@ -120,7 +120,7 @@ export const recallWindowGeometry = async ( pos?: [number, number]; size?: [number, number]; locked?: boolean; - } = {} + } = {}, ) => { const geometry = options.fancy && (await storage.get(windowKey)); if (geometry) { @@ -157,7 +157,7 @@ export const recallWindowGeometry = async ( pos = vecAdd( vecScale(areaAvailable, 0.5), vecScale(size, -0.5), - vecScale(screenOffset, -1.0) + vecScale(screenOffset, -1.0), ); setWindowPosition(pos); } @@ -182,7 +182,7 @@ export const setupDrag = async () => { */ const constraintPosition = ( pos: [number, number], - size: [number, number] + size: [number, number], ): [boolean, [number, number]] => { const screenPos = getScreenPosition(); const screenSize = getScreenSize(); @@ -208,8 +208,8 @@ export const dragStartHandler = (event) => { dragging = true; dragPointOffset = vecSubtract( [event.screenX, event.screenY], - getWindowPosition() - ); + getWindowPosition(), + ) as [number, number]; // Focus click target (event.target as HTMLElement)?.focus(); document.addEventListener('mousemove', dragMoveHandler); @@ -234,7 +234,10 @@ const dragMoveHandler = (event: MouseEvent) => { } event.preventDefault(); setWindowPosition( - vecSubtract([event.screenX, event.screenY], dragPointOffset) + vecSubtract([event.screenX, event.screenY], dragPointOffset) as [ + number, + number, + ], ); }; @@ -246,8 +249,8 @@ export const resizeStartHandler = resizing = true; dragPointOffset = vecSubtract( [event.screenX, event.screenY], - getWindowPosition() - ); + getWindowPosition(), + ) as [number, number]; initialSize = getWindowSize(); // Focus click target (event.target as HTMLElement)?.focus(); @@ -274,11 +277,14 @@ const resizeMoveHandler = (event: MouseEvent) => { event.preventDefault(); const currentOffset = vecSubtract( [event.screenX, event.screenY], - getWindowPosition() + getWindowPosition(), ); const delta = vecSubtract(currentOffset, dragPointOffset); // Extra 1x1 area is added to ensure the browser can see the cursor - size = vecAdd(initialSize, vecMultiply(resizeMatrix, delta), [1, 1]); + size = vecAdd(initialSize, vecMultiply(resizeMatrix, delta), [1, 1]) as [ + number, + number, + ]; // Sane window size values size[0] = Math.max(size[0], 150 * pixelRatio); size[1] = Math.max(size[1], 50 * pixelRatio); diff --git a/tgui/packages/tgui/interfaces/Adminhelp.tsx b/tgui/packages/tgui/interfaces/Adminhelp.tsx index c2a888cdc33c..7e29a807f1d4 100644 --- a/tgui/packages/tgui/interfaces/Adminhelp.tsx +++ b/tgui/packages/tgui/interfaces/Adminhelp.tsx @@ -1,6 +1,8 @@ import { BooleanLike } from 'common/react'; -import { useBackend, useLocalState } from '../backend'; -import { TextArea, Stack, Button, NoticeBox, Input, Box } from '../components'; +import { useState } from 'react'; + +import { useBackend } from '../backend'; +import { Box, Button, Input, NoticeBox, Stack, TextArea } from '../components'; import { Window } from '../layouts'; type AdminhelpData = { @@ -18,23 +20,18 @@ export const Adminhelp = (props) => { bannedFromUrgentAhelp, urgentAhelpPromptMessage, } = data; - const [requestForAdmin, setRequestForAdmin] = useLocalState( - 'request_for_admin', - false - ); - const [currentlyInputting, setCurrentlyInputting] = useLocalState( - 'confirm_request', - false - ); - const [ahelpMessage, setAhelpMessage] = useLocalState('ahelp_message', ''); + const [requestForAdmin, setRequestForAdmin] = useState(false); + const [currentlyInputting, setCurrentlyInputting] = useState(false); + const [ahelpMessage, setAhelpMessage] = useState(''); const confirmationText = 'alert admins'; return ( + backgroundImage: 'none', + }} + >