mirror of
https://github.com/VOREStation/VOREStation.git
synced 2026-08-26 05:27:39 +01:00
RS pack port and tgui core 3.0.4 (#17520)
* RS pack initial * fix RPD * urg wrong tgui folder * tgui core 3.0.4 * update tgui say * push that * .
This commit is contained in:
@@ -260,7 +260,10 @@
|
||||
json_data["config"] = list(
|
||||
"title" = title,
|
||||
"status" = status,
|
||||
"interface" = interface,
|
||||
"interface" = list(
|
||||
"name" = interface,
|
||||
"layout" = null, // user.client.prefs.read_preference(/datum/preference/choiced/tgui_layout), // unused
|
||||
),
|
||||
//"refreshing" = refreshing,
|
||||
"refreshing" = FALSE,
|
||||
"map" = (using_map && using_map.path) ? using_map.path : "Unknown",
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ plugins:
|
||||
- "@typescript-eslint"
|
||||
settings:
|
||||
react:
|
||||
version: '18.2'
|
||||
version: '19.1'
|
||||
rules:
|
||||
## Possible Errors
|
||||
## ----------------------------------------
|
||||
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/swcrc",
|
||||
"jsc": {
|
||||
"loose": true,
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"tsx": true
|
||||
},
|
||||
"transform": {
|
||||
"react": {
|
||||
"runtime": "automatic"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
-935
File diff suppressed because one or more lines are too long
Vendored
+948
File diff suppressed because one or more lines are too long
+1
-3
@@ -10,8 +10,6 @@ logFilters:
|
||||
- code: YN0062
|
||||
level: discard
|
||||
|
||||
pnpEnableEsmLoader: false
|
||||
|
||||
preferInteractive: true
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.8.1.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.9.1.cjs
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
module.exports = {
|
||||
roots: ['<rootDir>/packages'],
|
||||
testMatch: [
|
||||
'<rootDir>/packages/**/__tests__/*.{js,ts,tsx}',
|
||||
'<rootDir>/packages/**/*.{spec,test}.{js,ts,tsx}',
|
||||
],
|
||||
testPathIgnorePatterns: ['<rootDir>/packages/tgui-bench'],
|
||||
testEnvironment: 'jsdom',
|
||||
testRunner: require.resolve('jest-circus/runner'),
|
||||
transform: {
|
||||
'^.+\\.(js|cjs|ts|tsx)$': require.resolve('@swc/jest'),
|
||||
},
|
||||
moduleFileExtensions: ['js', 'cjs', 'ts', 'tsx', 'json'],
|
||||
resetMocks: true,
|
||||
};
|
||||
+19
-32
@@ -2,64 +2,51 @@
|
||||
"private": true,
|
||||
"name": "tgui-workspace",
|
||||
"version": "6.0.0",
|
||||
"packageManager": "yarn@4.8.1",
|
||||
"packageManager": "yarn@4.9.1",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"tgui:analyze": "webpack --analyze",
|
||||
"tgui:bench": "webpack --env TGUI_BENCH=1 && node packages/tgui-bench/index.js",
|
||||
"tgui:build": "BROWSERSLIST_IGNORE_OLD_DATA=true webpack",
|
||||
"tgui:analyze": "rspack --analyze",
|
||||
"tgui:bench": "rspack --env TGUI_BENCH=1 && node packages/tgui-bench/index.js",
|
||||
"tgui:build": "rspack build",
|
||||
"tgui:dev": "node --experimental-modules packages/tgui-dev-server/index.js",
|
||||
"tgui:lint": "eslint packages --ext .js,.cjs,.ts,.tsx",
|
||||
"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",
|
||||
"tgui:test": "vitest",
|
||||
"tgui:test-simple": "CI=true vitest",
|
||||
"tgui:test-ci": "CI=true vitest",
|
||||
"tgui:tsc": "tsc",
|
||||
"tgui:prettier-fix": "prettier --write .",
|
||||
"tgui:eslint-fix": "eslint --fix packages --ext .js,.cjs,.ts,.jsx,.tsx"
|
||||
},
|
||||
"dependencies": {
|
||||
"@swc/core": "^1.9.1",
|
||||
"@swc/jest": "^0.2.37",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.9.0",
|
||||
"@types/react": "^19.1.0",
|
||||
"@types/react-dom": "^19.1.1",
|
||||
"@types/webpack-env": "^1.18.5",
|
||||
"@types/wicg-file-system-access": "^2023.10.5",
|
||||
"@rspack/cli": "^1.3.4",
|
||||
"@rspack/core": "^1.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "^8.28.0",
|
||||
"@typescript-eslint/parser": "^8.28.0",
|
||||
"@typescript-eslint/utils": "^8.28.0",
|
||||
"css-loader": "^7.1.2",
|
||||
"esbuild-loader": "^4.2.2",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-react": "^7.34.1",
|
||||
"eslint-plugin-simple-import-sort": "^12.0.0",
|
||||
"eslint-plugin-sonarjs": "^0.25.0",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"file-loader": "^6.2.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-circus": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jsdom": "^25.0.1",
|
||||
"mini-css-extract-plugin": "^2.9.2",
|
||||
"jsdom": "^26.0.0",
|
||||
"prettier": "^3.2.5",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"sass": "^1.80.6",
|
||||
"sass-embedded": "^1.85.1",
|
||||
"sass-loader": "^16.0.3",
|
||||
"style-loader": "^4.0.0",
|
||||
"swc-loader": "^0.2.6",
|
||||
"tgui-core": "^2.0.5",
|
||||
"typescript": "5.8.3",
|
||||
"typescript-eslint": "^8.28.0",
|
||||
"url-loader": "^4.1.1",
|
||||
"webpack": "^5.96.1",
|
||||
"webpack-bundle-analyzer": "^4.10.2",
|
||||
"webpack-cli": "^5.1.4"
|
||||
"webpack": "^5.99.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/webpack-env": "^1.18.8",
|
||||
"@types/wicg-file-system-access": "^2023.10.6",
|
||||
"@types/ws": "^8.18.1",
|
||||
"vitest": "^3.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,136 +4,6 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* Iterates over elements of collection, returning an array of all elements
|
||||
* iteratee returns truthy for. The predicate is invoked with three
|
||||
* arguments: (value, index|key, collection).
|
||||
*
|
||||
* If collection is 'null' or 'undefined', it will be returned "as is"
|
||||
* without emitting any errors (which can be useful in some cases).
|
||||
*/
|
||||
export const filter = <T>(
|
||||
collection: T[],
|
||||
iterateeFn: (input: T, index: number, collection: T[]) => boolean,
|
||||
): T[] => {
|
||||
if (collection === null || collection === undefined) {
|
||||
return collection;
|
||||
}
|
||||
if (Array.isArray(collection)) {
|
||||
const result: T[] = [];
|
||||
for (let i = 0; i < collection.length; i++) {
|
||||
const item = collection[i];
|
||||
if (iterateeFn(item, i, collection)) {
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
throw new Error(`filter() can't iterate on type ${typeof collection}`);
|
||||
};
|
||||
|
||||
type MapFunction = {
|
||||
<T, U>(
|
||||
collection: T[],
|
||||
iterateeFn: (value: T, index: number, collection: T[]) => U,
|
||||
): U[];
|
||||
|
||||
<T, U, K extends string | number>(
|
||||
collection: Record<K, T>,
|
||||
iterateeFn: (value: T, index: K, collection: Record<K, T>) => U,
|
||||
): U[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates an array of values by running each element in collection
|
||||
* thru an iteratee function. The iteratee is invoked with three
|
||||
* arguments: (value, index|key, collection).
|
||||
*
|
||||
* If collection is 'null' or 'undefined', it will be returned "as is"
|
||||
* without emitting any errors (which can be useful in some cases).
|
||||
*/
|
||||
export const map: MapFunction = (collection, iterateeFn) => {
|
||||
if (collection === null || collection === undefined) {
|
||||
return collection;
|
||||
}
|
||||
|
||||
if (Array.isArray(collection)) {
|
||||
const result: unknown[] = [];
|
||||
for (let i = 0; i < collection.length; i++) {
|
||||
result.push(iterateeFn(collection[i], i, collection));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
if (typeof collection === 'object') {
|
||||
const result: unknown[] = [];
|
||||
for (const i in collection) {
|
||||
if (Object.prototype.hasOwnProperty.call(collection, i)) {
|
||||
result.push(iterateeFn(collection[i], i, collection));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new Error(`map() can't iterate on type ${typeof collection}`);
|
||||
};
|
||||
|
||||
const COMPARATOR = (objA, objB) => {
|
||||
const criteriaA = objA.criteria;
|
||||
const criteriaB = objB.criteria;
|
||||
const length = criteriaA.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
const a = criteriaA[i];
|
||||
const b = criteriaB[i];
|
||||
if (a < b) {
|
||||
return -1;
|
||||
}
|
||||
if (a > b) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates an array of elements, sorted in ascending order by the results
|
||||
* of running each element in a collection thru each iteratee.
|
||||
*
|
||||
* Iteratees are called with one argument (value).
|
||||
*/
|
||||
export const sortBy = <T>(
|
||||
array: T[],
|
||||
...iterateeFns: ((input: T) => unknown)[]
|
||||
): T[] => {
|
||||
if (!Array.isArray(array)) {
|
||||
return array;
|
||||
}
|
||||
let length = array.length;
|
||||
// Iterate over the array to collect criteria to sort it by
|
||||
const mappedArray: {
|
||||
criteria: unknown[];
|
||||
value: T;
|
||||
}[] = [];
|
||||
for (let i = 0; i < length; i++) {
|
||||
const value = array[i];
|
||||
mappedArray.push({
|
||||
criteria: iterateeFns.map((fn) => fn(value)),
|
||||
value,
|
||||
});
|
||||
}
|
||||
// Sort criteria using the base comparator
|
||||
mappedArray.sort(COMPARATOR);
|
||||
|
||||
// Unwrap values
|
||||
const values: T[] = [];
|
||||
while (length--) {
|
||||
values[length] = mappedArray[length].value;
|
||||
}
|
||||
return values;
|
||||
};
|
||||
|
||||
export const sort = <T>(array: T[]): T[] => sortBy(array);
|
||||
|
||||
/**
|
||||
* Returns a range of numbers from start to end, exclusively.
|
||||
* For example, range(0, 5) will return [0, 1, 2, 3, 4].
|
||||
@@ -163,26 +33,6 @@ type ReduceFunction = {
|
||||
): T;
|
||||
};
|
||||
|
||||
/**
|
||||
* A fast implementation of reduce.
|
||||
*/
|
||||
export const reduce: ReduceFunction = (array, reducerFn, initialValue?) => {
|
||||
const length = array.length;
|
||||
let i: number;
|
||||
let result;
|
||||
if (initialValue === undefined) {
|
||||
i = 1;
|
||||
result = array[0];
|
||||
} else {
|
||||
i = 0;
|
||||
result = initialValue;
|
||||
}
|
||||
for (; i < length; i++) {
|
||||
result = reducerFn(result, array[i], i, array);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a duplicate-free version of an array, using SameValueZero for
|
||||
* equality comparisons, in which only the first occurrence of each element
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "common",
|
||||
"version": "4.4.1"
|
||||
"version": "4.3.1",
|
||||
"devDependencies": {
|
||||
"vitest": "^3.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { describe, it } from 'vitest';
|
||||
|
||||
import {
|
||||
type Action,
|
||||
applyMiddleware,
|
||||
@@ -30,12 +32,12 @@ const increment = createAction('INCREMENT');
|
||||
const decrement = createAction('DECREMENT');
|
||||
|
||||
describe('Redux implementation tests', () => {
|
||||
test('createStore works', () => {
|
||||
it('createStore works', ({ expect }) => {
|
||||
const store = createStore(counterReducer);
|
||||
expect(store.getState()).toBe(0);
|
||||
});
|
||||
|
||||
test('createStore with applyMiddleware works', () => {
|
||||
it('createStore with applyMiddleware works', ({ expect }) => {
|
||||
const store = createStore(
|
||||
counterReducer,
|
||||
applyMiddleware(loggingMiddleware),
|
||||
@@ -43,7 +45,7 @@ describe('Redux implementation tests', () => {
|
||||
expect(store.getState()).toBe(0);
|
||||
});
|
||||
|
||||
test('dispatch works', () => {
|
||||
it('dispatch works', ({ expect }) => {
|
||||
const store = createStore(counterReducer);
|
||||
store.dispatch(increment());
|
||||
expect(store.getState()).toBe(1);
|
||||
@@ -51,7 +53,7 @@ describe('Redux implementation tests', () => {
|
||||
expect(store.getState()).toBe(0);
|
||||
});
|
||||
|
||||
test('combineReducers works', () => {
|
||||
it('combineReducers works', ({ expect }) => {
|
||||
const rootReducer = combineReducers({
|
||||
counter: counterReducer,
|
||||
});
|
||||
@@ -59,7 +61,7 @@ describe('Redux implementation tests', () => {
|
||||
expect(store.getState()).toEqual({ counter: 0 });
|
||||
});
|
||||
|
||||
test('createAction works', () => {
|
||||
it('createAction works', ({ expect }) => {
|
||||
const incrementAction = increment();
|
||||
expect(incrementAction).toEqual({ type: 'INCREMENT' });
|
||||
const decrementAction = decrement();
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
"version": "6.0.0",
|
||||
"dependencies": {
|
||||
"@fastify/static": "^8.0.2",
|
||||
"@types/react": "^19.1.0",
|
||||
"@types/react-dom": "^19.1.1",
|
||||
"common": "workspace:*",
|
||||
"fastify": "^5.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -13,6 +11,10 @@
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"tgui": "workspace:*",
|
||||
"tgui-core": "^2.0.5"
|
||||
"tgui-core": "^3.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.1.0",
|
||||
"@types/react-dom": "^19.1.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import React from 'react';
|
||||
import { createRenderer } from 'tgui/renderer';
|
||||
import { Button } from 'tgui-core/components';
|
||||
|
||||
const render = createRenderer();
|
||||
|
||||
export const SingleButton = () => {
|
||||
const node = <Button>Hello world!</Button>;
|
||||
render(node);
|
||||
};
|
||||
|
||||
export const SingleButtonWithCallback = () => {
|
||||
const node = <Button onClick={() => undefined}>Hello world!</Button>;
|
||||
render(node);
|
||||
};
|
||||
|
||||
export const ListOfButtons = () => {
|
||||
const nodes: React.JSX.Element[] = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const node = <Button key={i}>Hello world! {i}</Button>;
|
||||
nodes.push(node);
|
||||
}
|
||||
render(<div>{nodes}</div>);
|
||||
};
|
||||
|
||||
export const ListOfButtonsWithCallback = () => {
|
||||
const nodes: React.JSX.Element[] = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const node = (
|
||||
<Button key={i} onClick={() => undefined}>
|
||||
Hello world! {i}
|
||||
</Button>
|
||||
);
|
||||
nodes.push(node);
|
||||
}
|
||||
render(<div>{nodes}</div>);
|
||||
};
|
||||
|
||||
export const ListOfButtonsWithIcons = () => {
|
||||
const nodes: React.JSX.Element[] = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const node = (
|
||||
<Button key={i} icon={'arrow-left'}>
|
||||
Hello world! {i}
|
||||
</Button>
|
||||
);
|
||||
nodes.push(node);
|
||||
}
|
||||
render(<div>{nodes}</div>);
|
||||
};
|
||||
|
||||
export const ListOfButtonsWithTooltips = () => {
|
||||
const nodes: React.JSX.Element[] = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const node = (
|
||||
<Button key={i} tooltip={'Hello world!'}>
|
||||
Hello world! {i}
|
||||
</Button>
|
||||
);
|
||||
nodes.push(node);
|
||||
}
|
||||
render(<div>{nodes}</div>);
|
||||
};
|
||||
@@ -1,21 +1,10 @@
|
||||
import { backendUpdate, setGlobalStore } from 'tgui/backend';
|
||||
import { DisposalBin } from 'tgui/interfaces/DisposalBin';
|
||||
import { createRenderer } from 'tgui/renderer';
|
||||
import { render } from 'tgui/renderer';
|
||||
import { configureStore } from 'tgui/store';
|
||||
|
||||
const store = configureStore({ sideEffects: false });
|
||||
|
||||
const renderUi = createRenderer((dataJson: string) => {
|
||||
setGlobalStore(store);
|
||||
|
||||
store.dispatch(
|
||||
backendUpdate({
|
||||
data: Byond.parseJson(dataJson),
|
||||
}),
|
||||
);
|
||||
return <DisposalBin />;
|
||||
});
|
||||
|
||||
export const data = JSON.stringify({
|
||||
flush: 0,
|
||||
full_pressure: 1,
|
||||
@@ -25,4 +14,14 @@ export const data = JSON.stringify({
|
||||
isai: 0,
|
||||
});
|
||||
|
||||
export const Default = () => renderUi(data);
|
||||
export function Default() {
|
||||
setGlobalStore(store);
|
||||
|
||||
store.dispatch(
|
||||
backendUpdate({
|
||||
data: Byond.parseJson(data),
|
||||
}),
|
||||
);
|
||||
|
||||
return render(<DisposalBin />);
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { createRenderer } from 'tgui/renderer';
|
||||
import { Flex } from 'tgui-core/components';
|
||||
|
||||
const render = createRenderer();
|
||||
|
||||
export const Default = () => {
|
||||
const node = (
|
||||
<Flex align="baseline">
|
||||
<Flex.Item mr={1}>Text {Math.random()}</Flex.Item>
|
||||
<Flex.Item grow basis={0}>
|
||||
Text {Math.random()}
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
);
|
||||
render(node);
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
import { createRenderer } from 'tgui/renderer';
|
||||
import { Stack } from 'tgui-core/components';
|
||||
|
||||
const render = createRenderer();
|
||||
|
||||
export const Default = () => {
|
||||
const node = (
|
||||
<Stack align="baseline">
|
||||
<Stack.Item>Text {Math.random()}</Stack.Item>
|
||||
<Stack.Item grow basis={0}>
|
||||
Text {Math.random()}
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
);
|
||||
render(node);
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
import React from 'react';
|
||||
import { createRenderer } from 'tgui/renderer';
|
||||
import { Box, Tooltip } from 'tgui-core/components';
|
||||
|
||||
const render = createRenderer();
|
||||
|
||||
export const ListOfTooltips = () => {
|
||||
const nodes: React.JSX.Element[] = [];
|
||||
|
||||
for (let i = 0; i < 100; i++) {
|
||||
nodes.push(
|
||||
<Tooltip key={i} content={`This is from tooltip ${i}`} position="bottom">
|
||||
<Box as="span" backgroundColor="blue" fontSize="48px" m={1}>
|
||||
Tooltip #{i}
|
||||
</Box>
|
||||
</Tooltip>,
|
||||
);
|
||||
}
|
||||
|
||||
render(<div>{nodes}</div>);
|
||||
};
|
||||
@@ -4,7 +4,7 @@
|
||||
"version": "6.0.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@types/ws": "^8.5.13",
|
||||
"@rspack/core": "^1.3.4",
|
||||
"axios": "^1.7.7",
|
||||
"glob": "^7.2.3",
|
||||
"source-map": "^0.7.4",
|
||||
|
||||
@@ -13,31 +13,31 @@ import { createLogger } from './logging.js';
|
||||
import { reloadByondCache } from './reloader.js';
|
||||
import { resolveGlob } from './util.js';
|
||||
|
||||
const logger = createLogger('webpack');
|
||||
const logger = createLogger('rspack');
|
||||
|
||||
/**
|
||||
* @param {any} config
|
||||
* @return {WebpackCompiler}
|
||||
* @return {RspackCompiler}
|
||||
*/
|
||||
export const createCompiler = async (options) => {
|
||||
const compiler = new WebpackCompiler();
|
||||
const compiler = new RspackCompiler();
|
||||
await compiler.setup(options);
|
||||
return compiler;
|
||||
};
|
||||
|
||||
class WebpackCompiler {
|
||||
class RspackCompiler {
|
||||
async setup(options) {
|
||||
// Create a require context that is relative to project root
|
||||
// and retrieve all necessary dependencies.
|
||||
const requireFromRoot = createRequire(dirname(import.meta.url) + '/../..');
|
||||
const webpack = await requireFromRoot('webpack');
|
||||
const createConfig = await requireFromRoot('./webpack.config.js');
|
||||
const rspack = await requireFromRoot('@rspack/core');
|
||||
const createConfig = await requireFromRoot('./rspack.config.cjs');
|
||||
const config = createConfig({}, options);
|
||||
// Inject the HMR plugin into the config if we're using it
|
||||
if (options.hot) {
|
||||
config.plugins.push(new webpack.HotModuleReplacementPlugin());
|
||||
config.plugins.push(new rspack.HotModuleReplacementPlugin());
|
||||
}
|
||||
this.webpack = webpack;
|
||||
this.rspack = rspack;
|
||||
this.config = config;
|
||||
this.bundleDir = config.output.path;
|
||||
}
|
||||
@@ -47,7 +47,7 @@ class WebpackCompiler {
|
||||
// Setup link
|
||||
const link = setupLink();
|
||||
// Instantiate the compiler
|
||||
const compiler = this.webpack.webpack(this.config);
|
||||
const compiler = this.rspack.rspack(this.config);
|
||||
// Clear garbage before compiling
|
||||
compiler.hooks.watchRun.tapPromise('tgui-dev-server', async () => {
|
||||
const files = await resolveGlob(this.bundleDir, './*.hot-update.*');
|
||||
|
||||
@@ -33,9 +33,8 @@ export const ChatPageSettings = (props) => {
|
||||
<Stack.Item grow>
|
||||
<Input
|
||||
fluid
|
||||
updateOnPropsChange
|
||||
value={page.name}
|
||||
onChange={(e, value) =>
|
||||
onChange={(value) =>
|
||||
dispatch(
|
||||
updateChatPage({
|
||||
pageId: page.id,
|
||||
|
||||
@@ -652,10 +652,12 @@ class ChatRenderer {
|
||||
|
||||
/* eslint-disable react/no-danger */
|
||||
reactRoot.render(
|
||||
<Element {...outputProps}>
|
||||
<span dangerouslySetInnerHTML={oldHtml} />
|
||||
</Element>,
|
||||
childNode,
|
||||
<>
|
||||
<Element {...outputProps}>
|
||||
<span dangerouslySetInnerHTML={oldHtml} />
|
||||
</Element>
|
||||
{childNode}
|
||||
</>,
|
||||
);
|
||||
/* eslint-enable react/no-danger */
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { perf } from 'common/perf';
|
||||
import { combineReducers } from 'common/redux';
|
||||
import { setGlobalStore } from 'tgui/backend';
|
||||
import { captureExternalLinks } from 'tgui/links';
|
||||
import { createRenderer } from 'tgui/renderer';
|
||||
import { render } from 'tgui/renderer';
|
||||
import { configureStore } from 'tgui/store';
|
||||
import { setupGlobalEvents } from 'tgui-core/events';
|
||||
import { setupHotReloading } from 'tgui-dev-server/link/client.cjs';
|
||||
@@ -22,12 +22,13 @@ import { setupHotReloading } from 'tgui-dev-server/link/client.cjs';
|
||||
import { audioMiddleware, audioReducer } from './audio';
|
||||
import { chatMiddleware, chatReducer } from './chat';
|
||||
import { gameMiddleware, gameReducer } from './game';
|
||||
import { Panel } from './Panel';
|
||||
import { setupPanelFocusHacks } from './panelFocus';
|
||||
import { pingMiddleware, pingReducer } from './ping';
|
||||
import { settingsMiddleware, settingsReducer } from './settings';
|
||||
import { telemetryMiddleware } from './telemetry';
|
||||
|
||||
perf.mark('inception', window.performance?.timing?.navigationStart);
|
||||
perf.mark('inception', window.performance?.timeOrigin);
|
||||
perf.mark('init');
|
||||
|
||||
const store = configureStore({
|
||||
@@ -50,28 +51,24 @@ const store = configureStore({
|
||||
},
|
||||
});
|
||||
|
||||
const renderApp = createRenderer(() => {
|
||||
setGlobalStore(store);
|
||||
|
||||
const { Panel } = require('./Panel');
|
||||
return <Panel />;
|
||||
});
|
||||
|
||||
const setupApp = () => {
|
||||
function setupApp() {
|
||||
// Delay setup
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', setupApp);
|
||||
return;
|
||||
}
|
||||
|
||||
setGlobalStore(store);
|
||||
|
||||
setupGlobalEvents({
|
||||
ignoreWindowFocus: true,
|
||||
});
|
||||
|
||||
setupPanelFocusHacks();
|
||||
captureExternalLinks();
|
||||
|
||||
// Re-render UI on store updates
|
||||
store.subscribe(renderApp);
|
||||
store.subscribe(() => render(<Panel />));
|
||||
|
||||
// Dispatch incoming messages as store actions
|
||||
Byond.subscribe((type, payload) => store.dispatch({ type, payload }));
|
||||
@@ -104,10 +101,10 @@ const setupApp = () => {
|
||||
'./telemetry',
|
||||
],
|
||||
() => {
|
||||
renderApp();
|
||||
render(<Panel />);
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
setupApp();
|
||||
|
||||
@@ -3,14 +3,16 @@
|
||||
"name": "tgui-panel",
|
||||
"version": "6.0.0",
|
||||
"dependencies": {
|
||||
"@types/node": "^22.9.0",
|
||||
"@types/react": "^19.1.0",
|
||||
"common": "workspace:*",
|
||||
"dompurify": "^3.2.4",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"tgui": "workspace:*",
|
||||
"tgui-core": "^2.0.5",
|
||||
"tgui-core": "^3.0.4",
|
||||
"tgui-dev-server": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.1.0",
|
||||
"@types/react-dom": "^19.1.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ export const SettingsGeneral = (props) => {
|
||||
<Input
|
||||
width={'100%'}
|
||||
value={fontFamily}
|
||||
onChange={(e, value) =>
|
||||
onChange={(value) =>
|
||||
dispatch(
|
||||
updateSettings({
|
||||
fontFamily: value,
|
||||
@@ -183,11 +183,12 @@ export const SettingsGeneral = (props) => {
|
||||
<Box inline>
|
||||
<ColorBox mr={1} color={interleaveColor} />
|
||||
<Input
|
||||
expensive
|
||||
width="5em"
|
||||
monospace
|
||||
placeholder="#ffffff"
|
||||
value={interleaveColor}
|
||||
onInput={(e, value) =>
|
||||
onChange={(value) =>
|
||||
dispatch(
|
||||
updateSettings({
|
||||
interleaveColor: value,
|
||||
|
||||
@@ -165,11 +165,12 @@ const TextHighlightSetting = (props) => {
|
||||
<Stack.Item shrink={0}>
|
||||
<ColorBox mr={1} color={highlightColor} />
|
||||
<Input
|
||||
expensive
|
||||
width="5em"
|
||||
monospace
|
||||
placeholder="#ffffff"
|
||||
value={highlightColor}
|
||||
onInput={(e, value) =>
|
||||
onChange={(value) =>
|
||||
dispatch(
|
||||
updateHighlightSetting({
|
||||
id: id,
|
||||
@@ -181,10 +182,11 @@ const TextHighlightSetting = (props) => {
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<TextArea
|
||||
expensive
|
||||
height="3em"
|
||||
value={highlightText}
|
||||
placeholder="Put words to highlight here. Separate terms with commas, i.e. (term1, term2, term3)"
|
||||
onChange={(e, value) =>
|
||||
onChange={(value) =>
|
||||
dispatch(
|
||||
updateHighlightSetting({
|
||||
id: id,
|
||||
@@ -195,10 +197,11 @@ const TextHighlightSetting = (props) => {
|
||||
/>
|
||||
{!!highlightBlacklist && (
|
||||
<TextArea
|
||||
expensive
|
||||
height="3em"
|
||||
value={blacklistText}
|
||||
placeholder="Put names of senders you don't want highlighted here. Separate names with commas, i.e. (name1, name2, name3)"
|
||||
onChange={(e, value) =>
|
||||
onChange={(value) =>
|
||||
dispatch(
|
||||
updateHighlightSetting({
|
||||
id: id,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { beforeEach, describe, it } from 'vitest';
|
||||
|
||||
import { ChannelIterator } from './ChannelIterator';
|
||||
|
||||
describe('ChannelIterator', () => {
|
||||
@@ -7,7 +9,7 @@ describe('ChannelIterator', () => {
|
||||
channelIterator = new ChannelIterator();
|
||||
});
|
||||
|
||||
it('should cycle through channels properly', () => {
|
||||
it('should cycle through channels properly', ({ expect }) => {
|
||||
expect(channelIterator.current()).toBe('Say');
|
||||
expect(channelIterator.next()).toBe('Radio');
|
||||
expect(channelIterator.next()).toBe('Me');
|
||||
@@ -18,7 +20,7 @@ describe('ChannelIterator', () => {
|
||||
expect(channelIterator.next()).toBe('Say'); // Admin is blacklisted so it should be skipped
|
||||
});
|
||||
|
||||
it('should cycle through channels backwards properly', () => {
|
||||
it('should cycle through channels backwards properly', ({ expect }) => {
|
||||
expect(channelIterator.current()).toBe('Say');
|
||||
expect(channelIterator.prev()).toBe('OOC'); // Admin is blacklisted so it should be skipped
|
||||
expect(channelIterator.prev()).toBe('LOOC');
|
||||
@@ -29,32 +31,34 @@ describe('ChannelIterator', () => {
|
||||
expect(channelIterator.prev()).toBe('Say');
|
||||
});
|
||||
|
||||
it('should set a channel properly', () => {
|
||||
it('should set a channel properly', ({ expect }) => {
|
||||
channelIterator.set('OOC');
|
||||
expect(channelIterator.current()).toBe('OOC');
|
||||
});
|
||||
|
||||
it('should return true when current channel is "Say"', () => {
|
||||
it('should return true when current channel is "Say"', ({ expect }) => {
|
||||
channelIterator.set('Say');
|
||||
expect(channelIterator.isSay()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when current channel is not "Say"', () => {
|
||||
it('should return false when current channel is not "Say"', ({ expect }) => {
|
||||
channelIterator.set('Radio');
|
||||
expect(channelIterator.isSay()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when current channel is visible', () => {
|
||||
it('should return true when current channel is visible', ({ expect }) => {
|
||||
channelIterator.set('Say');
|
||||
expect(channelIterator.isVisible()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when current channel is not visible', () => {
|
||||
it('should return false when current channel is not visible', ({
|
||||
expect,
|
||||
}) => {
|
||||
channelIterator.set('OOC');
|
||||
expect(channelIterator.isVisible()).toBe(false);
|
||||
});
|
||||
|
||||
it('should not leak a message from a blacklisted channel', () => {
|
||||
it('should not leak a message from a blacklisted channel', ({ expect }) => {
|
||||
channelIterator.set('Admin');
|
||||
expect(channelIterator.next()).toBe('Admin');
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { beforeEach, describe, it } from 'vitest';
|
||||
|
||||
import { ChatHistory } from './ChatHistory';
|
||||
|
||||
describe('ChatHistory', () => {
|
||||
@@ -7,12 +9,12 @@ describe('ChatHistory', () => {
|
||||
chatHistory = new ChatHistory();
|
||||
});
|
||||
|
||||
it('should add a message to the history', () => {
|
||||
it('should add a message to the history', ({ expect }) => {
|
||||
chatHistory.add('Hello');
|
||||
expect(chatHistory.getOlderMessage()).toEqual('Hello');
|
||||
});
|
||||
|
||||
it('should retrieve older and newer messages', () => {
|
||||
it('should retrieve older and newer messages', ({ expect }) => {
|
||||
chatHistory.add('Hello');
|
||||
chatHistory.add('World');
|
||||
expect(chatHistory.getOlderMessage()).toEqual('World');
|
||||
@@ -22,7 +24,7 @@ describe('ChatHistory', () => {
|
||||
expect(chatHistory.getOlderMessage()).toEqual('World');
|
||||
});
|
||||
|
||||
it('should limit the history to 5 messages', () => {
|
||||
it('should limit the history to 5 messages', ({ expect }) => {
|
||||
for (let i = 1; i <= 6; i++) {
|
||||
chatHistory.add(`Message ${i}`);
|
||||
}
|
||||
@@ -34,13 +36,13 @@ describe('ChatHistory', () => {
|
||||
expect(chatHistory.getOlderMessage()).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle temp message correctly', () => {
|
||||
it('should handle temp message correctly', ({ expect }) => {
|
||||
chatHistory.saveTemp('Temp message');
|
||||
expect(chatHistory.getTemp()).toEqual('Temp message');
|
||||
expect(chatHistory.getTemp()).toBeNull();
|
||||
});
|
||||
|
||||
it('should reset correctly', () => {
|
||||
it('should reset correctly', ({ expect }) => {
|
||||
chatHistory.add('Hello');
|
||||
chatHistory.getOlderMessage();
|
||||
chatHistory.reset();
|
||||
|
||||
@@ -3,6 +3,7 @@ import './styles/main.scss';
|
||||
import {
|
||||
type FormEvent,
|
||||
type KeyboardEvent,
|
||||
type MouseEvent,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
@@ -63,9 +64,11 @@ export function TguiSay() {
|
||||
const [size, setSize] = useState(WindowSize.Small);
|
||||
const [maxLength, setMaxLength] = useState(4096);
|
||||
const [lightMode, setLightMode] = useState(false);
|
||||
const [position, setPosition] = useState([window.screenX, window.screenY]);
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
const position = useRef([window.screenX, window.screenY]);
|
||||
const isDragging = useRef(false);
|
||||
|
||||
function handleArrowKeys(direction: KEY.PageUp | KEY.PageDown): void {
|
||||
const chat = chatHistory.current;
|
||||
const iterator = channelIterator.current;
|
||||
@@ -110,6 +113,30 @@ export function TguiSay() {
|
||||
}
|
||||
}
|
||||
|
||||
function handleButtonClick(event: MouseEvent<HTMLButtonElement>): void {
|
||||
isDragging.current = true;
|
||||
|
||||
setTimeout(() => {
|
||||
// So the button doesn't jump around accidentally
|
||||
if (isDragging.current) {
|
||||
dragStartHandler(event.nativeEvent);
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
// Prevents the button from changing channels if it's dragged
|
||||
function handleButtonRelease(): void {
|
||||
isDragging.current = false;
|
||||
const currentPosition = [window.screenX, window.screenY];
|
||||
|
||||
if (JSON.stringify(position.current) !== JSON.stringify(currentPosition)) {
|
||||
position.current = currentPosition;
|
||||
return;
|
||||
}
|
||||
|
||||
handleIncrementChannel();
|
||||
}
|
||||
|
||||
function handleClose(): void {
|
||||
innerRef.current?.blur();
|
||||
windowClose(minimumWidth.current, minimumHeight.current, scale.current);
|
||||
@@ -157,9 +184,6 @@ export function TguiSay() {
|
||||
}
|
||||
|
||||
function handleIncrementChannel(): void {
|
||||
const xPos = window.screenX;
|
||||
const yPos = window.screenY;
|
||||
if (JSON.stringify(position) !== JSON.stringify([xPos, yPos])) return;
|
||||
const iterator = channelIterator.current;
|
||||
|
||||
iterator.next();
|
||||
@@ -285,19 +309,8 @@ export function TguiSay() {
|
||||
}
|
||||
}
|
||||
|
||||
function handleButtonDrag(e: React.MouseEvent<Element, MouseEvent>): void {
|
||||
const xPos = window.screenX;
|
||||
const yPos = window.screenY;
|
||||
setPosition([xPos, yPos]);
|
||||
dragStartHandler(e);
|
||||
}
|
||||
|
||||
function handleOpen(data: ByondOpen): void {
|
||||
setSize(minimumHeight.current);
|
||||
setTimeout(() => {
|
||||
innerRef.current?.focus();
|
||||
}, 1);
|
||||
|
||||
const { channel } = data;
|
||||
const iterator = channelIterator.current;
|
||||
// Catches the case where the modal is already open
|
||||
@@ -312,6 +325,10 @@ export function TguiSay() {
|
||||
minimumHeight.current,
|
||||
scale.current,
|
||||
);
|
||||
const input = innerRef.current;
|
||||
setTimeout(() => {
|
||||
input?.focus();
|
||||
}, 1);
|
||||
}
|
||||
|
||||
function handleProps(data: ByondProps): void {
|
||||
@@ -384,8 +401,8 @@ export function TguiSay() {
|
||||
>
|
||||
<button
|
||||
className={`button button-${theme}`}
|
||||
onClick={handleIncrementChannel}
|
||||
onMouseDown={handleButtonDrag}
|
||||
onMouseDown={handleButtonClick}
|
||||
onMouseUp={handleButtonRelease}
|
||||
type="button"
|
||||
>
|
||||
{buttonContent}
|
||||
|
||||
@@ -13,6 +13,9 @@ export function windowOpen(
|
||||
): void {
|
||||
setWindowVisibility(true, width, height, scale);
|
||||
Byond.sendMessage('open', { channel });
|
||||
Byond.winset('tgui_say.browser', {
|
||||
focus: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,12 +3,15 @@
|
||||
"name": "tgui-say",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@types/react": "^19.1.0",
|
||||
"@types/react-dom": "^19.1.1",
|
||||
"common": "workspace:*",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"tgui": "workspace:*",
|
||||
"tgui-core": "^2.0.5"
|
||||
"tgui-core": "^3.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.1.0",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"vitest": "^3.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
@each $channel, $color in colors.$channel-map {
|
||||
$darkened: color.adjust($color, $lightness: -20%, $space: hsl);
|
||||
|
||||
.button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.button-#{$channel} {
|
||||
border-color: color.adjust($color, $lightness: -10%, $space: hsl);
|
||||
color: $color;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { globalStore } from './backend';
|
||||
import { IconProvider } from './Icons';
|
||||
|
||||
export function App() {
|
||||
const { getRoutedComponent } = require('./routes');
|
||||
const Component = getRoutedComponent(globalStore);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Component />
|
||||
<IconProvider />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Suspense, useEffect } from 'react';
|
||||
import { fetchRetry } from 'tgui-core/http';
|
||||
|
||||
import { resolveAsset } from './assets';
|
||||
import { logger } from './logging';
|
||||
|
||||
function loadIconMap() {
|
||||
fetchRetry(resolveAsset('icon_ref_map.json'))
|
||||
.then((res) => res.json())
|
||||
.then((data) => (Byond.iconRefMap = data))
|
||||
.catch((error) => logger.log(error));
|
||||
}
|
||||
|
||||
function IconMapLoader() {
|
||||
useEffect(() => {
|
||||
if (Object.keys(Byond.iconRefMap).length === 0) {
|
||||
loadIconMap();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function IconProvider() {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<IconMapLoader />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -262,7 +262,10 @@ type BackendState<TData> = {
|
||||
config: {
|
||||
title: string;
|
||||
status: number;
|
||||
interface: string;
|
||||
interface: {
|
||||
name: string;
|
||||
layout: string;
|
||||
};
|
||||
refreshing: boolean;
|
||||
map: string; // Vorestation Add
|
||||
mapZLevel: number; // Vorestation Add
|
||||
|
||||
@@ -1,57 +1,59 @@
|
||||
import { describe, it } from 'vitest';
|
||||
|
||||
// import { getGasColor, getGasFromId, getGasFromPath, getGasLabel } from './constants';
|
||||
import { getGasColor, getGasFromId, getGasLabel } from './constants';
|
||||
|
||||
describe('gas helper functions', () => {
|
||||
it('should get the proper gas label', () => {
|
||||
it('should get the proper gas label', ({ expect }) => {
|
||||
// Testing for alphabetic gas id
|
||||
const gasId = 'oxygen';
|
||||
const gasLabel = getGasLabel(gasId);
|
||||
expect(gasLabel).toBe('O₂');
|
||||
});
|
||||
|
||||
it('should get the proper gas label', () => {
|
||||
it('should get the proper gas label', ({ expect }) => {
|
||||
// Testing for underscore gas id
|
||||
const gasId = 'nitrous_oxide';
|
||||
const gasLabel = getGasLabel(gasId);
|
||||
expect(gasLabel).toBe('N₂O');
|
||||
});
|
||||
|
||||
it('should get the proper gas label', () => {
|
||||
it('should get the proper gas label', ({ expect }) => {
|
||||
// Testing for wrong capitalization of two word gas
|
||||
const gasId = 'nitrous oxide';
|
||||
const gasLabel = getGasLabel(gasId); // This should set to Nitrous Oxide before checking
|
||||
expect(gasLabel).toBe('N₂O');
|
||||
});
|
||||
|
||||
it('should get the proper gas label with a fallback', () => {
|
||||
it('should get the proper gas label with a fallback', ({ expect }) => {
|
||||
const gasId = 'nonexistent';
|
||||
const gasLabel = getGasLabel(gasId, 'fallback');
|
||||
|
||||
expect(gasLabel).toBe('fallback');
|
||||
});
|
||||
|
||||
it('should return none if no gas and no fallback is found', () => {
|
||||
it('should return none if no gas and no fallback is found', ({ expect }) => {
|
||||
const gasId = 'nonexistent';
|
||||
const gasLabel = getGasLabel(gasId);
|
||||
|
||||
expect(gasLabel).toBe('None');
|
||||
});
|
||||
|
||||
it('should get the proper gas color', () => {
|
||||
it('should get the proper gas color', ({ expect }) => {
|
||||
const gasId = 'nitrous_oxide';
|
||||
const gasColor = getGasColor(gasId);
|
||||
|
||||
expect(gasColor).toBe('red');
|
||||
});
|
||||
|
||||
it('should return a string if no gas is found', () => {
|
||||
it('should return a string if no gas is found', ({ expect }) => {
|
||||
const gasId = 'nonexistent';
|
||||
const gasColor = getGasColor(gasId);
|
||||
|
||||
expect(gasColor).toBe('black');
|
||||
});
|
||||
|
||||
it('should return the gas object if found', () => {
|
||||
it('should return the gas object if found', ({ expect }) => {
|
||||
const gasId = 'nitrous_oxide';
|
||||
const gas = getGasFromId(gasId);
|
||||
|
||||
@@ -64,7 +66,7 @@ describe('gas helper functions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined if no gas is found', () => {
|
||||
it('should return undefined if no gas is found', ({ expect }) => {
|
||||
const gasId = 'nonexistent';
|
||||
const gas = getGasFromId(gasId);
|
||||
|
||||
@@ -72,7 +74,7 @@ describe('gas helper functions', () => {
|
||||
});
|
||||
|
||||
/*
|
||||
it('should return the gas using a path', () => {
|
||||
it('should return the gas using a path', ({ expect }) => {
|
||||
const gasPath = '/datum/gas/antinoblium';
|
||||
const gas = getGasFromPath(gasPath);
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { fetchRetry } from 'tgui-core/http';
|
||||
|
||||
import { resolveAsset } from './assets';
|
||||
import { logger } from './logging';
|
||||
|
||||
export function loadIconRefMap() {
|
||||
if (Object.keys(Byond.iconRefMap).length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetchRetry(resolveAsset('icon_ref_map.json'))
|
||||
.then((res) => res.json())
|
||||
.then((data) => (Byond.iconRefMap = data))
|
||||
.catch((error) => logger.log(error));
|
||||
}
|
||||
@@ -8,15 +8,24 @@
|
||||
import './styles/main.scss';
|
||||
import './styles/themes/abductor.scss';
|
||||
import './styles/themes/cardtable.scss';
|
||||
import './styles/themes/spookyconsole.scss';
|
||||
import './styles/themes/hackerman.scss';
|
||||
import './styles/themes/malfunction.scss';
|
||||
import './styles/themes/neutral.scss';
|
||||
import './styles/themes/ntos.scss';
|
||||
import './styles/themes/ntos_cat.scss';
|
||||
import './styles/themes/ntos_darkmode.scss';
|
||||
import './styles/themes/ntos_lightmode.scss';
|
||||
import './styles/themes/ntOS95.scss';
|
||||
import './styles/themes/ntos_synth.scss';
|
||||
import './styles/themes/ntos_terminal.scss';
|
||||
import './styles/themes/ntos_spooky.scss';
|
||||
import './styles/themes/paper.scss';
|
||||
import './styles/themes/pda-retro.scss';
|
||||
import './styles/themes/retro.scss';
|
||||
import './styles/themes/syndicate.scss';
|
||||
import './styles/themes/wizard.scss';
|
||||
import './styles/themes/admin.scss';
|
||||
import './styles/themes/abstract.scss';
|
||||
import './styles/themes/bingle.scss';
|
||||
|
||||
@@ -25,26 +34,17 @@ import { setupGlobalEvents } from 'tgui-core/events';
|
||||
import { setupHotKeys } from 'tgui-core/hotkeys';
|
||||
import { setupHotReloading } from 'tgui-dev-server/link/client.cjs';
|
||||
|
||||
import { App } from './App';
|
||||
import { setGlobalStore } from './backend';
|
||||
import { loadIconRefMap } from './icons';
|
||||
import { captureExternalLinks } from './links';
|
||||
import { createRenderer } from './renderer';
|
||||
import { render } from './renderer';
|
||||
import { configureStore } from './store';
|
||||
|
||||
perf.mark('inception', window.performance?.timing?.navigationStart);
|
||||
perf.mark('inception', window.performance?.timeOrigin);
|
||||
perf.mark('init');
|
||||
|
||||
const store = configureStore();
|
||||
|
||||
const renderApp = createRenderer(() => {
|
||||
setGlobalStore(store);
|
||||
loadIconRefMap();
|
||||
|
||||
const { getRoutedComponent } = require('./routes');
|
||||
const Component = getRoutedComponent(store);
|
||||
return <Component />;
|
||||
});
|
||||
|
||||
function setupApp() {
|
||||
// Delay setup
|
||||
if (document.readyState === 'loading') {
|
||||
@@ -52,12 +52,13 @@ function setupApp() {
|
||||
return;
|
||||
}
|
||||
|
||||
setGlobalStore(store);
|
||||
|
||||
setupGlobalEvents();
|
||||
setupHotKeys();
|
||||
captureExternalLinks();
|
||||
|
||||
// Re-render UI on store updates
|
||||
store.subscribe(renderApp);
|
||||
store.subscribe(() => render(<App />));
|
||||
|
||||
// Dispatch incoming messages as store actions
|
||||
Byond.subscribe((type, payload) => store.dispatch({ type, payload }));
|
||||
@@ -65,14 +66,8 @@ function setupApp() {
|
||||
// Enable hot module reloading
|
||||
if (module.hot) {
|
||||
setupHotReloading();
|
||||
// prettier-ignore
|
||||
module.hot.accept([
|
||||
'./components',
|
||||
'./debug',
|
||||
'./layouts',
|
||||
'./routes',
|
||||
], () => {
|
||||
renderApp();
|
||||
module.hot.accept(['./debug', './layouts', './routes', './App'], () => {
|
||||
render(<App />);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,10 +115,10 @@ const NewAccountView = (props) => {
|
||||
<Section title="Create Account">
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Account Holder">
|
||||
<Input value={holder} fluid onInput={(e, val) => setHolder(val)} />
|
||||
<Input value={holder} fluid onChange={(val) => setHolder(val)} />
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Initial Deposit">
|
||||
<Input value={newMoney} fluid onInput={(e, val) => setMoney(val)} />
|
||||
<Input value={newMoney} fluid onChange={(val) => setMoney(val)} />
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
<Button
|
||||
|
||||
@@ -300,7 +300,7 @@ export const AppearanceChangerMarkings = (props) => {
|
||||
fluid
|
||||
placeholder={'Search for markings...'}
|
||||
value={searchText}
|
||||
onInput={(e, val) => setSearchText(val)}
|
||||
onChange={(val) => setSearchText(val)}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
|
||||
@@ -39,7 +39,7 @@ export const AppearanceChangerParts = (props: {
|
||||
fluid
|
||||
placeholder={'Search for ' + section.toLowerCase() + '...'}
|
||||
value={searchText}
|
||||
onInput={(e, val) => setSearchText(val)}
|
||||
onChange={(val) => setSearchText(val)}
|
||||
/>
|
||||
)}
|
||||
</Stack.Item>
|
||||
@@ -107,7 +107,7 @@ export const AppearanceChangerHair = (props: {
|
||||
fluid
|
||||
placeholder={'Search for ' + section.toLowerCase() + '...'}
|
||||
value={searchText}
|
||||
onInput={(e, val) => setSearchText(val)}
|
||||
onChange={(val) => setSearchText(val)}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
|
||||
@@ -130,9 +130,9 @@ const Designs = (props) => {
|
||||
<Input
|
||||
fluid
|
||||
placeholder="Search all designs..."
|
||||
updateOnPropsChange
|
||||
expensive
|
||||
value={searchText}
|
||||
onChange={(e, val) => setSearchText(val)}
|
||||
onChange={(val) => setSearchText(val)}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
|
||||
@@ -278,17 +278,9 @@ const CustomWithdrawal = (props: {
|
||||
<Stack.Item>
|
||||
<Input
|
||||
fluid
|
||||
value={money}
|
||||
value={money.toString()}
|
||||
maxLength={10}
|
||||
onInput={(e, val) => {
|
||||
const value = parseInt(val, 10);
|
||||
if (isNaN(value)) {
|
||||
setMoney(0);
|
||||
} else {
|
||||
setMoney(value);
|
||||
}
|
||||
}}
|
||||
onChange={(e, val) => {
|
||||
onChange={(val) => {
|
||||
const value = parseInt(val, 10);
|
||||
if (isNaN(value)) {
|
||||
setMoney(0);
|
||||
@@ -479,18 +471,16 @@ const TransferMenu = (props: {
|
||||
<Input
|
||||
fluid
|
||||
maxLength={6}
|
||||
value={accountNum}
|
||||
onChange={(e, val) => updateAccountNum(val)}
|
||||
onInput={(e, val) => updateAccountNum(val)}
|
||||
value={accountNum.toString()}
|
||||
onChange={(val) => updateAccountNum(val)}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Funds To Transfer">
|
||||
<Input
|
||||
fluid
|
||||
maxLength={10}
|
||||
value={money}
|
||||
onChange={(e, val) => updateMoney(val)}
|
||||
onInput={(e, val) => updateMoney(val)}
|
||||
value={money.toString()}
|
||||
onChange={(val) => updateMoney(val)}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Transaction Purpose">
|
||||
@@ -498,8 +488,7 @@ const TransferMenu = (props: {
|
||||
fluid
|
||||
maxLength={20}
|
||||
value={purpose}
|
||||
onChange={(e, val) => setPurpose(val)}
|
||||
onInput={(e, val) => setPurpose(val)}
|
||||
onChange={(val) => setPurpose(val)}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
@@ -560,17 +549,11 @@ const LoginScreen = (props: { machine_id: string; card: string | null }) => {
|
||||
<Input
|
||||
fluid
|
||||
value={account}
|
||||
onChange={(e, val) => setAccount(val)}
|
||||
onInput={(e, val) => setAccount(val)}
|
||||
onChange={(val) => setAccount(val)}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="PIN">
|
||||
<Input
|
||||
fluid
|
||||
value={pin}
|
||||
onChange={(e, val) => setPin(val)}
|
||||
onInput={(e, val) => setPin(val)}
|
||||
/>
|
||||
<Input fluid value={pin} onChange={(val) => setPin(val)} />
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
<Button
|
||||
|
||||
@@ -169,7 +169,7 @@ const BiogeneratorSearch = (props: {
|
||||
placeholder="Search by item name.."
|
||||
value={props.searchText}
|
||||
width="100%"
|
||||
onInput={(e, value: string) => props.onSearchText(value)}
|
||||
onChange={(value: string) => props.onSearchText(value)}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item basis="30%">
|
||||
|
||||
@@ -155,7 +155,7 @@ export const CameraConsoleContent = (props) => {
|
||||
fluid
|
||||
mt={1}
|
||||
placeholder="Search for a camera"
|
||||
onInput={(e, value: string) => setSearchText(value)}
|
||||
onChange={(value: string) => setSearchText(value)}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
|
||||
@@ -81,7 +81,7 @@ const CasinoPrizeDispenserSearch = (props: {
|
||||
<Input
|
||||
placeholder="Search by item name.."
|
||||
width="100%"
|
||||
onInput={(_e, value) => props.onSearchText(value)}
|
||||
onChange={(value) => props.onSearchText(value)}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item basis="30%">
|
||||
|
||||
@@ -96,7 +96,7 @@ export const CheckboxInput = (props) => {
|
||||
<Input
|
||||
fluid
|
||||
value={searchQuery}
|
||||
onInput={(e, value: string) => setSearchQuery(value)}
|
||||
onChange={(value: string) => setSearchQuery(value)}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
|
||||
@@ -109,7 +109,7 @@ export const AnalyzerSearchList = (props: {
|
||||
fluid
|
||||
value={searchText}
|
||||
placeholder={'Search for ' + title + '...'}
|
||||
onInput={(e, value: string) => onSearchText(value)}
|
||||
onChange={(value: string) => onSearchText(value)}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Divider />
|
||||
|
||||
@@ -243,12 +243,10 @@ export const ColorMateMatrix = (props) => {
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Config">
|
||||
<Input
|
||||
updateOnPropsChange
|
||||
expensive
|
||||
fluid
|
||||
value={Object.values(matrixcolors).toString()}
|
||||
onChange={(e, value: string) =>
|
||||
act('set_matrix_string', { value })
|
||||
}
|
||||
onChange={(value: string) => act('set_matrix_string', { value })}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
|
||||
@@ -71,7 +71,7 @@ export const CommunicatorPhoneTab = (props) => {
|
||||
<Input
|
||||
fluid
|
||||
value={targetAddress}
|
||||
onInput={(e, val) => act('write_target_address', { val: val })}
|
||||
onChange={(val) => act('write_target_address', { val: val })}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
|
||||
@@ -79,7 +79,7 @@ export const CrewMonitorCrew = (props: { crew: crewmember[] }) => {
|
||||
fluid
|
||||
value={nameSearch}
|
||||
placeholder="Search for Name..."
|
||||
onChange={(e, val) => setNameSearch(val)}
|
||||
onChange={(val) => setNameSearch(val)}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
|
||||
@@ -184,8 +184,9 @@ export const NarrationInput = (props) => {
|
||||
<Stack>
|
||||
<Stack.Item width="85%">
|
||||
<TextArea
|
||||
expensive
|
||||
height={'18rem'}
|
||||
onChange={(e, val) => setNarration(val)}
|
||||
onChange={(val) => setNarration(val)}
|
||||
value={narration || ''}
|
||||
/>
|
||||
</Stack.Item>
|
||||
|
||||
@@ -110,7 +110,7 @@ export const PartLists = (props: {
|
||||
fluid
|
||||
placeholder="Search for..."
|
||||
value={searchText}
|
||||
onInput={(e, v) => setSearchText(v)}
|
||||
onChange={(v) => setSearchText(v)}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
|
||||
@@ -21,7 +21,7 @@ export const GeneralRecordsList = (props) => {
|
||||
<Input
|
||||
fluid
|
||||
placeholder="Search by Name, DNA, or ID"
|
||||
onInput={(e, value: string) => setSearchText(value)}
|
||||
onChange={(value: string) => setSearchText(value)}
|
||||
/>
|
||||
<Box mt="0.5rem">
|
||||
{records.map((record, i) => (
|
||||
|
||||
@@ -168,18 +168,16 @@ export const IdentificationComputerAccessModification = (props: {
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Registered Name">
|
||||
<Input
|
||||
updateOnPropsChange
|
||||
value={target_owner!}
|
||||
fluid
|
||||
onInput={(e, val) => act('reg', { reg: val })}
|
||||
onChange={(val) => act('reg', { reg: val })}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Account Number">
|
||||
<Input
|
||||
updateOnPropsChange
|
||||
value={account_number!}
|
||||
value={account_number?.toString()}
|
||||
fluid
|
||||
onInput={(e, val) => act('account', { account: val })}
|
||||
onChange={(val) => act('account', { account: val })}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Dismissals">
|
||||
|
||||
@@ -252,14 +252,14 @@ export const Jukebox = (props) => {
|
||||
<Input
|
||||
width="100%"
|
||||
value={newTitle}
|
||||
onChange={(e, val: string) => setNewTitle(val)}
|
||||
onChange={(val: string) => setNewTitle(val)}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="URL">
|
||||
<Input
|
||||
width="100%"
|
||||
value={newUrl}
|
||||
onChange={(e, val: string) => setNewUrl(val)}
|
||||
onChange={(val: string) => setNewUrl(val)}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Playtime">
|
||||
@@ -276,7 +276,7 @@ export const Jukebox = (props) => {
|
||||
<Input
|
||||
width="100%"
|
||||
value={newArtist}
|
||||
onChange={(e, val: string) => setNewArtist(val)}
|
||||
onChange={(val: string) => setNewArtist(val)}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Genre">
|
||||
@@ -286,7 +286,7 @@ export const Jukebox = (props) => {
|
||||
<Input
|
||||
width="100%"
|
||||
value={newGenre}
|
||||
onChange={(e, val: string) => setNewGenre(val)}
|
||||
onChange={(val: string) => setNewGenre(val)}
|
||||
/>
|
||||
) : (
|
||||
<Box>{newGenre}</Box>
|
||||
|
||||
@@ -294,10 +294,10 @@ export const LawManagerLaws = (props: {
|
||||
<Table.Cell collapsing>Zero</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Input
|
||||
updateOnPropsChange
|
||||
expensive
|
||||
value={zeroth_law}
|
||||
fluid
|
||||
onChange={(e, val: string) =>
|
||||
onChange={(val: string) =>
|
||||
act('change_zeroth_law', { val: val })
|
||||
}
|
||||
/>
|
||||
@@ -316,10 +316,10 @@ export const LawManagerLaws = (props: {
|
||||
<Table.Cell collapsing>Ion</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Input
|
||||
updateOnPropsChange
|
||||
expensive
|
||||
value={ion_law}
|
||||
fluid
|
||||
onChange={(e, val: string) =>
|
||||
onChange={(val: string) =>
|
||||
act('change_ion_law', { val: val })
|
||||
}
|
||||
/>
|
||||
@@ -335,10 +335,10 @@ export const LawManagerLaws = (props: {
|
||||
<Table.Cell>Inherent</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Input
|
||||
updateOnPropsChange
|
||||
expensive
|
||||
value={inherent_law}
|
||||
fluid
|
||||
onChange={(e, val: string) =>
|
||||
onChange={(val: string) =>
|
||||
act('change_inherent_law', { val: val })
|
||||
}
|
||||
/>
|
||||
@@ -354,10 +354,10 @@ export const LawManagerLaws = (props: {
|
||||
<Table.Cell>Supplied</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Input
|
||||
updateOnPropsChange
|
||||
expensive
|
||||
value={supplied_law}
|
||||
fluid
|
||||
onChange={(e, val: string) =>
|
||||
onChange={(val: string) =>
|
||||
act('change_supplied_law', { val: val })
|
||||
}
|
||||
/>
|
||||
@@ -496,7 +496,7 @@ export const LawManagerLawSets = (props: {
|
||||
fluid
|
||||
value={searchLawName}
|
||||
placeholder="Search for laws..."
|
||||
onInput={(e, value: string) => onSearchLawName(value)}
|
||||
onChange={(value: string) => onSearchLawName(value)}
|
||||
/>
|
||||
{law_sets.length
|
||||
? prepareSearch(law_sets, searchLawName).map((laws) => (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useBackend } from 'tgui/backend';
|
||||
import { Window } from 'tgui/layouts';
|
||||
import { Autofocus, Button, Input, Section, Stack } from 'tgui-core/components';
|
||||
@@ -31,7 +31,7 @@ export const ListInputModal = (props) => {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
// User presses up or down on keyboard
|
||||
// Simulates clicking an item
|
||||
const inputRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const onArrowKey = (key: KEY) => {
|
||||
const len = filteredItems.length - 1;
|
||||
if (key === KEY.Down) {
|
||||
@@ -160,11 +160,17 @@ export const ListInputModal = (props) => {
|
||||
/>
|
||||
</Stack.Item>
|
||||
{searchBarVisible && (
|
||||
<SearchBar
|
||||
filteredItems={filteredItems}
|
||||
onSearch={onSearch}
|
||||
searchQuery={searchQuery}
|
||||
selected={selected}
|
||||
<Input
|
||||
autoFocus
|
||||
autoSelect
|
||||
fluid
|
||||
expensive
|
||||
onEnter={() => {
|
||||
act('submit', { entry: filteredItems[selected] });
|
||||
}}
|
||||
onChange={onSearch}
|
||||
placeholder="Search..."
|
||||
value={searchQuery}
|
||||
/>
|
||||
)}
|
||||
<Stack.Item>
|
||||
@@ -200,6 +206,7 @@ const ListDisplay = (props) => {
|
||||
{filteredItems.map((item, index) => {
|
||||
return (
|
||||
<Button
|
||||
className="candystripe"
|
||||
color="transparent"
|
||||
fluid
|
||||
id={index}
|
||||
@@ -225,27 +232,3 @@ const ListDisplay = (props) => {
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a search bar input.
|
||||
* Closing the bar defaults input to an empty string.
|
||||
*/
|
||||
const SearchBar = (props) => {
|
||||
const { act } = useBackend<ListInputData>();
|
||||
const { filteredItems, onSearch, searchQuery, selected } = props;
|
||||
|
||||
return (
|
||||
<Input
|
||||
autoFocus
|
||||
autoSelect
|
||||
fluid
|
||||
onEnter={(event) => {
|
||||
event.preventDefault();
|
||||
act('submit', { entry: filteredItems[selected] });
|
||||
}}
|
||||
onInput={(_, value) => onSearch(value)}
|
||||
placeholder="Search..."
|
||||
value={searchQuery}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -37,7 +37,7 @@ export const MaterialStack = (props) => {
|
||||
fluid
|
||||
placeholder="Search for recipe..."
|
||||
value={searchText}
|
||||
onInput={(e, val) => setSearchText(val)}
|
||||
onChange={(val) => setSearchText(val)}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
|
||||
@@ -11,7 +11,7 @@ export const MedicalRecordsList = (props) => {
|
||||
<Input
|
||||
fluid
|
||||
placeholder="Search by Name, DNA, or ID"
|
||||
onChange={(e, value: string) => act('search', { t1: value })}
|
||||
onChange={(value: string) => act('search', { t1: value })}
|
||||
/>
|
||||
<Box mt="0.5rem">
|
||||
{records.map((record, i) => (
|
||||
|
||||
@@ -23,9 +23,10 @@ export const MessageMonitorLogin = (props) => {
|
||||
<Box color="label" my="1rem">
|
||||
Decryption Key:
|
||||
<Input
|
||||
expensive
|
||||
placeholder="Decryption Key"
|
||||
ml="0.5rem"
|
||||
onChange={(e, val) => act('auth', { key: val })}
|
||||
onChange={(val) => act('auth', { key: val })}
|
||||
/>
|
||||
</Box>
|
||||
{!!isMalfAI && (
|
||||
|
||||
@@ -160,15 +160,17 @@ export const MessageMonitorAdmin = (props) => {
|
||||
<LabeledList.Item label="Sender">
|
||||
<Input
|
||||
fluid
|
||||
expensive
|
||||
value={customsender}
|
||||
onChange={(e, val) => act('set_sender', { val: val })}
|
||||
onChange={(val) => act('set_sender', { val: val })}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Sender's Job">
|
||||
<Input
|
||||
fluid
|
||||
expensive
|
||||
value={customjob}
|
||||
onChange={(e, val) => act('set_sender_job', { val: val })}
|
||||
onChange={(val) => act('set_sender_job', { val: val })}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Recipient">
|
||||
@@ -189,8 +191,9 @@ export const MessageMonitorAdmin = (props) => {
|
||||
<Input
|
||||
fluid
|
||||
mb={0.5}
|
||||
expensive
|
||||
value={custommessage}
|
||||
onChange={(e, val: string) => act('set_message', { val: val })}
|
||||
onChange={(val: string) => act('set_message', { val: val })}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
|
||||
@@ -137,7 +137,7 @@ const MiningVendorSearch = (props: {
|
||||
placeholder="Search by item name.."
|
||||
value={props.searchText}
|
||||
width="100%"
|
||||
onInput={(_e, value) => props.onSearchText(value)}
|
||||
onChange={(value) => props.onSearchText(value)}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item basis="30%">
|
||||
|
||||
@@ -190,7 +190,7 @@ const GeneralMobSettings = (props: {
|
||||
<Input
|
||||
fluid
|
||||
value={data.path_name}
|
||||
onChange={(e, val: string) => props.onName(val)}
|
||||
onChange={(val: string) => props.onName(val)}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Mob Path">
|
||||
@@ -364,7 +364,8 @@ const GeneralMobSettings = (props: {
|
||||
<br />
|
||||
<TextArea
|
||||
height={'18rem'}
|
||||
onChange={(e, val: string) => props.onDesc(val)}
|
||||
expensive
|
||||
onChange={(val: string) => props.onDesc(val)}
|
||||
value={data.desc}
|
||||
/>
|
||||
</Stack.Item>
|
||||
@@ -373,8 +374,9 @@ const GeneralMobSettings = (props: {
|
||||
<br />
|
||||
<TextArea
|
||||
height={'18rem'}
|
||||
expensive
|
||||
value={data.flavor_text}
|
||||
onChange={(e, val: string) => props.onFlavor(val)}
|
||||
onChange={(val: string) => props.onFlavor(val)}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
|
||||
@@ -142,7 +142,7 @@ const AccessSection = (props: {
|
||||
fluid
|
||||
value={searchText}
|
||||
placeholder="Search for access..."
|
||||
onInput={(e, value: string) => onSearchText(value)}
|
||||
onChange={(value: string) => onSearchText(value)}
|
||||
/>
|
||||
<Divider />
|
||||
<Stack>
|
||||
|
||||
@@ -242,7 +242,7 @@ const ComponentSection = (props: {
|
||||
fluid
|
||||
value={searchText}
|
||||
placeholder="Search for components..."
|
||||
onInput={(e, value: string) => onSearchText(value)}
|
||||
onChange={(value: string) => onSearchText(value)}
|
||||
/>
|
||||
<Divider />
|
||||
<Stack>
|
||||
|
||||
@@ -160,7 +160,7 @@ const SelectionField = (props: {
|
||||
fluid
|
||||
value={searchText}
|
||||
placeholder="Search for modules..."
|
||||
onInput={(e, value: string) => onSearchText(value)}
|
||||
onChange={(value: string) => onSearchText(value)}
|
||||
/>
|
||||
<Divider />
|
||||
<Stack>
|
||||
|
||||
@@ -36,7 +36,7 @@ export const ModifyRobotPKA = (props: { target: Target }) => {
|
||||
fluid
|
||||
value={searchModkitText}
|
||||
placeholder="Search for modkits..."
|
||||
onInput={(e, value: string) => setSearchModkitText(value)}
|
||||
onChange={(value: string) => setSearchModkitText(value)}
|
||||
/>
|
||||
<Divider />
|
||||
{prepareSearch(target.pka.modkits, searchModkitText).map(
|
||||
@@ -82,7 +82,7 @@ export const ModifyRobotPKA = (props: { target: Target }) => {
|
||||
fluid
|
||||
value={searchInstalledtext}
|
||||
placeholder="Search for modkits..."
|
||||
onInput={(e, value: string) => setSearchInstalledtext(value)}
|
||||
onChange={(value: string) => setSearchInstalledtext(value)}
|
||||
/>
|
||||
<Divider />
|
||||
{prepareSearch(
|
||||
|
||||
@@ -89,7 +89,7 @@ const RadioSection = (props: {
|
||||
fluid
|
||||
value={searchText}
|
||||
placeholder="Search for channels..."
|
||||
onInput={(e, value: string) => onSearchText(value)}
|
||||
onChange={(value: string) => onSearchText(value)}
|
||||
/>
|
||||
<Divider />
|
||||
<Stack>
|
||||
|
||||
@@ -122,7 +122,7 @@ const UpgradeSection = (props: {
|
||||
fluid
|
||||
value={searchText}
|
||||
placeholder="Search for upgrades..."
|
||||
onInput={(e, value: string) => onSearchText(value)}
|
||||
onChange={(value: string) => onSearchText(value)}
|
||||
/>
|
||||
<Divider />
|
||||
<Stack>
|
||||
|
||||
@@ -178,7 +178,7 @@ export const ModifyRobot = (props) => {
|
||||
<Input
|
||||
width="200px"
|
||||
value={robotName}
|
||||
onChange={(e, value) => setRobotName(value)}
|
||||
onChange={(value) => setRobotName(value)}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
|
||||
@@ -25,9 +25,9 @@ export const NewscasterNewChannel = (props: { setScreen: Function }) => {
|
||||
<LabeledList.Item label="Channel Name">
|
||||
<Input
|
||||
fluid
|
||||
updateOnPropsChange
|
||||
expensive
|
||||
value={decodeHtmlEntities(channel_name)}
|
||||
onInput={(e, val: string) => act('set_channel_name', { val: val })}
|
||||
onChange={(val: string) => act('set_channel_name', { val: val })}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Channel Author" color="good">
|
||||
|
||||
@@ -31,17 +31,17 @@ export const NewscasterNewWanted = (props: { setScreen: Function }) => {
|
||||
<LabeledList.Item label="Criminal Name">
|
||||
<Input
|
||||
fluid
|
||||
updateOnPropsChange
|
||||
expensive
|
||||
value={decodeHtmlEntities(channel_name)}
|
||||
onInput={(e, val) => act('set_channel_name', { val: val })}
|
||||
onChange={(val) => act('set_channel_name', { val: val })}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Description">
|
||||
<Input
|
||||
fluid
|
||||
updateOnPropsChange
|
||||
expensive
|
||||
value={decodeHtmlEntities(msg)}
|
||||
onInput={(e, val) => act('set_wanted_desc', { val: val })}
|
||||
onChange={(val) => act('set_wanted_desc', { val: val })}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Attach Photo">
|
||||
|
||||
@@ -397,7 +397,7 @@ const NtosEmailClientNewMessage = (props) => {
|
||||
<Input
|
||||
fluid
|
||||
value={msg_title!}
|
||||
onInput={(e, val: string) => act('edit_title', { val: val })}
|
||||
onChange={(val: string) => act('edit_title', { val: val })}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Recipient" verticalAlign="top">
|
||||
@@ -406,9 +406,7 @@ const NtosEmailClientNewMessage = (props) => {
|
||||
<Input
|
||||
fluid
|
||||
value={msg_recipient!}
|
||||
onInput={(e, val: string) =>
|
||||
act('edit_recipient', { val: val })
|
||||
}
|
||||
onChange={(val: string) => act('edit_recipient', { val: val })}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
@@ -498,14 +496,14 @@ const NtosEmailClientLogin = (props) => {
|
||||
<Input
|
||||
fluid
|
||||
value={stored_login!}
|
||||
onInput={(e, val: string) => act('edit_login', { val: val })}
|
||||
onChange={(val: string) => act('edit_login', { val: val })}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Password">
|
||||
<Input
|
||||
fluid
|
||||
value={stored_password!}
|
||||
onInput={(e, val: string) => act('edit_password', { val: val })}
|
||||
onChange={(val: string) => act('edit_password', { val: val })}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
|
||||
@@ -121,7 +121,7 @@ export const NtosNetChat = (props) => {
|
||||
fluid
|
||||
selfClear
|
||||
mt={1}
|
||||
onEnter={(e, value) =>
|
||||
onEnter={(value) =>
|
||||
act('PRG_speak', {
|
||||
message: value,
|
||||
})
|
||||
|
||||
@@ -9,32 +9,37 @@ import {
|
||||
Stack,
|
||||
} from 'tgui-core/components';
|
||||
import { isEscape, KEY } from 'tgui-core/keys';
|
||||
import type { BooleanLike } from 'tgui-core/react';
|
||||
|
||||
import { InputButtons } from './common/InputButtons';
|
||||
import { Loader } from './common/Loader';
|
||||
|
||||
type NumberInputData = {
|
||||
type Data = {
|
||||
init_value: number;
|
||||
large_buttons: boolean;
|
||||
max_value: number | null;
|
||||
large_buttons: BooleanLike;
|
||||
max_value: number;
|
||||
message: string;
|
||||
min_value: number | null;
|
||||
min_value: number;
|
||||
round_value: BooleanLike;
|
||||
timeout: number;
|
||||
title: string;
|
||||
round_value: boolean;
|
||||
};
|
||||
|
||||
export const NumberInputModal = (props) => {
|
||||
const { act, data } = useBackend<NumberInputData>();
|
||||
const { init_value, large_buttons, message = '', timeout, title } = data;
|
||||
const [input, setInput] = useState(init_value);
|
||||
export function NumberInputModal(props) {
|
||||
const { act, data } = useBackend<Data>();
|
||||
const {
|
||||
init_value,
|
||||
large_buttons,
|
||||
max_value = 10000,
|
||||
message = '',
|
||||
min_value = 0,
|
||||
round_value,
|
||||
timeout,
|
||||
title,
|
||||
} = data;
|
||||
|
||||
const setValue = (value: number) => {
|
||||
if (value === input) {
|
||||
return;
|
||||
}
|
||||
setInput(value);
|
||||
};
|
||||
const [value, setValue] = useState(init_value);
|
||||
const [isValid, setIsValid] = useState(true);
|
||||
|
||||
// Dynamically changes the window height based on the message.
|
||||
const windowHeight =
|
||||
@@ -42,88 +47,89 @@ export const NumberInputModal = (props) => {
|
||||
(message.length > 30 ? Math.ceil(message.length / 3) : 0) +
|
||||
(message.length && large_buttons ? 5 : 0);
|
||||
|
||||
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||
if (event.key === KEY.Enter && isValid) {
|
||||
act('submit', { entry: value });
|
||||
}
|
||||
if (isEscape(event.key)) {
|
||||
act('cancel');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Window title={title} width={270} height={windowHeight}>
|
||||
{timeout && <Loader value={timeout} />}
|
||||
<Window.Content
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === KEY.Enter) {
|
||||
act('submit', { entry: input });
|
||||
}
|
||||
if (isEscape(event.key)) {
|
||||
act('cancel');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Window.Content onKeyDown={handleKeyDown}>
|
||||
<Section fill>
|
||||
<Stack fill vertical>
|
||||
<Stack.Item grow>
|
||||
<Box color="label">{message}</Box>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<InputArea
|
||||
input={input}
|
||||
onClick={setValue}
|
||||
onChange={setValue}
|
||||
onBlur={setValue}
|
||||
/>
|
||||
<Stack fill>
|
||||
<Stack.Item>
|
||||
<Button
|
||||
disabled={value === min_value}
|
||||
icon="angle-double-left"
|
||||
onClick={() => setValue(min_value ?? 0)}
|
||||
tooltip={min_value ? `Min (${min_value})` : 'Min'}
|
||||
/>
|
||||
</Stack.Item>
|
||||
|
||||
<Stack.Item>
|
||||
<Button
|
||||
icon="angle-down"
|
||||
disabled={value <= min_value}
|
||||
onClick={() => setValue((value) => value - 1)}
|
||||
/>
|
||||
</Stack.Item>
|
||||
|
||||
<Stack.Item grow>
|
||||
<RestrictedInput
|
||||
autoFocus
|
||||
autoSelect
|
||||
fluid
|
||||
allowFloats={!round_value}
|
||||
minValue={min_value}
|
||||
maxValue={max_value}
|
||||
onChange={setValue}
|
||||
onValidationChange={setIsValid}
|
||||
value={value}
|
||||
/>
|
||||
</Stack.Item>
|
||||
|
||||
<Stack.Item>
|
||||
<Button
|
||||
icon="angle-up"
|
||||
disabled={value >= max_value}
|
||||
onClick={() => setValue((value) => value + 1)}
|
||||
/>
|
||||
</Stack.Item>
|
||||
|
||||
<Stack.Item>
|
||||
<Button
|
||||
disabled={value === max_value}
|
||||
icon="angle-double-right"
|
||||
onClick={() => setValue(max_value ?? 10000)}
|
||||
tooltip={max_value ? `Max (${max_value})` : 'Max'}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Button
|
||||
disabled={value === init_value}
|
||||
icon="redo"
|
||||
onClick={() => setValue(init_value ?? 0)}
|
||||
tooltip={init_value ? `Reset (${init_value})` : 'Reset'}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<InputButtons input={input} />
|
||||
<InputButtons input={value} disabled={!isValid} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Section>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
|
||||
/** Gets the user input and invalidates if there's a constraint. */
|
||||
const InputArea = (props) => {
|
||||
const { act, data } = useBackend<NumberInputData>();
|
||||
const { min_value, max_value, init_value, round_value } = data;
|
||||
const { input, onClick, onChange, onBlur } = props;
|
||||
|
||||
return (
|
||||
<Stack fill>
|
||||
<Stack.Item>
|
||||
<Button
|
||||
disabled={input === min_value || min_value === -Infinity}
|
||||
icon={min_value === -Infinity ? 'infinity' : 'angle-double-left'}
|
||||
onClick={() => onClick(min_value)}
|
||||
tooltip={min_value ? `Min (${min_value})` : 'Min'}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<RestrictedInput
|
||||
autoFocus
|
||||
autoSelect
|
||||
fluid
|
||||
allowFloats={!round_value}
|
||||
minValue={min_value}
|
||||
maxValue={max_value}
|
||||
onChange={(_, value) => onChange(value)}
|
||||
onBlur={(_, value) => onBlur(value)}
|
||||
onEnter={(_, value) => act('submit', { entry: value })}
|
||||
value={input}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Button
|
||||
disabled={input === max_value || max_value === Infinity}
|
||||
icon={max_value === Infinity ? 'infinity' : 'angle-double-right'}
|
||||
onClick={() => onClick(max_value)}
|
||||
tooltip={max_value ? `Max (${max_value})` : 'Max'}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Button
|
||||
disabled={input === init_value}
|
||||
icon="redo"
|
||||
onClick={() => onClick(init_value)}
|
||||
tooltip={init_value ? `Reset (${init_value})` : 'Reset'}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ export const PersonalCrafting = (props) => {
|
||||
value={searchText}
|
||||
mb={1}
|
||||
placeholder="Search for recipes..."
|
||||
onInput={(e, value: string) => setSearchText(value)}
|
||||
onChange={(value: string) => setSearchText(value)}
|
||||
/>
|
||||
<Stack>
|
||||
<Stack.Item>
|
||||
|
||||
@@ -132,12 +132,7 @@ export const GamePreferencesPage = (props) => {
|
||||
)}
|
||||
<Box position="absolute" right={4} top={4} style={{ zIndex: '100' }}>
|
||||
{searchVisible && (
|
||||
<Input
|
||||
width={16}
|
||||
value={search}
|
||||
onInput={(e, val) => setSearch(val)}
|
||||
onChange={(e, val) => setSearch(val)}
|
||||
/>
|
||||
<Input width={16} value={search} onChange={(val) => setSearch(val)} />
|
||||
)}
|
||||
<Button
|
||||
selected={searchVisible}
|
||||
|
||||
@@ -265,7 +265,7 @@ export const FeatureShortTextInput = (
|
||||
width="100%"
|
||||
value={props.value}
|
||||
maxLength={props.serverData.maximum_length}
|
||||
onChange={(_, value) => props.handleSetValue(value)}
|
||||
onChange={(value) => props.handleSetValue(value)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ export const WikiSearchList = (props: {
|
||||
fluid
|
||||
value={searchText}
|
||||
placeholder={'Search for ' + title + '...'}
|
||||
onInput={(e, value: string) => onSearchText(value)}
|
||||
onChange={(value: string) => onSearchText(value)}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Divider />
|
||||
|
||||
@@ -9,7 +9,7 @@ export const LayerSection = (props) => {
|
||||
const { category: rootCategoryIndex, piping_layer, pipe_layers } = data;
|
||||
const previews = data.preview_rows.flatMap((row) => row.previews);
|
||||
return (
|
||||
<Section fill width={7.5}>
|
||||
<Section fill width="100px">
|
||||
{rootCategoryIndex === 0 && (
|
||||
<Stack vertical mb={1}>
|
||||
{Object.keys(pipe_layers).map((layer) => (
|
||||
@@ -28,7 +28,7 @@ export const LayerSection = (props) => {
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
<Box width="120px">
|
||||
<Stack wrap mt="30px">
|
||||
{previews.map((preview) => (
|
||||
<Button
|
||||
ml={0}
|
||||
@@ -58,7 +58,7 @@ export const LayerSection = (props) => {
|
||||
/>
|
||||
</Button>
|
||||
))}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -198,10 +198,10 @@ const BuildTab = (props: {
|
||||
>
|
||||
<Input
|
||||
fluid
|
||||
updateOnPropsChange
|
||||
expensive
|
||||
placeholder="Search for..."
|
||||
value={data.search}
|
||||
onInput={(e, v: string) => act('search', { search: v })}
|
||||
onChange={(v: string) => act('search', { search: v })}
|
||||
mb={1}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
@@ -33,10 +33,10 @@ export const DesignList = (props) => {
|
||||
>
|
||||
<Input
|
||||
fluid
|
||||
updateOnPropsChange
|
||||
expensive
|
||||
placeholder="Search for..."
|
||||
value={data.search}
|
||||
onInput={(e, v) => act('search', { search: v })}
|
||||
onChange={(v) => act('search', { search: v })}
|
||||
mb={1}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
@@ -41,10 +41,9 @@ export const IconSection = (props: {
|
||||
<Stack.Item grow>
|
||||
<Tooltip content="Adjust your name">
|
||||
<Input
|
||||
updateOnPropsChange
|
||||
fluid
|
||||
value={robotName}
|
||||
onChange={(e, value) => {
|
||||
onChange={(value) => {
|
||||
act('rename', { value });
|
||||
setRobotName(value);
|
||||
}}
|
||||
|
||||
@@ -26,7 +26,7 @@ export const ModuleSection = (props: {
|
||||
fluid
|
||||
value={searchText}
|
||||
placeholder="Search for modules..."
|
||||
onInput={(e, value: string) => setSearchText(value)}
|
||||
onChange={(value: string) => setSearchText(value)}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
|
||||
@@ -64,7 +64,7 @@ export const SpriteSection = (props: {
|
||||
fluid
|
||||
value={searchText}
|
||||
placeholder="Search for sprites..."
|
||||
onInput={(e, value: string) => setSearchText(value)}
|
||||
onChange={(value: string) => setSearchText(value)}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
|
||||
@@ -208,7 +208,7 @@ const Status = (props) => {
|
||||
width="5em"
|
||||
monospace
|
||||
value={light_color}
|
||||
onChange={(e, value: string) => act('set_light_col', { value })}
|
||||
onChange={(value: string) => act('set_light_col', { value })}
|
||||
/>
|
||||
</Box>
|
||||
</LabeledList.Item>
|
||||
|
||||
@@ -11,7 +11,7 @@ export const SecurityRecordsList = (props) => {
|
||||
<Input
|
||||
fluid
|
||||
placeholder="Search by Name, DNA, or ID"
|
||||
onChange={(e, value: string) => act('search', { t1: value })}
|
||||
onChange={(value: string) => act('search', { t1: value })}
|
||||
/>
|
||||
<Box mt="0.5rem">
|
||||
{records!.map((record, i) => (
|
||||
|
||||
@@ -115,7 +115,7 @@ const SheetSearch = (props: {
|
||||
placeholder="Search by item name.."
|
||||
value={searchText}
|
||||
width="100%"
|
||||
onInput={(_e, value) => onSearchText(value)}
|
||||
onChange={(value) => onSearchText(value)}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item basis="30%">
|
||||
|
||||
@@ -52,7 +52,7 @@ export const SupplyConsoleMenuOrder = (props) => {
|
||||
fluid
|
||||
placeholder={'Search for category...'}
|
||||
value={searchCategory}
|
||||
onInput={(e, val) => setSearchCategory(val)}
|
||||
onChange={(val) => setSearchCategory(val)}
|
||||
/>
|
||||
<Divider />
|
||||
<Section scrollable fill>
|
||||
@@ -75,7 +75,7 @@ export const SupplyConsoleMenuOrder = (props) => {
|
||||
fluid
|
||||
placeholder={'Search for pack...'}
|
||||
value={searchCategory}
|
||||
onInput={(e, val) => setSearchContent(val)}
|
||||
onChange={(val) => setSearchContent(val)}
|
||||
/>
|
||||
<Divider />
|
||||
<Section scrollable fill>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { type KeyboardEvent, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useBackend } from 'tgui/backend';
|
||||
import { Window } from 'tgui/layouts';
|
||||
import { Box, Section, Stack, TextArea } from 'tgui-core/components';
|
||||
import { isEscape, KEY } from 'tgui-core/keys';
|
||||
import { isEscape } from 'tgui-core/keys';
|
||||
import { KEY } from 'tgui-core/keys';
|
||||
|
||||
import { InputButtons } from './common/InputButtons';
|
||||
import { Loader } from './common/Loader';
|
||||
@@ -22,7 +23,7 @@ export const sanitizeMultiline = (toSanitize: string) => {
|
||||
};
|
||||
|
||||
export const removeAllSkiplines = (toSanitize: string) => {
|
||||
return toSanitize.replace(/[\r\n]+/, ' ');
|
||||
return toSanitize.replace(/[\r\n]+/, '');
|
||||
};
|
||||
|
||||
export const TextInputModal = (props) => {
|
||||
@@ -38,6 +39,7 @@ export const TextInputModal = (props) => {
|
||||
} = data;
|
||||
|
||||
const [input, setInput] = useState(placeholder || '');
|
||||
|
||||
const onType = (value: string) => {
|
||||
if (value === input) {
|
||||
return;
|
||||
@@ -56,34 +58,41 @@ export const TextInputModal = (props) => {
|
||||
(visualMultiline ? 75 : 0) +
|
||||
(message.length && large_buttons ? 5 : 0);
|
||||
|
||||
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||
if (event.key === KEY.Enter && (!visualMultiline || !event.shiftKey)) {
|
||||
act('submit', { entry: input });
|
||||
}
|
||||
if (isEscape(event.key)) {
|
||||
act('cancel');
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Window title={title} width={325} height={windowHeight}>
|
||||
{timeout && <Loader value={timeout} />}
|
||||
<Window.Content
|
||||
onKeyDown={(event) => {
|
||||
if (
|
||||
event.key === KEY.Enter &&
|
||||
(!visualMultiline || !event.shiftKey)
|
||||
) {
|
||||
act('submit', { entry: input });
|
||||
}
|
||||
if (isEscape(event.key)) {
|
||||
act('cancel');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Window.Content onKeyDown={handleKeyDown}>
|
||||
<Section fill>
|
||||
<Stack fill vertical>
|
||||
<Stack.Item>
|
||||
<Box color="label">{message}</Box>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<InputArea key={title} input={input} onType={onType} />
|
||||
<TextArea
|
||||
autoFocus
|
||||
autoSelect
|
||||
fluid
|
||||
userMarkup={{ u: '_', i: '|', b: '+' }}
|
||||
height={multiline || input.length >= 30 ? '100%' : '1.8rem'}
|
||||
maxLength={max_length}
|
||||
onEscape={() => act('cancel')}
|
||||
onChange={onType}
|
||||
placeholder="Type something..."
|
||||
value={input}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<InputButtons
|
||||
input={input}
|
||||
message={`${input.length}/${max_length}`}
|
||||
message={`${input.length}/${max_length || '∞'}`}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
@@ -92,37 +101,3 @@ export const TextInputModal = (props) => {
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
|
||||
/** Gets the user input and invalidates if there's a constraint. */
|
||||
const InputArea = (props: {
|
||||
input: string;
|
||||
onType: (value: string) => void;
|
||||
}) => {
|
||||
const { act, data } = useBackend<TextInputData>();
|
||||
const { max_length, multiline } = data;
|
||||
const { input, onType } = props;
|
||||
|
||||
const visualMultiline = multiline || input.length >= 30;
|
||||
|
||||
return (
|
||||
<TextArea
|
||||
autoFocus
|
||||
userMarkup={{ u: '_', i: '|', b: '+' }}
|
||||
autoSelect
|
||||
height={multiline || input.length >= 30 ? '100%' : '1.8rem'}
|
||||
maxLength={max_length}
|
||||
onEscape={() => act('cancel')}
|
||||
onEnter={(event: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (visualMultiline && event.shiftKey) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
act('submit', { entry: input });
|
||||
}}
|
||||
onChange={(_, value) => onType(value)}
|
||||
onInput={(_, value) => onType(value)}
|
||||
placeholder="Type something..."
|
||||
value={input}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -56,7 +56,7 @@ export const GenericUplink = (props: {
|
||||
<Input
|
||||
autoFocus
|
||||
value={searchText}
|
||||
onInput={(e, value: string) => setSearchText(value)}
|
||||
onChange={(value: string) => setSearchText(value)}
|
||||
mx={1}
|
||||
/>
|
||||
</Stack.Item>
|
||||
|
||||
@@ -144,7 +144,7 @@ export const VendingProducts = (props: {
|
||||
<Input
|
||||
fluid
|
||||
placeholder="Search for products..."
|
||||
onInput={(e, value: string) => props.onSearch(value)}
|
||||
onChange={(value: string) => props.onSearch(value)}
|
||||
/>
|
||||
</Table.Cell>
|
||||
</Table>
|
||||
|
||||
@@ -131,7 +131,7 @@ export const ComplexModal = (props) => {
|
||||
my="0.5rem"
|
||||
autoFocus
|
||||
autoSelect
|
||||
onChange={(_e, val) => {
|
||||
onChange={(val) => {
|
||||
curValue = val;
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -7,20 +7,41 @@ type InputButtonsData = {
|
||||
};
|
||||
|
||||
type InputButtonsProps = {
|
||||
input: string | number | string[];
|
||||
message?: string;
|
||||
};
|
||||
input: string | number | string[] | [string, number][];
|
||||
} & Partial<{
|
||||
on_submit: () => void;
|
||||
on_cancel: () => void;
|
||||
message: string;
|
||||
/** Disables the submit button */
|
||||
disabled: boolean;
|
||||
}>;
|
||||
|
||||
export const InputButtons = (props: InputButtonsProps) => {
|
||||
const { act, data } = useBackend<InputButtonsData>();
|
||||
const { large_buttons, swapped_buttons } = data;
|
||||
const { input, message } = props;
|
||||
const { input, message, on_submit, on_cancel, disabled } = props;
|
||||
|
||||
let on_submit_actual = on_submit;
|
||||
if (!on_submit_actual) {
|
||||
on_submit_actual = () => {
|
||||
act('submit', { entry: input });
|
||||
};
|
||||
}
|
||||
|
||||
let on_cancel_actual = on_cancel;
|
||||
if (!on_cancel_actual) {
|
||||
on_cancel_actual = () => {
|
||||
act('cancel');
|
||||
};
|
||||
}
|
||||
|
||||
const submitButton = (
|
||||
<Button
|
||||
color="good"
|
||||
disabled={disabled}
|
||||
fluid={!!large_buttons}
|
||||
height={!!large_buttons && 2}
|
||||
onClick={() => act('submit', { entry: input })}
|
||||
onClick={on_submit_actual}
|
||||
m={0.5}
|
||||
pl={2}
|
||||
pr={2}
|
||||
@@ -37,7 +58,7 @@ export const InputButtons = (props: InputButtonsProps) => {
|
||||
color="bad"
|
||||
fluid={!!large_buttons}
|
||||
height={!!large_buttons && 2}
|
||||
onClick={() => act('cancel')}
|
||||
onClick={on_cancel_actual}
|
||||
m={0.5}
|
||||
pl={2}
|
||||
pr={2}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { afterEach, beforeEach, describe, it, vi } from 'vitest';
|
||||
|
||||
import { captureExternalLinks } from './links';
|
||||
|
||||
describe('captureExternalLinks', () => {
|
||||
@@ -5,7 +7,7 @@ describe('captureExternalLinks', () => {
|
||||
let clickHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
addEventListenerSpy = jest.spyOn(document, 'addEventListener');
|
||||
addEventListenerSpy = vi.spyOn(document, 'addEventListener');
|
||||
captureExternalLinks();
|
||||
clickHandler = addEventListenerSpy.mock.calls[0][1];
|
||||
});
|
||||
@@ -14,24 +16,26 @@ describe('captureExternalLinks', () => {
|
||||
addEventListenerSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should subscribe to document clicks', () => {
|
||||
it('should subscribe to document clicks', ({ expect }) => {
|
||||
expect(addEventListenerSpy).toHaveBeenCalledWith(
|
||||
'click',
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('should preventDefault and send a message when a non-BYOND external link is clicked', () => {
|
||||
it('should preventDefault and send a message when a non-BYOND external link is clicked', ({
|
||||
expect,
|
||||
}) => {
|
||||
const externalLink = {
|
||||
tagName: 'A',
|
||||
getAttribute: () => 'https://example.com',
|
||||
parentElement: document.body,
|
||||
};
|
||||
const byond = { sendMessage: jest.fn() };
|
||||
const byond = { sendMessage: vi.fn() };
|
||||
// @ts-ignore
|
||||
global.Byond = byond;
|
||||
|
||||
const evt = { target: externalLink, preventDefault: jest.fn() };
|
||||
const evt = { target: externalLink, preventDefault: vi.fn() };
|
||||
clickHandler(evt);
|
||||
|
||||
expect(evt.preventDefault).toHaveBeenCalled();
|
||||
@@ -41,34 +45,36 @@ describe('captureExternalLinks', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should not preventDefault or send a message when a BYOND link is clicked', () => {
|
||||
it('should not preventDefault or send a message when a BYOND link is clicked', ({
|
||||
expect,
|
||||
}) => {
|
||||
const byondLink = {
|
||||
tagName: 'A',
|
||||
getAttribute: () => 'byond://server-address',
|
||||
parentElement: document.body,
|
||||
};
|
||||
const byond = { sendMessage: jest.fn() };
|
||||
const byond = { sendMessage: vi.fn() };
|
||||
// @ts-ignore
|
||||
global.Byond = byond;
|
||||
|
||||
const evt = { target: byondLink, preventDefault: jest.fn() };
|
||||
const evt = { target: byondLink, preventDefault: vi.fn() };
|
||||
clickHandler(evt);
|
||||
|
||||
expect(evt.preventDefault).not.toHaveBeenCalled();
|
||||
expect(byond.sendMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should add https:// to www links', () => {
|
||||
it('should add https:// to www links', ({ expect }) => {
|
||||
const wwwLink = {
|
||||
tagName: 'A',
|
||||
getAttribute: () => 'www.example.com',
|
||||
parentElement: document.body,
|
||||
};
|
||||
const byond = { sendMessage: jest.fn() };
|
||||
const byond = { sendMessage: vi.fn() };
|
||||
// @ts-ignore
|
||||
global.Byond = byond;
|
||||
|
||||
const evt = { target: wwwLink, preventDefault: jest.fn() };
|
||||
const evt = { target: wwwLink, preventDefault: vi.fn() };
|
||||
clickHandler(evt);
|
||||
|
||||
expect(byond.sendMessage).toHaveBeenCalledWith({
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
"name": "tgui",
|
||||
"version": "6.0.0",
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@types/marked": "4.3.2",
|
||||
"@types/react": "^19.1.0",
|
||||
"@floating-ui/react": "^0.27.5",
|
||||
"common": "workspace:*",
|
||||
"dateformat": "^5.0.3",
|
||||
"dompurify": "^3.2.4",
|
||||
@@ -16,8 +14,13 @@
|
||||
"marked": "^4.3.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-popper": "^2.3.0",
|
||||
"tgui-core": "^2.0.5",
|
||||
"tgui-core": "^3.0.4",
|
||||
"tgui-dev-server": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/marked": "4.3.2",
|
||||
"@types/react": "^19.1.0",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"vitest": "^3.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,65 +11,51 @@ let initialRender: string | boolean = true;
|
||||
let suspended = false;
|
||||
|
||||
// These functions are used purely for profiling.
|
||||
export const resumeRenderer = () => {
|
||||
export function resumeRenderer() {
|
||||
initialRender = initialRender || 'resumed';
|
||||
suspended = false;
|
||||
};
|
||||
}
|
||||
|
||||
export const suspendRenderer = () => {
|
||||
export function suspendRenderer() {
|
||||
suspended = true;
|
||||
};
|
||||
|
||||
type CreateRenderer = <T extends unknown[] = [unknown]>(
|
||||
getVNode?: (...args: T) => ReactNode,
|
||||
) => (...args: T) => void;
|
||||
}
|
||||
|
||||
enum Render {
|
||||
Start = 'render/start',
|
||||
Finish = 'render/finish',
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
export const createRenderer: CreateRenderer = (getVNode) => (...args) => {
|
||||
export function render(component: ReactNode) {
|
||||
perf.mark(Render.Start);
|
||||
// Start rendering
|
||||
if (!reactRoot) {
|
||||
const element = document.getElementById('react-root');
|
||||
reactRoot = createRoot(element!);
|
||||
}
|
||||
if (getVNode) {
|
||||
reactRoot.render(getVNode(...args));
|
||||
}
|
||||
else {
|
||||
reactRoot.render(args[0] as any);
|
||||
}
|
||||
|
||||
reactRoot.render(component);
|
||||
|
||||
perf.mark(Render.Finish);
|
||||
if (suspended) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Report rendering time
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (initialRender === 'resumed') {
|
||||
logger.log('rendered in',
|
||||
perf.measure(Render.Start, Render.Finish));
|
||||
}
|
||||
else if (initialRender) {
|
||||
logger.log('rendered in', perf.measure(Render.Start, Render.Finish));
|
||||
} else if (initialRender) {
|
||||
logger.debug('serving from:', location.href);
|
||||
logger.debug('bundle entered in',
|
||||
perf.measure('inception', 'init'));
|
||||
logger.debug('initialized in',
|
||||
perf.measure('init', Render.Start));
|
||||
logger.log('rendered in',
|
||||
perf.measure(Render.Start, Render.Finish));
|
||||
logger.log('fully loaded in',
|
||||
perf.measure('inception', Render.Finish));
|
||||
}
|
||||
else {
|
||||
logger.debug('rendered in',
|
||||
perf.measure(Render.Start, Render.Finish));
|
||||
logger.debug('bundle entered in', perf.measure('inception', 'init'));
|
||||
logger.debug('initialized in', perf.measure('init', Render.Start));
|
||||
logger.log('rendered in', perf.measure(Render.Start, Render.Finish));
|
||||
logger.log('fully loaded in', perf.measure('inception', Render.Finish));
|
||||
} else {
|
||||
logger.debug('rendered in', perf.measure(Render.Start, Render.Finish));
|
||||
}
|
||||
}
|
||||
|
||||
if (initialRender) {
|
||||
initialRender = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { useBackend } from './backend';
|
||||
import { useDebug } from './debug';
|
||||
import { LoadingScreen } from './interfaces/common/LoadingToolbox';
|
||||
import { LoadingScreen } from './interfaces/common/LoadingScreen';
|
||||
import { Window } from './layouts';
|
||||
|
||||
const requireInterface = require.context('./interfaces');
|
||||
@@ -32,16 +32,16 @@ export const routingError =
|
||||
};
|
||||
|
||||
// Displays an empty Window with scrollable content
|
||||
const SuspendedWindow = () => {
|
||||
function SuspendedWindow() {
|
||||
return (
|
||||
<Window>
|
||||
<Window.Content scrollable />
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// Displays a loading screen with a spinning icon
|
||||
const RefreshingWindow = () => {
|
||||
function RefreshingWindow() {
|
||||
return (
|
||||
<Window title="Loading">
|
||||
<Window.Content>
|
||||
@@ -49,10 +49,10 @@ const RefreshingWindow = () => {
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// Get the component for the current route
|
||||
export const getRoutedComponent = () => {
|
||||
export function getRoutedComponent() {
|
||||
const { suspended, config } = useBackend();
|
||||
const { kitchenSink = false } = useDebug();
|
||||
|
||||
@@ -62,6 +62,7 @@ export const getRoutedComponent = () => {
|
||||
if (config?.refreshing) {
|
||||
return RefreshingWindow;
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
// Show a kitchen sink
|
||||
if (kitchenSink) {
|
||||
@@ -69,13 +70,14 @@ export const getRoutedComponent = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const name = config?.interface;
|
||||
const name = config?.interface?.name;
|
||||
const interfacePathBuilders = [
|
||||
(name: string) => `./${name}.tsx`,
|
||||
(name: string) => `./${name}.jsx`,
|
||||
(name: string) => `./${name}/index.tsx`,
|
||||
(name: string) => `./${name}/index.jsx`,
|
||||
];
|
||||
|
||||
let esModule;
|
||||
while (!esModule && interfacePathBuilders.length > 0) {
|
||||
const interfacePathBuilder = interfacePathBuilders.shift()!;
|
||||
@@ -88,12 +90,15 @@ export const getRoutedComponent = () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!esModule) {
|
||||
return routingError('notFound', name);
|
||||
}
|
||||
|
||||
const Component = esModule[name];
|
||||
if (!Component) {
|
||||
return routingError('missingExport', name);
|
||||
}
|
||||
|
||||
return Component;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { describe, it } from 'vitest';
|
||||
|
||||
import { sanitizeText } from './sanitize';
|
||||
|
||||
describe('sanitizeText', () => {
|
||||
it('should sanitize basic HTML input', () => {
|
||||
it('should sanitize basic HTML input', ({ expect }) => {
|
||||
const input = '<b>Hello, world!</b><script>alert("hack")</script>';
|
||||
const expected = '<b>Hello, world!</b>';
|
||||
const result = sanitizeText(input);
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it('should sanitize advanced HTML input when advHtml flag is true', () => {
|
||||
it('should sanitize advanced HTML input when advHtml flag is true', ({
|
||||
expect,
|
||||
}) => {
|
||||
const input =
|
||||
'<b>Hello, world!</b><iframe src="https://example.com"></iframe>';
|
||||
const expected = '<b>Hello, world!</b>';
|
||||
@@ -16,7 +20,9 @@ describe('sanitizeText', () => {
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it('should allow specific HTML tags when tags array is provided', () => {
|
||||
it('should allow specific HTML tags when tags array is provided', ({
|
||||
expect,
|
||||
}) => {
|
||||
const input = '<b>Hello, world!</b><span>Goodbye, world!</span>';
|
||||
const tags = ['b'];
|
||||
const expected = '<b>Hello, world!</b>Goodbye, world!';
|
||||
@@ -24,7 +30,9 @@ describe('sanitizeText', () => {
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it('should allow advanced HTML tags when advTags array is provided and advHtml flag is true', () => {
|
||||
it('should allow advanced HTML tags when advTags array is provided and advHtml flag is true', ({
|
||||
expect,
|
||||
}) => {
|
||||
const input =
|
||||
'<b>Hello, world!</b><iframe src="https://example.com"></iframe>';
|
||||
const advTags = ['iframe'];
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user