[MIRROR] tgui 516 (#10158)

Co-authored-by: Kashargul <144968721+Kashargul@users.noreply.github.com>
This commit is contained in:
CHOMPStation2StaffMirrorBot
2025-02-14 06:35:23 -07:00
committed by GitHub
parent 021ac2b32d
commit bb759b294c
422 changed files with 1193 additions and 1595 deletions

View File

@@ -5,13 +5,6 @@
"tgui.bundle.css" = file("tgui/public/tgui.bundle.css"), "tgui.bundle.css" = file("tgui/public/tgui.bundle.css"),
) )
/datum/asset/simple/tgui_edge
keep_local_name = TRUE
assets = list(
"tgui.bundle.edge.js" = file("tgui/public/tgui.bundle.edge.js"),
"tgui.bundle.edge.css" = file("tgui/public/tgui.bundle.edge.css"),
)
/datum/asset/simple/tgui_panel /datum/asset/simple/tgui_panel
keep_local_name = TRUE keep_local_name = TRUE
assets = list( assets = list(

View File

@@ -226,9 +226,7 @@
//CONNECT// //CONNECT//
/////////// ///////////
/client/New(TopicData) /client/New(TopicData)
// TODO: Remove version check with 516 winset(src, null, "browser-options=[DEFAULT_CLIENT_BROWSER_OPTIONS]")
if(byond_version >= 516) // Enable 516 compat browser storage mechanisms
winset(src, null, "browser-options=[DEFAULT_CLIENT_BROWSER_OPTIONS]")
TopicData = null //Prevent calls to client.Topic from connect TopicData = null //Prevent calls to client.Topic from connect
@@ -360,11 +358,9 @@
fully_created = TRUE fully_created = TRUE
attempt_auto_fit_viewport() attempt_auto_fit_viewport()
// TODO: Remove version check with 516 // Now that we're fully initialized, use our prefs
if(byond_version >= 516) if(prefs?.read_preference(/datum/preference/toggle/browser_dev_tools))
// Now that we're fully initialized, use our prefs winset(src, null, "browser-options=[DEFAULT_CLIENT_BROWSER_OPTIONS],devtools")
if(prefs?.read_preference(/datum/preference/toggle/browser_dev_tools))
winset(src, null, "browser-options=[DEFAULT_CLIENT_BROWSER_OPTIONS],devtools")
////////////// //////////////
//DISCONNECT// //DISCONNECT//
@@ -672,18 +668,6 @@
src << browse("<html>[message]</html>","window=dropmessage;size=480x360;can_close=1") src << browse("<html>[message]</html>","window=dropmessage;size=480x360;can_close=1")
qdel(src) qdel(src)
/// Keydown event in a tgui window this client has open. Has keycode passed to it.
/client/verb/TguiKeyDown(keycode as text)
set name = "TguiKeyDown"
set hidden = TRUE
return // stub
/// Keyup event in a tgui window this client has open. Has keycode passed to it.
/client/verb/TguiKeyUp(keycode as text) // Doesn't seem to currently fire?
set name = "TguiKeyUp"
set hidden = TRUE
return // stub
/client/verb/toggle_fullscreen() /client/verb/toggle_fullscreen()
set name = "Toggle Fullscreen" set name = "Toggle Fullscreen"
set category = "OOC.Client Settings" set category = "OOC.Client Settings"

View File

@@ -341,7 +341,7 @@ var/global/list/valid_bloodtypes = list("A+", "A-", "B+", "B-", "AB+", "AB-", "O
var/key = pref.rlimb_data[name] var/key = pref.rlimb_data[name]
if(!istext(key)) if(!istext(key))
log_debug("Bad rlimb_data for [key_name(pref.client)], [name] was set to [key]") log_debug("Bad rlimb_data for [key_name(pref.client)], [name] was set to [key]")
to_chat(user, span_warning("Error loading robot limb data for `[name]`, clearing pref.")) to_chat(usr, span_warning("Error loading robot limb data for `[name]`, clearing pref."))
pref.rlimb_data -= name pref.rlimb_data -= name
else else
R = LAZYACCESS(all_robolimbs, key) R = LAZYACCESS(all_robolimbs, key)
@@ -552,7 +552,7 @@ var/global/list/valid_bloodtypes = list("A+", "A-", "B+", "B-", "AB+", "AB-", "O
pref.set_biological_gender(mob_species.genders[1]) pref.set_biological_gender(mob_species.genders[1])
pref.custom_species = null pref.custom_species = null
//grab one of the valid hair styles for the newly chosen species //grab one of the valid hair styles for the newly chosen species
var/list/valid_hairstyles = pref.get_valid_hairstyles(user) var/list/valid_hairstyles = pref.get_valid_hairstyles()
if(valid_hairstyles.len) if(valid_hairstyles.len)
if(!(pref.h_style in valid_hairstyles)) if(!(pref.h_style in valid_hairstyles))
@@ -604,7 +604,7 @@ var/global/list/valid_bloodtypes = list("A+", "A-", "B+", "B-", "AB+", "AB-", "O
return TOPIC_REFRESH_UPDATE_PREVIEW return TOPIC_REFRESH_UPDATE_PREVIEW
else if(href_list["hair_style"]) else if(href_list["hair_style"])
var/list/valid_hairstyles = pref.get_valid_hairstyles(user) var/list/valid_hairstyles = pref.get_valid_hairstyles()
var/new_h_style = tgui_input_list(user, "Choose your character's hair style:", "Character Preference", valid_hairstyles, pref.h_style) var/new_h_style = tgui_input_list(user, "Choose your character's hair style:", "Character Preference", valid_hairstyles, pref.h_style)
if(new_h_style && CanUseTopic(user)) if(new_h_style && CanUseTopic(user))
@@ -629,7 +629,7 @@ var/global/list/valid_bloodtypes = list("A+", "A-", "B+", "B-", "AB+", "AB-", "O
else if(href_list["hair_style_left"]) else if(href_list["hair_style_left"])
var/H = href_list["hair_style_left"] var/H = href_list["hair_style_left"]
var/list/valid_hairstyles = pref.get_valid_hairstyles(user) var/list/valid_hairstyles = pref.get_valid_hairstyles()
var/start = valid_hairstyles.Find(H) var/start = valid_hairstyles.Find(H)
if(start != 1) //If we're not the beginning of the list, become the previous element. if(start != 1) //If we're not the beginning of the list, become the previous element.
@@ -640,7 +640,7 @@ var/global/list/valid_bloodtypes = list("A+", "A-", "B+", "B-", "AB+", "AB-", "O
else if(href_list["hair_style_right"]) else if(href_list["hair_style_right"])
var/H = href_list["hair_style_right"] var/H = href_list["hair_style_right"]
var/list/valid_hairstyles = pref.get_valid_hairstyles(user) var/list/valid_hairstyles = pref.get_valid_hairstyles()
var/start = valid_hairstyles.Find(H) var/start = valid_hairstyles.Find(H)
if(start != valid_hairstyles.len) //If we're not the end of the list, become the next element. if(start != valid_hairstyles.len) //If we're not the end of the list, become the next element.
@@ -1054,7 +1054,7 @@ var/global/list/valid_bloodtypes = list("A+", "A-", "B+", "B-", "AB+", "AB-", "O
return TOPIC_REFRESH_UPDATE_PREVIEW return TOPIC_REFRESH_UPDATE_PREVIEW
else if(href_list["ear_color2"]) else if(href_list["ear_color2"])
var/new_earc2 = tgui_color_picker(user, "Choose your character's ear colour:", "Character Preference", var/new_earc2 = tgui_color_picker(user, "Choose your character's secondary ear colour:", "Character Preference",
pref.read_preference(/datum/preference/color/human/ears_color2)) pref.read_preference(/datum/preference/color/human/ears_color2))
if(new_earc2) if(new_earc2)
pref.update_preference_by_type(/datum/preference/color/human/ears_color2, new_earc2) pref.update_preference_by_type(/datum/preference/color/human/ears_color2, new_earc2)

View File

@@ -81,7 +81,7 @@
savefile_identifier = PREFERENCE_PLAYER savefile_identifier = PREFERENCE_PLAYER
minimum = 1 minimum = 1
maximum = 5 maximum = 20
step = 1 step = 1
/datum/preference/numeric/tgui_say_height/create_default_value() /datum/preference/numeric/tgui_say_height/create_default_value()
@@ -90,6 +90,22 @@
/datum/preference/numeric/tgui_say_height/apply_to_client(client/client, value) /datum/preference/numeric/tgui_say_height/apply_to_client(client/client, value)
client.tgui_say?.load() client.tgui_say?.load()
/datum/preference/numeric/tgui_say_width
category = PREFERENCE_CATEGORY_GAME_PREFERENCES
savefile_key = "tgui_say_width"
savefile_identifier = PREFERENCE_PLAYER
minimum = 360
maximum = 800
step = 20
/datum/preference/numeric/tgui_say_width/create_default_value()
return 360
/datum/preference/numeric/tgui_say_width/apply_to_client(client/client, value)
client.tgui_say?.load()
/datum/preference/text/preset_colors /datum/preference/text/preset_colors
category = PREFERENCE_CATEGORY_MANUALLY_RENDERED category = PREFERENCE_CATEGORY_MANUALLY_RENDERED
savefile_identifier = PREFERENCE_PLAYER savefile_identifier = PREFERENCE_PLAYER

View File

@@ -25,7 +25,7 @@ var/global/list/MOVE_KEY_MAPPINGS = list(
// Validate input. Must be one (and only one) of the key codes) // Validate input. Must be one (and only one) of the key codes)
if(isnull(movekey) || (movekey & ~0xFFF) || (movekey & (movekey - 1))) if(isnull(movekey) || (movekey & ~0xFFF) || (movekey & (movekey - 1)))
log_debug("Client [ckey] sent an illegal movement key down: [movekeyName] ([movekey])") // og_debug("Client [ckey] sent an illegal movement key down: [movekeyName] ([movekey])") // We forward tgui keys nowadays
return return
// Record that we are now holding the key! // Record that we are now holding the key!
@@ -57,7 +57,7 @@ var/global/list/MOVE_KEY_MAPPINGS = list(
// Validate input. Must be one (and only one) of the key codes) // Validate input. Must be one (and only one) of the key codes)
if(isnull(movekey) || (movekey & ~0xFFF) || (movekey & (movekey - 1))) if(isnull(movekey) || (movekey & ~0xFFF) || (movekey & (movekey - 1)))
log_debug("Client [ckey] sent an illegal movement key up: [movekeyName] ([movekey])") // log_debug("Client [ckey] sent an illegal movement key up: [movekeyName] ([movekey])") // We forward tgui keys nowadays
return return
// Clear bit indicating we were holding the key // Clear bit indicating we were holding the key

View File

@@ -106,11 +106,8 @@
strict_mode = TRUE, strict_mode = TRUE,
fancy = user.read_preference(/datum/preference/toggle/tgui_fancy), fancy = user.read_preference(/datum/preference/toggle/tgui_fancy),
assets = list( assets = list(
// FIXME: Delete this when 516 is required! get_asset_datum(/datum/asset/simple/tgui),
user.client.byond_version >= 516 \ ))
? get_asset_datum(/datum/asset/simple/tgui_edge) \
: get_asset_datum(/datum/asset/simple/tgui),
))
else else
window.send_message("ping") window.send_message("ping")
send_assets() send_assets()

View File

@@ -18,7 +18,7 @@
else else
return return
// Client does NOT have tgui_input on: Returns regular input // Client does NOT have tgui_input on: Returns regular input
if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_mode) || user.client.byond_version < 516) // Todo, remove once virgo is on 516, but currently the TGUI interface is already programmed for edge if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_mode))
return input(user, message, title, default) as color|null return input(user, message, title, default) as color|null
var/datum/tgui_color_picker/picker = new(user, message, title, default, timeout, autofocus) var/datum/tgui_color_picker/picker = new(user, message, title, default, timeout, autofocus)
picker.tgui_interact(user) picker.tgui_interact(user)

View File

@@ -65,11 +65,13 @@
window_open = FALSE window_open = FALSE
var/minimumHeight = client?.prefs?.read_preference(/datum/preference/numeric/tgui_say_height) || 1 var/minimumHeight = client?.prefs?.read_preference(/datum/preference/numeric/tgui_say_height) || 1
winset(client, "tgui_say", "pos=410,400;size=360,[(minimumHeight * 20) + 10];is-visible=0;") var/minimumWidth = client?.prefs?.read_preference(/datum/preference/numeric/tgui_say_width) || 1
winset(client, "tgui_say", "pos=410,400;size=360,30;is-visible=0;")
window.send_message("props", list( window.send_message("props", list(
lightMode = client?.prefs?.read_preference(/datum/preference/toggle/tgui_say_light), lightMode = client?.prefs?.read_preference(/datum/preference/toggle/tgui_say_light),
minimumHeight = minimumHeight, minimumHeight = minimumHeight,
minimumWidth = minimumWidth,
maxLength = max_length, maxLength = max_length,
)) ))

View File

@@ -32,12 +32,9 @@
tgui_panel.initialize(force = TRUE) tgui_panel.initialize(force = TRUE)
// Force show the panel to see if there are any errors // Force show the panel to see if there are any errors
winset(src, "legacy_output_selector", "left=output_browser") winset(src, "legacy_output_selector", "left=output_browser")
// TODO: Remove version check with 516
if(byond_version >= 516) if(prefs?.read_preference(/datum/preference/toggle/browser_dev_tools))
if(prefs?.read_preference(/datum/preference/toggle/browser_dev_tools)) winset(src, null, "browser-options=[DEFAULT_CLIENT_BROWSER_OPTIONS],devtools")
winset(src, null, "browser-options=[DEFAULT_CLIENT_BROWSER_OPTIONS],devtools")
else
winset(src, null, "browser-options=[DEFAULT_CLIENT_BROWSER_OPTIONS]")
/client/verb/refresh_tgui() /client/verb/refresh_tgui()
set name = "Refresh TGUI" set name = "Refresh TGUI"

View File

@@ -111,7 +111,8 @@ Notes:
last_target = null last_target = null
/datum/tooltip/proc/do_hide() /datum/tooltip/proc/do_hide()
winshow(owner, control, FALSE) if(owner)
winshow(owner, control, FALSE)
/datum/tooltip/Destroy(force) /datum/tooltip/Destroy(force)
last_target = null last_target = null

View File

@@ -9,7 +9,7 @@
"scripts": { "scripts": {
"tgui:analyze": "webpack --analyze", "tgui:analyze": "webpack --analyze",
"tgui:bench": "webpack --env TGUI_BENCH=1 && node packages/tgui-bench/index.js", "tgui:bench": "webpack --env TGUI_BENCH=1 && node packages/tgui-bench/index.js",
"tgui:build": "BROWSERSLIST_IGNORE_OLD_DATA=true webpack && webpack --config ./webpack.config.edge.js", "tgui:build": "BROWSERSLIST_IGNORE_OLD_DATA=true webpack",
"tgui:dev": "node --experimental-modules packages/tgui-dev-server/index.js", "tgui:dev": "node --experimental-modules packages/tgui-dev-server/index.js",
"tgui:lint": "eslint packages --ext .js,.cjs,.ts,.tsx", "tgui:lint": "eslint packages --ext .js,.cjs,.ts,.tsx",
"tgui:prettier": "prettier --check .", "tgui:prettier": "prettier --check .",

View File

@@ -0,0 +1,68 @@
import {
Action,
applyMiddleware,
combineReducers,
createAction,
createStore,
Reducer,
} from './redux';
// Dummy Reducer
const counterReducer: Reducer<number, Action<string>> = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
};
// Dummy Middleware
const loggingMiddleware = (storeApi) => (next) => (action) => {
console.log('Middleware:', action);
return next(action);
};
// Dummy Action Creators
const increment = createAction('INCREMENT');
const decrement = createAction('DECREMENT');
describe('Redux implementation tests', () => {
test('createStore works', () => {
const store = createStore(counterReducer);
expect(store.getState()).toBe(0);
});
test('createStore with applyMiddleware works', () => {
const store = createStore(
counterReducer,
applyMiddleware(loggingMiddleware),
);
expect(store.getState()).toBe(0);
});
test('dispatch works', () => {
const store = createStore(counterReducer);
store.dispatch(increment());
expect(store.getState()).toBe(1);
store.dispatch(decrement());
expect(store.getState()).toBe(0);
});
test('combineReducers works', () => {
const rootReducer = combineReducers({
counter: counterReducer,
});
const store = createStore(rootReducer);
expect(store.getState()).toEqual({ counter: 0 });
});
test('createAction works', () => {
const incrementAction = increment();
expect(incrementAction).toEqual({ type: 'INCREMENT' });
const decrementAction = decrement();
expect(decrementAction).toEqual({ type: 'DECREMENT' });
});
});

View File

@@ -29,36 +29,6 @@ const testHubStorage = testGeneric(
() => window.hubStorage && window.hubStorage.getItem, () => window.hubStorage && window.hubStorage.getItem,
); );
// TODO: Remove with 516
// prettier-ignore
const testIndexedDb = testGeneric(() => (
(window.indexedDB || window.msIndexedDB)
&& (window.IDBTransaction || window.msIDBTransaction)
));
class MemoryBackend {
constructor() {
this.impl = IMPL_MEMORY;
this.store = {};
}
async get(key) {
return this.store[key];
}
async set(key, value) {
this.store[key] = value;
}
async remove(key) {
this.store[key] = undefined;
}
async clear() {
this.store = {};
}
}
class HubStorageBackend { class HubStorageBackend {
constructor() { constructor() {
this.impl = IMPL_HUB_STORAGE; this.impl = IMPL_HUB_STORAGE;
@@ -84,63 +54,6 @@ class HubStorageBackend {
} }
} }
class IndexedDbBackend {
// TODO: Remove with 516
constructor() {
this.impl = IMPL_INDEXED_DB;
/** @type {Promise<IDBDatabase>} */
this.dbPromise = new Promise((resolve, reject) => {
const indexedDB = window.indexedDB || window.msIndexedDB;
const req = indexedDB.open(INDEXED_DB_NAME, INDEXED_DB_VERSION);
req.onupgradeneeded = () => {
try {
req.result.createObjectStore(INDEXED_DB_STORE_NAME);
} catch (err) {
reject(new Error('Failed to upgrade IDB: ' + req.error));
}
};
req.onsuccess = () => resolve(req.result);
req.onerror = () => {
reject(new Error('Failed to open IDB: ' + req.error));
};
});
}
async getStore(mode) {
// prettier-ignore
return this.dbPromise.then((db) => db
.transaction(INDEXED_DB_STORE_NAME, mode)
.objectStore(INDEXED_DB_STORE_NAME));
}
async get(key) {
const store = await this.getStore(READ_ONLY);
return new Promise((resolve, reject) => {
const req = store.get(key);
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}
async set(key, value) {
// NOTE: We deliberately make this operation transactionless
const store = await this.getStore(READ_WRITE);
store.put(value, key);
}
async remove(key) {
// NOTE: We deliberately make this operation transactionless
const store = await this.getStore(READ_WRITE);
store.delete(key);
}
async clear() {
// NOTE: We deliberately make this operation transactionless
const store = await this.getStore(READ_WRITE);
store.clear();
}
}
/** /**
* Web Storage Proxy object, which selects the best backend available * Web Storage Proxy object, which selects the best backend available
* depending on the environment. * depending on the environment.
@@ -151,18 +64,6 @@ class StorageProxy {
if (!Byond.TRIDENT && testHubStorage()) { if (!Byond.TRIDENT && testHubStorage()) {
return new HubStorageBackend(); return new HubStorageBackend();
} }
// TODO: Remove with 516
if (testIndexedDb()) {
try {
const backend = new IndexedDbBackend();
await backend.dbPromise;
return backend;
} catch {}
}
console.warn(
'No supported storage backend found. Using in-memory storage.',
);
return new MemoryBackend();
})(); })();
} }

View File

@@ -1,51 +0,0 @@
/**
* N-dimensional vector manipulation functions.
*
* Vectors are plain number arrays, i.e. [x, y, z].
*
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { map, reduce, zip } from './collections';
const ADD = (a: number, b: number): number => a + b;
const SUB = (a: number, b: number): number => a - b;
const MUL = (a: number, b: number): number => a * b;
const DIV = (a: number, b: number): number => a / b;
export type Vector = number[];
export const vecAdd = (...vecs: Vector[]): Vector => {
return map(zip(...vecs), (x) => reduce(x, ADD));
};
export const vecSubtract = (...vecs: Vector[]): Vector => {
return map(zip(...vecs), (x) => reduce(x, SUB));
};
export const vecMultiply = (...vecs: Vector[]): Vector => {
return map(zip(...vecs), (x) => reduce(x, MUL));
};
export const vecDivide = (...vecs: Vector[]): Vector => {
return map(zip(...vecs), (x) => reduce(x, DIV));
};
export const vecScale = (vec: Vector, n: number): Vector => {
return map(vec, (x) => x * n);
};
export const vecInverse = (vec: Vector): Vector => {
return map(vec, (x) => -x);
};
export const vecLength = (vec: Vector): number => {
return Math.sqrt(reduce(vecMultiply(vec, vec), ADD));
};
export const vecNormalize = (vec: Vector): Vector => {
const length = vecLength(vec);
return map(vec, (c) => c / length);
};

View File

@@ -31,7 +31,7 @@ class WebpackCompiler {
// and retrieve all necessary dependencies. // and retrieve all necessary dependencies.
const requireFromRoot = createRequire(dirname(import.meta.url) + '/../..'); const requireFromRoot = createRequire(dirname(import.meta.url) + '/../..');
const webpack = await requireFromRoot('webpack'); const webpack = await requireFromRoot('webpack');
const createConfig = await requireFromRoot('./webpack.config.edge.js'); const createConfig = await requireFromRoot('./webpack.config.js');
const config = createConfig({}, options); const config = createConfig({}, options);
// Inject the HMR plugin into the config if we're using it // Inject the HMR plugin into the config if we're using it
if (options.hot) { if (options.hot) {

View File

@@ -217,6 +217,7 @@ export const chatMiddleware = (store) => {
chatRenderer.processBatch([payload_obj.content], { chatRenderer.processBatch([payload_obj.content], {
doArchive: true, doArchive: true,
}); });
sequences.push(sequence);
if (game.roundId !== settings.lastId) { if (game.roundId !== settings.lastId) {
storedRounds.push(game.roundId); storedRounds.push(game.roundId);
storedLines.push(settings.totalStoredMessages - 1); storedLines.push(settings.totalStoredMessages - 1);

View File

@@ -29,7 +29,7 @@ import {
typeIsImportant, typeIsImportant,
} from './model'; } from './model';
import { highlightNode, linkifyNode } from './replaceInTextNode'; import { highlightNode, linkifyNode } from './replaceInTextNode';
import { message } from './types'; import type { message } from './types';
const logger = createLogger('chatRenderer'); const logger = createLogger('chatRenderer');

View File

@@ -7,9 +7,9 @@
* @license MIT * @license MIT
*/ */
import { vecLength, vecSubtract } from 'common/vector';
import { focusMap } from 'tgui/focus'; import { focusMap } from 'tgui/focus';
import { canStealFocus, globalEvents } from 'tgui-core/events'; import { canStealFocus, globalEvents } from 'tgui-core/events';
import { vecLength, vecSubtract } from 'tgui-core/vector';
// Empyrically determined number for the smallest possible // Empyrically determined number for the smallest possible
// text you can select with the mouse. // text you can select with the mouse.

View File

@@ -15,7 +15,6 @@ img {
margin: 0; margin: 0;
padding: 0; padding: 0;
line-height: 1; line-height: 1;
-ms-interpolation-mode: nearest-neighbor; // TODO: Remove with 516
image-rendering: pixelated; image-rendering: pixelated;
} }

View File

@@ -33,7 +33,6 @@ img {
margin: 0; margin: 0;
padding: 0; padding: 0;
line-height: 1; line-height: 1;
-ms-interpolation-mode: nearest-neighbor; // TODO: Remove with 516
image-rendering: pixelated; image-rendering: pixelated;
} }

View File

@@ -15,7 +15,6 @@ img {
margin: 0; margin: 0;
padding: 0; padding: 0;
line-height: 1; line-height: 1;
-ms-interpolation-mode: nearest-neighbor; // TODO: Remove with 516
image-rendering: pixelated; image-rendering: pixelated;
} }

View File

@@ -33,7 +33,6 @@ img {
margin: 0; margin: 0;
padding: 0; padding: 0;
line-height: 1; line-height: 1;
-ms-interpolation-mode: nearest-neighbor; // TODO: Remove with 516
image-rendering: pixelated; image-rendering: pixelated;
} }

View File

@@ -1,17 +1,15 @@
import { Component, createRef, RefObject } from 'react'; import './styles/main.scss';
import { FormEvent, KeyboardEvent, useEffect, useRef, useState } from 'react';
import { dragStartHandler } from 'tgui/drag'; import { dragStartHandler } from 'tgui/drag';
import {
removeAllSkiplines,
sanitizeMultiline,
} from 'tgui/interfaces/TextInputModal';
import { isEscape, KEY } from 'tgui-core/keys'; import { isEscape, KEY } from 'tgui-core/keys';
import { clamp } from 'tgui-core/math'; import { clamp } from 'tgui-core/math';
import { BooleanLike } from 'tgui-core/react'; import { type BooleanLike, classes } from 'tgui-core/react';
import { Channel, ChannelIterator } from './ChannelIterator'; import { Channel, ChannelIterator } from './ChannelIterator';
import { ChatHistory } from './ChatHistory'; import { ChatHistory } from './ChatHistory';
import { LINE_LENGTHS, RADIO_PREFIXES, WINDOW_SIZES } from './constants'; import { LineLength, RADIO_PREFIXES, WindowSize } from './constants';
import { windowClose, windowOpen, windowSet } from './helpers'; import { getPrefix, windowClose, windowOpen, windowSet } from './helpers';
import { byondMessages } from './timers'; import { byondMessages } from './timers';
type ByondOpen = { type ByondOpen = {
@@ -21,390 +19,330 @@ type ByondOpen = {
type ByondProps = { type ByondProps = {
maxLength: number; maxLength: number;
minimumHeight: number; minimumHeight: number;
minimumWidth: number;
lightMode: BooleanLike; lightMode: BooleanLike;
}; };
type State = { const ROWS: Record<keyof typeof WindowSize, number> = {
buttonContent: string | number; Small: 1,
size: number; Medium: 2,
}; Large: 3,
Max: 20,
const CHANNEL_REGEX = /^:\w\s|^,b\s/; Width: 360,
MaxWidth: 800,
const ROWS: Record<keyof typeof WINDOW_SIZES, number> = {
small: 1,
medium: 2,
large: 3,
max: 6,
width: 1, // not used
} as const; } as const;
export class TguiSay extends Component<{}, State> { export function TguiSay() {
private channelIterator: ChannelIterator; const innerRef = useRef<HTMLTextAreaElement>(null);
private chatHistory: ChatHistory; const channelIterator = useRef(new ChannelIterator());
private currentPrefix: keyof typeof RADIO_PREFIXES | null; const chatHistory = useRef(new ChatHistory());
private innerRef: RefObject<HTMLTextAreaElement>; const messages = useRef(byondMessages);
private lightMode: boolean;
private minimumHeight: number;
private maxLength: number;
private messages: typeof byondMessages;
state: State;
constructor(props: never) { // I initially wanted to make these an object or a reducer, but it's not really worth it.
super(props); // You lose the granulatity and add a lot of boilerplate.
const [buttonContent, setButtonContent] = useState('');
const [currentPrefix, setCurrentPrefix] = useState<
keyof typeof RADIO_PREFIXES | null
>(null);
const [size, setSize] = useState(WindowSize.Small);
const [maxLength, setMaxLength] = useState(1024);
const [minimumHeight, setMinimumHeight] = useState(WindowSize.Small);
const [minimumWidth, setMinimumWidth] = useState(WindowSize.Width);
const [lightMode, setLightMode] = useState(false);
const [value, setValue] = useState('');
this.channelIterator = new ChannelIterator(); function handleArrowKeys(direction: KEY.Up | KEY.Down): void {
this.chatHistory = new ChatHistory(); const chat = chatHistory.current;
this.currentPrefix = null; const iterator = channelIterator.current;
this.innerRef = createRef();
this.lightMode = false;
this.minimumHeight = 1;
this.maxLength = 1024;
this.messages = byondMessages;
this.state = {
buttonContent: '',
size: WINDOW_SIZES.small,
};
this.handleArrowKeys = this.handleArrowKeys.bind(this); if (direction === KEY.Up) {
this.handleBackspaceDelete = this.handleBackspaceDelete.bind(this); if (chat.isAtLatest() && value) {
this.handleClose = this.handleClose.bind(this);
this.handleEnter = this.handleEnter.bind(this);
this.handleForceSay = this.handleForceSay.bind(this);
this.handleIncrementChannel = this.handleIncrementChannel.bind(this);
this.handleInput = this.handleInput.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleOpen = this.handleOpen.bind(this);
this.handleProps = this.handleProps.bind(this);
this.reset = this.reset.bind(this);
this.setSize = this.setSize.bind(this);
this.setValue = this.setValue.bind(this);
}
componentDidMount() {
Byond.subscribeTo('props', this.handleProps);
Byond.subscribeTo('force', this.handleForceSay);
Byond.subscribeTo('open', this.handleOpen);
}
handleArrowKeys(direction: KEY.PageUp | KEY.PageDown) {
const currentValue = this.innerRef.current?.value;
if (direction === KEY.PageUp) {
if (this.chatHistory.isAtLatest() && currentValue) {
// Save current message to temp history if at the most recent message // Save current message to temp history if at the most recent message
this.chatHistory.saveTemp(currentValue); chat.saveTemp(value);
} }
// Try to get the previous message, fall back to the current value if none // Try to get the previous message, fall back to the current value if none
const prevMessage = this.chatHistory.getOlderMessage(); const prevMessage = chat.getOlderMessage();
if (prevMessage) { if (prevMessage) {
this.setState({ buttonContent: this.chatHistory.getIndex() }); setButtonContent(chat.getIndex().toString());
this.setSize(prevMessage.length); setValue(prevMessage);
this.setValue(prevMessage);
} }
} else { } else {
const nextMessage = const nextMessage = chat.getNewerMessage() || chat.getTemp() || '';
this.chatHistory.getNewerMessage() || this.chatHistory.getTemp() || '';
const buttonContent = this.chatHistory.isAtLatest() const newContent = chat.isAtLatest()
? this.channelIterator.current() ? iterator.current()
: this.chatHistory.getIndex(); : chat.getIndex().toString();
this.setState({ buttonContent }); setButtonContent(newContent);
this.setSize(nextMessage.length); setValue(nextMessage);
this.setValue(nextMessage);
} }
} }
handleBackspaceDelete() { function handleBackspaceDelete(): void {
const typed = this.innerRef.current?.value; const chat = chatHistory.current;
const iterator = channelIterator.current;
// User is on a chat history message // User is on a chat history message
if (!this.chatHistory.isAtLatest()) { if (!chat.isAtLatest()) {
this.chatHistory.reset(); chat.reset();
this.setState({ setButtonContent(currentPrefix ?? iterator.current());
buttonContent: this.currentPrefix ?? this.channelIterator.current(),
});
// Empty input, resets the channel // Empty input, resets the channel
} else if ( } else if (currentPrefix && iterator.isSay() && value?.length === 0) {
!!this.currentPrefix && setCurrentPrefix(null);
this.channelIterator.isSay() && setButtonContent(iterator.current());
typed?.length === 0
) {
this.currentPrefix = null;
this.setState({ buttonContent: this.channelIterator.current() });
} }
this.setSize(typed?.length);
} }
handleClose() { function handleClose(): void {
const current = this.innerRef.current; innerRef.current?.blur();
if (current) {
current.blur();
}
this.reset();
this.chatHistory.reset();
this.channelIterator.reset();
this.currentPrefix = null;
windowClose(); windowClose();
setTimeout(() => {
chatHistory.current.reset();
channelIterator.current.reset();
unloadChat();
}, 25);
} }
handleEnter() { function handleEnter(): void {
const prefix = this.currentPrefix ?? ''; const iterator = channelIterator.current;
const value = this.innerRef.current?.value; const prefix = currentPrefix ?? '';
if (value?.length && value.length < this.maxLength) {
this.chatHistory.add(value);
// Everything can be multiline, but only emotes get passed that way to the game
const sanitizedValue = this.channelIterator.isMultiline()
? sanitizeMultiline(value)
: removeAllSkiplines(value);
if (value?.length && value.length < maxLength) {
chatHistory.current.add(value);
Byond.sendMessage('entry', { Byond.sendMessage('entry', {
channel: this.channelIterator.current(), channel: iterator.current(),
entry: this.channelIterator.isSay() entry: iterator.isSay() ? prefix + value : value,
? prefix + sanitizedValue
: sanitizedValue,
}); });
} }
this.handleClose(); handleClose();
} }
handleForceSay() { function handleForceSay(): void {
const currentValue = this.innerRef.current?.value; const iterator = channelIterator.current;
// Only force say if we're on a visible channel and have typed something // Only force say if we're on a visible channel and have typed something
if (!currentValue || !this.channelIterator.isVisible()) return; if (!value || iterator.isVisible()) return;
const prefix = this.currentPrefix ?? ''; const prefix = currentPrefix ?? '';
const grunt = this.channelIterator.isSay() const grunt = iterator.isSay() ? prefix + value : value;
? prefix + currentValue
: currentValue;
this.messages.forceSayMsg(grunt); messages.current.forceSayMsg(grunt, iterator.current());
this.reset(); unloadChat();
} }
handleIncrementChannel() { function handleIncrementChannel(): void {
this.currentPrefix = null; const iterator = channelIterator.current;
this.channelIterator.next(); iterator.next();
setButtonContent(iterator.current());
// If we've looped onto a quiet channel, tell byond to hide thinking indicators setCurrentPrefix(null);
if (!this.channelIterator.isVisible()) { messages.current.channelIncrementMsg(iterator.isVisible());
this.messages.channelIncrementMsg(false, this.channelIterator.current());
} else {
this.messages.channelIncrementMsg(true, this.channelIterator.current());
}
this.setState({ buttonContent: this.channelIterator.current() });
} }
handleDecrementChannel() { function handleInput(event: FormEvent<HTMLTextAreaElement>): void {
this.currentPrefix = null; let newValue = event.currentTarget.value;
this.channelIterator.prev(); let newPrefix = getPrefix(newValue) || currentPrefix;
// Handles switching prefixes
if (newPrefix && newPrefix !== currentPrefix) {
setButtonContent(RADIO_PREFIXES[newPrefix]);
setCurrentPrefix(newPrefix);
newValue = newValue.slice(3);
// If we've looped onto a quiet channel, tell byond to hide thinking indicators if (newPrefix === ',b ') {
if (!this.channelIterator.isVisible()) { Byond.sendMessage('thinking', { visible: false });
this.messages.channelIncrementMsg(false, this.channelIterator.current()); }
} else {
this.messages.channelIncrementMsg(true, this.channelIterator.current());
} }
this.setState({ buttonContent: this.channelIterator.current() }); // Handles typing indicators
if (channelIterator.current.isVisible() && newPrefix !== ',b ') {
messages.current.typingMsg();
}
setValue(newValue);
} }
handleInput() { function getMarkupString(
const typed = this.innerRef.current?.value; inputText: string,
markupType: string,
// If we're typing, send the message startPosition: number,
if (this.channelIterator.isVisible()) { endPosition: number,
this.messages.typingMsg(this.channelIterator.current()); ) {
} return `${inputText.substring(0, startPosition)}${markupType}${inputText.substring(startPosition, endPosition)}${markupType}${inputText.substring(endPosition)}`;
this.setSize(typed?.length);
// Is there a value? Is it long enough to be a prefix?
if (!typed || typed.length < 3) {
return;
}
if (!CHANNEL_REGEX.test(typed)) {
return;
}
// Is it a valid prefix?
const prefix = typed
.slice(0, 3)
?.toLowerCase() as keyof typeof RADIO_PREFIXES;
if (!RADIO_PREFIXES[prefix] || prefix === this.currentPrefix) {
return;
}
this.channelIterator.set('Say');
this.currentPrefix = prefix;
this.setState({ buttonContent: RADIO_PREFIXES[prefix] });
this.setValue(typed.slice(3));
} }
handleKeyDown(event: React.KeyboardEvent<HTMLTextAreaElement>) { function handleKeyDown(event: KeyboardEvent<HTMLTextAreaElement>): void {
const currentValue = this.innerRef.current?.value; if (event.getModifierState('AltGraph')) return;
switch (event.key) { switch (event.key) {
case KEY.PageUp: case 'u': // replace with tgui core 1.8.x
case KEY.PageDown: if (event.ctrlKey || event.metaKey) {
// Allow moving between lines if there are newlines event.preventDefault();
/* if (currentValue?.includes('\n')) { const { value, selectionStart, selectionEnd } = event.currentTarget;
break; event.currentTarget.value = getMarkupString(
} */ value,
'_',
selectionStart,
selectionEnd,
);
event.currentTarget.selectionEnd = selectionEnd + 2;
}
break;
case 'i': // replace with tgui core 1.8.x
if (event.ctrlKey || event.metaKey) {
event.preventDefault();
const { value, selectionStart, selectionEnd } = event.currentTarget;
event.currentTarget.value = getMarkupString(
value,
'|',
selectionStart,
selectionEnd,
);
event.currentTarget.selectionEnd = selectionEnd + 2;
}
break;
case 'b': // replace with tgui core 1.8.x
if (event.ctrlKey || event.metaKey) {
event.preventDefault();
const { value, selectionStart, selectionEnd } = event.currentTarget;
event.currentTarget.value = getMarkupString(
value,
'+',
selectionStart,
selectionEnd,
);
event.currentTarget.selectionEnd = selectionEnd + 2;
}
break;
case KEY.Up:
case KEY.Down:
event.preventDefault(); event.preventDefault();
this.handleArrowKeys(event.key); handleArrowKeys(event.key);
break; break;
case KEY.Delete: case KEY.Delete:
case KEY.Backspace: case KEY.Backspace:
this.handleBackspaceDelete(); handleBackspaceDelete();
break; break;
case KEY.Enter: case KEY.Enter:
// Allow inputting newlines
if (event.shiftKey) {
break;
}
event.preventDefault(); event.preventDefault();
this.handleEnter(); handleEnter();
break; break;
case KEY.Tab: case KEY.Tab:
event.preventDefault(); event.preventDefault();
if (event.shiftKey) { handleIncrementChannel();
this.handleDecrementChannel();
} else {
this.handleIncrementChannel();
}
break; break;
default: default:
if (isEscape(event.key)) { if (isEscape(event.key)) {
this.handleClose(); handleClose();
} }
} }
} }
handleOpen = (data: ByondOpen) => { function handleOpen(data: ByondOpen): void {
setTimeout(() => { setTimeout(() => {
this.innerRef.current?.focus(); innerRef.current?.focus();
}, 0); windowSet(WindowSize.Width, WindowSize.Small);
setSize(WindowSize.Width);
}, 1);
const { channel } = data; const { channel } = data;
const iterator = channelIterator.current;
// Catches the case where the modal is already open // Catches the case where the modal is already open
if (this.channelIterator.isSay()) { if (iterator.isSay()) {
this.channelIterator.set(channel); iterator.set(channel);
} }
this.setState({ buttonContent: this.channelIterator.current() });
windowOpen(this.channelIterator.current()); setButtonContent(iterator.current());
}; windowOpen(iterator.current());
handleProps = (data: ByondProps) => {
const { maxLength, minimumHeight, lightMode } = data;
this.maxLength = maxLength;
this.minimumHeight = minimumHeight;
this.setSize();
this.lightMode = !!lightMode;
};
reset() {
this.setValue('');
this.setSize();
this.setState({
buttonContent: this.channelIterator.current(),
});
} }
setSize(length = 0) { function handleProps(data: ByondProps): void {
let newSize: number; setMaxLength(data.maxLength);
setMinimumHeight(data.minimumHeight);
const currentValue = this.innerRef.current?.value; const minWidth = clamp(
data.minimumWidth,
if (currentValue?.includes('\n')) { WindowSize.Width,
newSize = WINDOW_SIZES.large; WindowSize.MaxWidth,
} else if (length > LINE_LENGTHS.medium) {
newSize = WINDOW_SIZES.large;
} else if (length <= LINE_LENGTHS.medium && length > LINE_LENGTHS.small) {
newSize = WINDOW_SIZES.medium;
} else {
newSize = WINDOW_SIZES.small;
}
newSize = clamp(newSize, this.minimumHeight * 20 + 10, WINDOW_SIZES.max);
console.log(newSize);
if (this.state.size !== newSize) {
this.setState({ size: newSize });
windowSet(newSize);
}
}
setValue(value: string) {
const textArea = this.innerRef.current;
if (textArea) {
textArea.value = value;
}
}
render() {
const theme =
(this.lightMode && 'lightMode') ||
(this.currentPrefix && RADIO_PREFIXES[this.currentPrefix]) ||
this.channelIterator.current();
return (
<div className={`window window-${theme} window-${this.state.size}`}>
<Dragzone position="top" theme={theme} />
<div className="center">
<Dragzone position="left" theme={theme} />
<div className="input">
<button
className={`button button-${theme}`}
onClick={this.handleIncrementChannel}
type="button"
>
{this.state.buttonContent}
</button>
<textarea
autoCorrect="off"
className={`textarea textarea-${theme}`}
maxLength={this.maxLength}
onInput={this.handleInput}
onKeyDown={this.handleKeyDown}
ref={this.innerRef}
spellCheck={false} // TODO: make preference
rows={ROWS[this.state.size] || 1}
/>
</div>
<Dragzone position="right" theme={theme} />
</div>
<Dragzone position="bottom" theme={theme} />
</div>
); );
setMinimumWidth(minWidth);
setLightMode(!!data.lightMode);
} }
}
const Dragzone = ({ theme, position }: { theme: string; position: string }) => { function unloadChat(): void {
// Horizontal or vertical? setCurrentPrefix(null);
const location = setButtonContent(channelIterator.current.current());
position === 'left' || position === 'right' ? 'vertical' : 'horizontal'; setValue('');
}
/** Subscribe to Byond messages */
useEffect(() => {
Byond.subscribeTo('props', handleProps);
Byond.subscribeTo('force', handleForceSay);
Byond.subscribeTo('open', handleOpen);
}, []);
/** Value has changed, we need to check if the size of the window is ok */
useEffect(() => {
const len = value?.length || 0;
let newSize: WindowSize;
if (len > LineLength.Medium) {
newSize = WindowSize.Large;
} else if (len <= LineLength.Medium && len > LineLength.Small) {
newSize = WindowSize.Medium;
} else {
newSize = WindowSize.Small;
}
newSize = clamp(newSize, minimumHeight * 20 + 10, WindowSize.Max);
if (size !== newSize) {
setSize(newSize);
windowSet(minimumWidth, newSize);
}
}, [value]);
const theme =
(lightMode && 'lightMode') ||
(currentPrefix && RADIO_PREFIXES[currentPrefix]) ||
channelIterator.current.current();
return ( return (
<div <>
className={`dragzone-${location} dragzone-${position} dragzone-${theme}`} <div
onMouseDown={dragStartHandler} className={`window window-${theme} window-${size}`}
/> onMouseDown={dragStartHandler}
>
{!lightMode && <div className={`shine shine-${theme}`} />}
</div>
<div className={classes(['content', lightMode && 'content-lightMode'])}>
<button
className={`button button-${theme}`}
onClick={handleIncrementChannel}
onMouseDown={dragStartHandler}
type="button"
>
{buttonContent}
</button>
<textarea
autoCorrect="off"
className={`textarea textarea-${theme}`}
maxLength={maxLength}
onInput={handleInput}
onKeyDown={handleKeyDown}
ref={innerRef}
spellCheck={false}
rows={ROWS[size] || 1}
value={value}
/>
</div>
</>
); );
}; }

View File

@@ -1,16 +1,17 @@
/** Window sizes in pixels */ /** Window sizes in pixels */
export enum WINDOW_SIZES { export enum WindowSize {
small = 30, Small = 30,
medium = 50, Medium = 50,
large = 70, Large = 70,
max = 130, Max = 410,
width = 360, Width = 360,
MaxWidth = 800,
} }
/** Line lengths for autoexpand */ /** Line lengths for autoexpand */
export enum LINE_LENGTHS { export enum LineLength {
small = 22, Small = 22,
medium = 45, Medium = 45,
} }
/** /**

View File

@@ -1,33 +1,35 @@
import { Channel } from './ChannelIterator'; import { Channel } from './ChannelIterator';
import { WINDOW_SIZES } from './constants'; import { RADIO_PREFIXES, WindowSize } from './constants';
/** /**
* Once byond signals this via keystroke, it * Once byond signals this via keystroke, it
* ensures window size, visibility, and focus. * ensures window size, visibility, and focus.
*/ */
export const windowOpen = (channel: Channel) => { export function windowOpen(channel: Channel): void {
setWindowVisibility(true); setWindowVisibility(true);
Byond.sendMessage('open', { channel }); Byond.sendMessage('open', { channel });
}; }
/** /**
* Resets the state of the window and hides it from user view. * Resets the state of the window and hides it from user view.
* Sending "close" logs it server side. * Sending "close" logs it server side.
*/ */
export const windowClose = () => { export function windowClose(): void {
setWindowVisibility(false); setWindowVisibility(false);
Byond.winset('map', { Byond.winset('map', {
focus: true, focus: true,
}); });
Byond.sendMessage('close'); Byond.sendMessage('close');
}; }
/** /**
* Modifies the window size. * Modifies the window size.
*/ */
export const windowSet = (size: number = WINDOW_SIZES.small) => { export function windowSet(
let sizeStr = `${WINDOW_SIZES.width}x${size}`; width = WindowSize.Width,
console.log(sizeStr); size = WindowSize.Small,
): void {
let sizeStr = `${width}x${size}`;
Byond.winset('tgui_say.browser', { Byond.winset('tgui_say.browser', {
size: sizeStr, size: sizeStr,
@@ -36,11 +38,34 @@ export const windowSet = (size: number = WINDOW_SIZES.small) => {
Byond.winset('tgui_say', { Byond.winset('tgui_say', {
size: sizeStr, size: sizeStr,
}); });
}; }
/** Helper function to set window size and visibility */ /** Helper function to set window size and visibility */
const setWindowVisibility = (visible: boolean) => { function setWindowVisibility(visible: boolean): void {
Byond.winset('tgui_say', { Byond.winset('tgui_say', {
'is-visible': visible, 'is-visible': visible,
size: `${WindowSize.Width}x${WindowSize.Small}`,
}); });
}; }
const CHANNEL_REGEX = /^[:.]|,b\w\s/;
/** Tests for a channel prefix, returning it or none */
export function getPrefix(
value: string,
): keyof typeof RADIO_PREFIXES | undefined {
if (!value || value.length < 3 || !CHANNEL_REGEX.test(value)) {
return;
}
let adjusted = value
.slice(0, 3)
?.toLowerCase()
?.replace('.', ':') as keyof typeof RADIO_PREFIXES;
if (!RADIO_PREFIXES[adjusted]) {
return;
}
return adjusted;
}

View File

@@ -1,33 +0,0 @@
@use 'sass:color';
@use './colors.scss';
.button {
align-items: center;
background-color: colors.$button;
border-radius: 0.3rem;
border: thin solid;
display: flex;
flex-grow: 1;
font-family: inherit;
font-size: 0.9rem;
font-weight: bold;
justify-content: center;
padding: 0;
width: 2.6rem;
&:hover {
background-color: color.adjust(
colors.$button,
$lightness: 10%,
$space: hsl
);
}
}
.button-lightMode {
background-color: colors.$lightBorder;
border: none;
color: black;
&:hover {
background-color: colors.$lightHover;
}
}

View File

@@ -1,10 +1,10 @@
@use 'sass:map'; @use 'sass:map';
$background: hsl(0, 0%, 7%); $background: hsl(0, 0%, 7.5%);
$button: hsl(0, 0%, 12%); $button: hsl(0, 0%, 12.2%);
$lightMode: hsl(0, 0%, 100%); $lightMode: hsl(0, 0%, 100%);
$lightBorder: hsl(0, 0%, 73%); $lightBorder: hsl(0, 0%, 73.3%);
$lightHover: hsl(0, 0%, 92%); $lightHover: hsl(0, 0%, 91.8%);
$scrollbar-color-multiplier: 1 !default; $scrollbar-color-multiplier: 1 !default;
$_channel_map: ( $_channel_map: (

View File

@@ -1,41 +0,0 @@
.center {
display: flex;
}
// Remove conditionals with 516
@supports (not (-webkit-hyphens: none)) and (not (-moz-appearance: none)) {
.center {
flex-grow: 1;
}
}
// Remove with 516
@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {
.center {
height: 100%;
width: 100%;
flex: 1 1 0;
}
}
.input {
display: flex;
font-family: 'Consolas', monospace;
}
// Remove conditionals with 516
@supports (not (-webkit-hyphens: none)) and (not (-moz-appearance: none)) {
.input {
flex-grow: 1;
gap: 0.2rem;
}
}
// Remove with 516
@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {
.input {
height: 100%;
width: 100%;
flex: 1 1 0;
}
}

View File

@@ -1,49 +0,0 @@
@use 'sass:color';
@use './colors.scss';
// Remove conditionals with 516
@supports (not (-webkit-hyphens: none)) and (not (-moz-appearance: none)) {
$dragSize: 0.3rem;
}
// Remove with 516
@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {
$dragSize: 0.6rem;
}
$dragSize: 0.3rem;
$borderSize: 0.2rem;
.dragzone-horizontal {
border-left: $borderSize solid;
border-right: $borderSize solid;
color: transparent;
width: 100%;
height: $dragSize;
}
.dragzone-left {
border-left: $borderSize solid;
}
.dragzone-right {
border-right: $borderSize solid;
}
.dragzone-vertical {
color: transparent;
height: 100%;
width: $dragSize;
}
.dragzone-top {
border-top: $borderSize solid;
}
.dragzone-bottom {
border-bottom: $borderSize solid;
}
/** Lightmode static theme */
.dragzone-lightMode {
border-color: colors.$lightBorder;
}

View File

@@ -3,27 +3,13 @@
@use './colors.scss'; @use './colors.scss';
// Core styles // Core styles
@include meta.load-css('~tgui/styles/reset.scss'); @include meta.load-css('~tgui/styles/reset');
// Atomic styles // Atomic styles
@include meta.load-css('~tgui/styles/atomic/text.scss'); @include meta.load-css('~tgui/styles/atomic/text.scss');
// External styles // External styles
@include meta.load-css('~tgui-core/styles/components/TextArea'); @include meta.load-css('~tgui-core/styles/components/TextArea');
// Local styles // Local styles
@include meta.load-css('./button.scss'); @include meta.load-css('./styles.scss');
@include meta.load-css('./content.scss');
@include meta.load-css('./dragzone.scss');
@include meta.load-css('./textarea.scss');
@include meta.load-css('./window.scss');
@keyframes gradient {
0% {
background-position: 0 0;
}
100% {
background-position: 100% 0;
}
}
@each $channel, $color in colors.$channel-map { @each $channel, $color in colors.$channel-map {
$darkened: color.adjust($color, $lightness: -20%, $space: hsl); $darkened: color.adjust($color, $lightness: -20%, $space: hsl);
@@ -37,34 +23,22 @@
} }
} }
.dragzone-#{$channel} {
border-color: $darkened;
}
.textarea-#{$channel} { .textarea-#{$channel} {
color: $color; color: $color;
} }
.window-#{$channel} { .window-#{$channel} {
&:after { background-color: $color;
animation: gradient 10s linear infinite; }
background: linear-gradient(
to right, .shine-#{$channel} {
color.adjust($color, $lightness: -35%, $space: hsl), background: radial-gradient(
$color, circle,
color.adjust($color, $lightness: 10%, $space: hsl), color.adjust($color, $lightness: 5%, $space: hsl),
$color, color.adjust($color, $lightness: -15%, $space: hsl),
color.adjust($color, $lightness: -35%, $space: hsl) color.adjust($color, $lightness: 15%, $space: hsl),
); color.adjust($color, $lightness: -15%, $space: hsl),
background-position: 0% 0%; color.adjust($color, $lightness: 5%, $space: hsl)
background-size: 500% auto; );
bottom: 0px;
content: '';
height: 2px;
left: 0px;
position: absolute;
right: 0px;
z-index: 999;
}
} }
} }

View File

@@ -0,0 +1,119 @@
@use 'sass:color';
@use './colors.scss';
.window {
background-color: black;
position: relative;
overflow: hidden;
}
@keyframes shine {
0% {
transform: rotate(0deg);
}
50% {
transform: rotate(270deg);
}
100% {
transform: rotate(360deg);
}
}
.shine {
animation: shine 15s linear infinite;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-size: 150% 150%;
}
.window-lightMode {
background-color: colors.$lightMode;
}
/** Window sizes */
.window-30 {
height: 30px;
}
.window-50 {
height: 50px;
}
.window-70 {
height: 70px;
}
.content {
background-color: black;
display: grid;
font: 'tgfont';
grid-template-columns: 3.5rem 1fr;
inset: 2px;
overflow: hidden;
padding: 2px;
position: absolute;
}
.content-lightMode {
background-color: white;
}
.button {
background-color: colors.$button;
border-radius: 0.3rem;
border: none;
font-family: inherit;
font-size: 11px;
font-weight: bold;
outline: none;
overflow: hidden;
padding: 0.1rem 0.2rem;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
&:hover {
background-color: color.adjust(
colors.$button,
$lightness: 10%,
$space: hsl
);
}
}
.button-lightMode {
background-color: colors.$lightBorder;
border: none;
color: black;
&:hover {
background-color: colors.$lightHover;
}
}
.textarea {
background: transparent;
border: none;
font-family: inherit;
font-size: 1.1rem;
margin: 0.1rem 0 0 0.4rem;
outline: none;
resize: none;
&::-webkit-scrollbar {
width: 0.8rem;
}
&::-webkit-scrollbar-track {
background: color.scale(
colors.$button,
$lightness: -25% * colors.$scrollbar-color-multiplier
);
}
&::-webkit-scrollbar-thumb {
background: color.scale(
colors.$button,
$lightness: 10% * colors.$scrollbar-color-multiplier
);
}
}

View File

@@ -1,51 +0,0 @@
@use 'sass:color';
@use './colors.scss';
@supports (not (-webkit-hyphens: none)) and (not (-moz-appearance: none)) {
* {
&::-webkit-scrollbar {
width: 0.8rem;
}
&::-webkit-scrollbar-track {
background: color.scale(
colors.$button,
$lightness: -25% * colors.$scrollbar-color-multiplier
);
}
&::-webkit-scrollbar-thumb {
background: color.scale(
colors.$button,
$lightness: 10% * colors.$scrollbar-color-multiplier
);
}
}
}
.textarea {
background: transparent;
border: none;
font-family: inherit;
font-size: 1.1rem;
}
// Remove conditionals with 516
@supports (not (-webkit-hyphens: none)) and (not (-moz-appearance: none)) {
.textarea {
flex-grow: 8;
outline: none;
resize: none;
}
}
// Remove with 516
@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {
.textarea {
align-items: center;
display: flex;
flex-grow: 4;
overflow: hidden;
margin: 0.1rem 0 0 0.4rem;
}
}

View File

@@ -1,47 +0,0 @@
@use 'sass:color';
@use './colors.scss';
.window {
background-color: colors.$background;
display: flex;
flex-direction: column;
max-width: 360px;
overflow: hidden;
}
// Remove with 516
@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {
.window {
height: 100%;
width: 100%;
}
}
.window-lightMode {
background-color: colors.$lightMode;
}
/** Window sizes */
.window-30 {
height: 30px;
}
.window-50 {
height: 50px;
}
.window-70 {
height: 70px;
}
.window-90 {
height: 90px;
}
.window-110 {
height: 110px;
}
.window-130 {
height: 130px;
}

View File

@@ -1,23 +1,22 @@
import { debounce, throttle } from 'tgui-core/timer'; import { debounce, throttle } from 'tgui-core/timer';
import { Channel } from './ChannelIterator';
const SECONDS = 1000; const SECONDS = 1000;
/** Timers: Prevents overloading the server, throttles messages */ /** Timers: Prevents overloading the server, throttles messages */
export const byondMessages = { export const byondMessages = {
// Debounce: Prevents spamming the server // Debounce: Prevents spamming the server
channelIncrementMsg: debounce( channelIncrementMsg: debounce(
(visible: boolean, channel: string) => (visible: boolean) => Byond.sendMessage('thinking', { visible }),
Byond.sendMessage('thinking', { visible, channel }),
0.4 * SECONDS, 0.4 * SECONDS,
), ),
forceSayMsg: debounce( forceSayMsg: debounce(
(entry: string) => Byond.sendMessage('force', { entry, channel: 'Say' }), (entry: string, channel: Channel) =>
Byond.sendMessage('force', { entry, channel }),
1 * SECONDS, 1 * SECONDS,
true, true,
), ),
// Throttle: Prevents spamming the server // Throttle: Prevents spamming the server
typingMsg: throttle( typingMsg: throttle(() => Byond.sendMessage('typing'), 4 * SECONDS),
(channel: string) => Byond.sendMessage('typing', { channel }),
4 * SECONDS,
),
} as const; } as const;

View File

@@ -49,9 +49,18 @@ export class NanoMap extends Component<Props, State> {
handleDragMove: (e: MouseEvent) => void; handleDragMove: (e: MouseEvent) => void;
handleDragEnd: (e: MouseEvent) => void; handleDragEnd: (e: MouseEvent) => void;
handleZoom: (e: Event, v: number) => void; handleZoom: (e: Event, v: number) => void;
handleWheel: (e: WheelEvent) => void;
handleKey: (e: KeyEvent) => void; handleKey: (e: KeyEvent) => void;
ref: EventTarget; ref: EventTarget;
componentDidMount() {
document.addEventListener('wheel', this.handleWheel);
}
componentWillUnmount() {
document.removeEventListener('wheel', this.handleWheel);
}
setZoom(zoom: number) { setZoom(zoom: number) {
const newZoom = Math.min(Math.max(zoom, 1), 8); const newZoom = Math.min(Math.max(zoom, 1), 8);
this.setState((state) => { this.setState((state) => {
@@ -134,6 +143,14 @@ export class NanoMap extends Component<Props, State> {
pauseEvent(e); pauseEvent(e);
}; };
this.handleWheel = (e: WheelEvent) => {
if (e.deltaY > 0) {
this.setZoom(this.state.zoom + 1);
} else if (e.deltaY < 0) {
this.setZoom(this.state.zoom - 1);
}
};
this.handleZoom = (_e: Event, value: number) => { this.handleZoom = (_e: Event, value: number) => {
this.setZoom(value); this.setZoom(value);
}; };

View File

@@ -5,7 +5,7 @@
*/ */
import { storage } from 'common/storage'; import { storage } from 'common/storage';
import { vecAdd, vecMultiply, vecScale, vecSubtract } from 'common/vector'; import { vecAdd, vecMultiply, vecScale, vecSubtract } from 'tgui-core/vector';
import { createLogger } from './logging'; import { createLogger } from './logging';

View File

@@ -7,7 +7,7 @@ import {
ProgressBar, ProgressBar,
Section, Section,
} from 'tgui-core/components'; } from 'tgui-core/components';
import { BooleanLike } from 'tgui-core/react'; import type { BooleanLike } from 'tgui-core/react';
type Data = { type Data = {
name: string; name: string;

View File

@@ -9,7 +9,7 @@ import {
ProgressBar, ProgressBar,
Section, Section,
} from 'tgui-core/components'; } from 'tgui-core/components';
import { BooleanLike } from 'tgui-core/react'; import type { BooleanLike } from 'tgui-core/react';
import { FullscreenNotice } from './common/FullscreenNotice'; import { FullscreenNotice } from './common/FullscreenNotice';
import { InterfaceLockNoticeBox } from './common/InterfaceLockNoticeBox'; import { InterfaceLockNoticeBox } from './common/InterfaceLockNoticeBox';

View File

@@ -9,7 +9,7 @@ import {
Table, Table,
Tabs, Tabs,
} from 'tgui-core/components'; } from 'tgui-core/components';
import { BooleanLike } from 'tgui-core/react'; import type { BooleanLike } from 'tgui-core/react';
type Data = { type Data = {
id_inserted: BooleanLike; id_inserted: BooleanLike;

View File

@@ -1,7 +1,7 @@
import { useBackend } from 'tgui/backend'; import { useBackend } from 'tgui/backend';
import { Window } from 'tgui/layouts'; import { Window } from 'tgui/layouts';
import { Button, Section, Table } from 'tgui-core/components'; import { Button, Section, Table } from 'tgui-core/components';
import { BooleanLike } from 'tgui-core/react'; import type { BooleanLike } from 'tgui-core/react';
type Data = { type Data = {
entries: { name: string; value: string }[]; entries: { name: string; value: string }[];

View File

@@ -1,7 +1,7 @@
import { useBackend } from 'tgui/backend'; import { useBackend } from 'tgui/backend';
import { Window } from 'tgui/layouts'; import { Window } from 'tgui/layouts';
import { Button, LabeledList, Section } from 'tgui-core/components'; import { Button, LabeledList, Section } from 'tgui-core/components';
import { BooleanLike } from 'tgui-core/react'; import type { BooleanLike } from 'tgui-core/react';
type Data = { type Data = {
power: { power: {

View File

@@ -8,7 +8,7 @@ import {
ProgressBar, ProgressBar,
Section, Section,
} from 'tgui-core/components'; } from 'tgui-core/components';
import { BooleanLike } from 'tgui-core/react'; import type { BooleanLike } from 'tgui-core/react';
type Data = { type Data = {
AI_present: boolean; AI_present: boolean;

View File

@@ -7,7 +7,7 @@ import {
ProgressBar, ProgressBar,
Section, Section,
} from 'tgui-core/components'; } from 'tgui-core/components';
import { BooleanLike } from 'tgui-core/react'; import type { BooleanLike } from 'tgui-core/react';
import { FullscreenNotice } from './common/FullscreenNotice'; import { FullscreenNotice } from './common/FullscreenNotice';

View File

@@ -4,10 +4,10 @@ import { getGasColor, getGasLabel } from 'tgui/constants';
import { Window } from 'tgui/layouts'; import { Window } from 'tgui/layouts';
import { Box, Button, LabeledList, Section } from 'tgui-core/components'; import { Box, Button, LabeledList, Section } from 'tgui-core/components';
import { toFixed } from 'tgui-core/math'; import { toFixed } from 'tgui-core/math';
import { BooleanLike } from 'tgui-core/react'; import type { BooleanLike } from 'tgui-core/react';
import { Scrubber, Vent } from './common/AtmosControls'; import { Scrubber, Vent } from './common/AtmosControls';
import { single_scrubber, single_vent } from './common/CommonTypes'; import type { single_scrubber, single_vent } from './common/CommonTypes';
import { InterfaceLockNoticeBox } from './common/InterfaceLockNoticeBox'; import { InterfaceLockNoticeBox } from './common/InterfaceLockNoticeBox';
type Data = { type Data = {

View File

@@ -1,7 +1,7 @@
import { useBackend } from 'tgui/backend'; import { useBackend } from 'tgui/backend';
import { Button, ColorBox, LabeledList, Section } from 'tgui-core/components'; import { Button, ColorBox, LabeledList, Section } from 'tgui-core/components';
import { Data, species } from './types'; import type { Data, species } from './types';
export const AppearanceChangerSpecies = (props) => { export const AppearanceChangerSpecies = (props) => {
const { act, data } = useBackend<Data>(); const { act, data } = useBackend<Data>();

View File

@@ -1,7 +1,7 @@
import { useBackend } from 'tgui/backend'; import { useBackend } from 'tgui/backend';
import { Button, Section } from 'tgui-core/components'; import { Button, Section } from 'tgui-core/components';
import { Data } from './types'; import type { Data } from './types';
export const AppearanceChangerBodyRecords = () => { export const AppearanceChangerBodyRecords = () => {
const { act, data } = useBackend<Data>(); const { act, data } = useBackend<Data>();

View File

@@ -14,8 +14,11 @@ import {
} from 'tgui-core/components'; } from 'tgui-core/components';
import { createSearch } from 'tgui-core/string'; import { createSearch } from 'tgui-core/string';
import { MARKINGS_PER_PAGE } from './constants'; import {
import { bodyStyle, Data, SPRITE_ACCESSORY_COLOR_CHANNEL_NAMES } from './types'; MARKINGS_PER_PAGE,
SPRITE_ACCESSORY_COLOR_CHANNEL_NAMES,
} from './constants';
import type { bodyStyle, Data } from './types';
export const AppearanceChangerColors = (props) => { export const AppearanceChangerColors = (props) => {
const { act, data } = useBackend<Data>(); const { act, data } = useBackend<Data>();

View File

@@ -2,7 +2,7 @@ import { useBackend } from 'tgui/backend';
import { Box, Button, LabeledList, Section } from 'tgui-core/components'; import { Box, Button, LabeledList, Section } from 'tgui-core/components';
import { capitalize } from 'tgui-core/string'; import { capitalize } from 'tgui-core/string';
import { Data } from './types'; import type { Data } from './types';
export const AppearanceChangerFlavor = (props) => { export const AppearanceChangerFlavor = (props) => {
const { act, data } = useBackend<Data>(); const { act, data } = useBackend<Data>();

View File

@@ -1,7 +1,7 @@
import { useBackend } from 'tgui/backend'; import { useBackend } from 'tgui/backend';
import { Button, Section } from 'tgui-core/components'; import { Button, Section } from 'tgui-core/components';
import { Data } from './types'; import type { Data } from './types';
export const AppearanceChangerHeader = (props) => { export const AppearanceChangerHeader = (props) => {
const { act, data } = useBackend<Data>(); const { act, data } = useBackend<Data>();

View File

@@ -3,7 +3,7 @@ import { useBackend } from 'tgui/backend';
import { ImageButton, Input, Section, Stack } from 'tgui-core/components'; import { ImageButton, Input, Section, Stack } from 'tgui-core/components';
import { createSearch } from 'tgui-core/string'; import { createSearch } from 'tgui-core/string';
import { bodyStyle, Data, styles } from './types'; import type { bodyStyle, Data, styles } from './types';
export const AppearanceChangerParts = (props: { export const AppearanceChangerParts = (props: {
sectionNames: string[]; sectionNames: string[];

View File

@@ -1,3 +1,10 @@
export const SPRITE_ACCESSORY_COLOR_CHANNEL_NAMES = [
'Primary',
'Secondary',
'Tertiary',
'Quaternary',
];
export const TAB_RACE = 0; export const TAB_RACE = 0;
export const TAB_FLAVOR = 1; export const TAB_FLAVOR = 1;
export const TAB_GENDER = 2; export const TAB_GENDER = 2;

View File

@@ -40,7 +40,7 @@ import {
TAB_TAIL, TAB_TAIL,
TAB_WINGS, TAB_WINGS,
} from './constants'; } from './constants';
import { Data } from './types'; import type { Data } from './types';
export const AppearanceChanger = (props) => { export const AppearanceChanger = (props) => {
const { act, config, data } = useBackend<Data>(); const { act, config, data } = useBackend<Data>();

View File

@@ -1,12 +1,5 @@
import { BooleanLike } from 'tgui-core/react'; import { BooleanLike } from 'tgui-core/react';
export const SPRITE_ACCESSORY_COLOR_CHANNEL_NAMES = [
'Primary',
'Secondary',
'Tertiary',
'Quaternary',
];
export type Data = { export type Data = {
name: string; name: string;
specimen: string; specimen: string;

View File

@@ -8,7 +8,7 @@ import {
Section, Section,
Stack, Stack,
} from 'tgui-core/components'; } from 'tgui-core/components';
import { BooleanLike } from 'tgui-core/react'; import type { BooleanLike } from 'tgui-core/react';
type Data = { type Data = {
name: string; name: string;

View File

@@ -2,7 +2,7 @@ import { useState } from 'react';
import { useBackend } from 'tgui/backend'; import { useBackend } from 'tgui/backend';
import { Window } from 'tgui/layouts'; import { Window } from 'tgui/layouts';
import { Box, Button, LabeledList, Section, Stack } from 'tgui-core/components'; import { Box, Button, LabeledList, Section, Stack } from 'tgui-core/components';
import { BooleanLike } from 'tgui-core/react'; import type { BooleanLike } from 'tgui-core/react';
import { toTitleCase } from 'tgui-core/string'; import { toTitleCase } from 'tgui-core/string';
type scrubber = { type scrubber = {

View File

@@ -1,7 +1,7 @@
import { useBackend } from 'tgui/backend'; import { useBackend } from 'tgui/backend';
import { Window } from 'tgui/layouts'; import { Window } from 'tgui/layouts';
import { Button, LabeledList, Section } from 'tgui-core/components'; import { Button, LabeledList, Section } from 'tgui-core/components';
import { BooleanLike } from 'tgui-core/react'; import type { BooleanLike } from 'tgui-core/react';
type Data = { type Data = {
on: BooleanLike; on: BooleanLike;

View File

@@ -8,7 +8,7 @@ import {
} from 'tgui-core/components'; } from 'tgui-core/components';
import { formatTime } from 'tgui-core/format'; import { formatTime } from 'tgui-core/format';
import { round } from 'tgui-core/math'; import { round } from 'tgui-core/math';
import { BooleanLike } from 'tgui-core/react'; import type { BooleanLike } from 'tgui-core/react';
type Data = { type Data = {
timing: number; timing: number;

View File

@@ -8,7 +8,7 @@ import {
NumberInput, NumberInput,
Section, Section,
} from 'tgui-core/components'; } from 'tgui-core/components';
import { BooleanLike } from 'tgui-core/react'; import type { BooleanLike } from 'tgui-core/react';
type Data = { type Data = {
on: BooleanLike; on: BooleanLike;

View File

@@ -6,7 +6,7 @@ import {
NumberInput, NumberInput,
Section, Section,
} from 'tgui-core/components'; } from 'tgui-core/components';
import { BooleanLike } from 'tgui-core/react'; import type { BooleanLike } from 'tgui-core/react';
type Data = { type Data = {
on: BooleanLike; on: BooleanLike;

View File

@@ -12,7 +12,7 @@ import {
VirtualList, VirtualList,
} from 'tgui-core/components'; } from 'tgui-core/components';
import { formatSiUnit } from 'tgui-core/format'; import { formatSiUnit } from 'tgui-core/format';
import { BooleanLike } from 'tgui-core/react'; import type { BooleanLike } from 'tgui-core/react';
import { toTitleCase } from 'tgui-core/string'; import { toTitleCase } from 'tgui-core/string';
import { Materials } from './ExosuitFabricator/Material'; import { Materials } from './ExosuitFabricator/Material';

View File

@@ -10,7 +10,7 @@ import {
Stack, Stack,
Table, Table,
} from 'tgui-core/components'; } from 'tgui-core/components';
import { BooleanLike } from 'tgui-core/react'; import type { BooleanLike } from 'tgui-core/react';
import { capitalize } from 'tgui-core/string'; import { capitalize } from 'tgui-core/string';
type Data = { type Data = {

View File

@@ -10,7 +10,7 @@ import {
Section, Section,
Stack, Stack,
} from 'tgui-core/components'; } from 'tgui-core/components';
import { BooleanLike } from 'tgui-core/react'; import type { BooleanLike } from 'tgui-core/react';
import { createSearch } from 'tgui-core/string'; import { createSearch } from 'tgui-core/string';
type sortable = { type sortable = {

View File

@@ -6,7 +6,7 @@ import { BodyScannerMainOccupant } from './BodyScannerMainOccupant';
import { BodyScannerMainOrgansExternal } from './BodyScannerMainOrgansExternal'; import { BodyScannerMainOrgansExternal } from './BodyScannerMainOrgansExternal';
import { BodyScannerMainOrgansInternal } from './BodyScannerMainOrgansInternal'; import { BodyScannerMainOrgansInternal } from './BodyScannerMainOrgansInternal';
import { BodyScannerMainReagents } from './BodyScannerMainReagents'; import { BodyScannerMainReagents } from './BodyScannerMainReagents';
import { occupant } from './types'; import type { occupant } from './types';
export const BodyScannerMain = (props: { occupant: occupant }) => { export const BodyScannerMain = (props: { occupant: occupant }) => {
const { occupant } = props; const { occupant } = props;

View File

@@ -1,7 +1,7 @@
import { Box, Section } from 'tgui-core/components'; import { Box, Section } from 'tgui-core/components';
import { abnormalities } from './constants'; import { abnormalities } from './constants';
import { occupant } from './types'; import type { occupant } from './types';
export const BodyScannerMainAbnormalities = (props: { occupant: occupant }) => { export const BodyScannerMainAbnormalities = (props: { occupant: occupant }) => {
const { occupant } = props; const { occupant } = props;

View File

@@ -3,7 +3,7 @@ import { toFixed } from 'tgui-core/math';
import { damageRange, damages } from './constants'; import { damageRange, damages } from './constants';
import { mapTwoByTwo } from './functions'; import { mapTwoByTwo } from './functions';
import { occupant } from './types'; import type { occupant } from './types';
export const BodyScannerMainDamage = (props: { occupant: occupant }) => { export const BodyScannerMainDamage = (props: { occupant: occupant }) => {
const { occupant } = props; const { occupant } = props;

View File

@@ -9,7 +9,7 @@ import {
import { toFixed } from 'tgui-core/math'; import { toFixed } from 'tgui-core/math';
import { stats } from './constants'; import { stats } from './constants';
import { occupant } from './types'; import type { occupant } from './types';
export const BodyScannerMainOccupant = (props: { occupant: occupant }) => { export const BodyScannerMainOccupant = (props: { occupant: occupant }) => {
const { act } = useBackend(); const { act } = useBackend();

View File

@@ -10,7 +10,7 @@ import { toFixed } from 'tgui-core/math';
import { damageRange } from './constants'; import { damageRange } from './constants';
import { germStatus, reduceOrganStatus } from './functions'; import { germStatus, reduceOrganStatus } from './functions';
import { externalOrgan } from './types'; import type { externalOrgan } from './types';
export const BodyScannerMainOrgansExternal = (props: { export const BodyScannerMainOrgansExternal = (props: {
organs: externalOrgan[]; organs: externalOrgan[];

View File

@@ -3,7 +3,7 @@ import { toFixed } from 'tgui-core/math';
import { damageRange } from './constants'; import { damageRange } from './constants';
import { germStatus, reduceOrganStatus } from './functions'; import { germStatus, reduceOrganStatus } from './functions';
import { internalOrgan } from './types'; import type { internalOrgan } from './types';
export const BodyScannerMainOrgansInternal = (props: { export const BodyScannerMainOrgansInternal = (props: {
organs: internalOrgan[]; organs: internalOrgan[];

View File

@@ -1,6 +1,6 @@
import { Box, Section, Table } from 'tgui-core/components'; import { Box, Section, Table } from 'tgui-core/components';
import { occupant } from './types'; import type { occupant } from './types';
export const BodyScannerMainReagents = (props: { occupant: occupant }) => { export const BodyScannerMainReagents = (props: { occupant: occupant }) => {
const { occupant } = props; const { occupant } = props;

View File

@@ -1,5 +1,5 @@
import { Box } from 'tgui-core/components'; import { Box } from 'tgui-core/components';
import { BooleanLike } from 'tgui-core/react'; import type { BooleanLike } from 'tgui-core/react';
/* /*
*/ */

View File

@@ -3,7 +3,7 @@ import { Window } from 'tgui/layouts';
import { BodyScannerEmpty } from './BodyScannerEmpty'; import { BodyScannerEmpty } from './BodyScannerEmpty';
import { BodyScannerMain } from './BodyScannerMain'; import { BodyScannerMain } from './BodyScannerMain';
import { Data, occupant } from './types'; import type { Data, occupant } from './types';
export const BodyScanner = (props) => { export const BodyScanner = (props) => {
const { data } = useBackend<Data>(); const { data } = useBackend<Data>();

View File

@@ -9,7 +9,7 @@ import {
Section, Section,
Slider, Slider,
} from 'tgui-core/components'; } from 'tgui-core/components';
import { BooleanLike } from 'tgui-core/react'; import type { BooleanLike } from 'tgui-core/react';
type Data = { type Data = {
simulating: BooleanLike; simulating: BooleanLike;

View File

@@ -7,7 +7,7 @@ import {
NoticeBox, NoticeBox,
Section, Section,
} from 'tgui-core/components'; } from 'tgui-core/components';
import { BooleanLike } from 'tgui-core/react'; import type { BooleanLike } from 'tgui-core/react';
type Data = { type Data = {
activity: BooleanLike; activity: BooleanLike;

View File

@@ -7,7 +7,7 @@ import {
NoticeBox, NoticeBox,
Section, Section,
} from 'tgui-core/components'; } from 'tgui-core/components';
import { BooleanLike } from 'tgui-core/react'; import type { BooleanLike } from 'tgui-core/react';
type Data = { type Data = {
geneMasks: { tag: string; mask: string }[]; geneMasks: { tag: string; mask: string }[];

View File

@@ -3,7 +3,7 @@ import { Window } from 'tgui/layouts';
import { Button, NumberInput, Section, Stack } from 'tgui-core/components'; import { Button, NumberInput, Section, Stack } from 'tgui-core/components';
import { formatTime } from 'tgui-core/format'; import { formatTime } from 'tgui-core/format';
import { round } from 'tgui-core/math'; import { round } from 'tgui-core/math';
import { BooleanLike } from 'tgui-core/react'; import type { BooleanLike } from 'tgui-core/react';
type Data = { type Data = {
time_left: number; time_left: number;

View File

@@ -9,7 +9,7 @@ import {
Section, Section,
Stack, Stack,
} from 'tgui-core/components'; } from 'tgui-core/components';
import { BooleanLike, classes } from 'tgui-core/react'; import { type BooleanLike, classes } from 'tgui-core/react';
import { createSearch } from 'tgui-core/string'; import { createSearch } from 'tgui-core/string';
type activeCamera = { name: string; status: BooleanLike } | null; type activeCamera = { name: string; status: BooleanLike } | null;

View File

@@ -13,7 +13,7 @@ import {
} from 'tgui-core/components'; } from 'tgui-core/components';
import { formatSiUnit } from 'tgui-core/format'; import { formatSiUnit } from 'tgui-core/format';
import { toFixed } from 'tgui-core/math'; import { toFixed } from 'tgui-core/math';
import { BooleanLike } from 'tgui-core/react'; import type { BooleanLike } from 'tgui-core/react';
type Data = { type Data = {
connected: BooleanLike; connected: BooleanLike;

View File

@@ -3,7 +3,7 @@ import { useBackend } from 'tgui/backend';
import { Window } from 'tgui/layouts'; import { Window } from 'tgui/layouts';
import { Box, Button, Stack } from 'tgui-core/components'; import { Box, Button, Stack } from 'tgui-core/components';
import { clamp } from 'tgui-core/math'; import { clamp } from 'tgui-core/math';
import { BooleanLike } from 'tgui-core/react'; import type { BooleanLike } from 'tgui-core/react';
const PX_PER_UNIT = 24; const PX_PER_UNIT = 24;

View File

@@ -9,7 +9,7 @@ import {
Section, Section,
Table, Table,
} from 'tgui-core/components'; } from 'tgui-core/components';
import { BooleanLike } from 'tgui-core/react'; import type { BooleanLike } from 'tgui-core/react';
const getTagColor = (tag: string) => { const getTagColor = (tag: string) => {
switch (tag) { switch (tag) {

View File

@@ -3,7 +3,7 @@ import { Box, Button, Section } from 'tgui-core/components';
import { BeakerContents } from '../common/BeakerContents'; import { BeakerContents } from '../common/BeakerContents';
import { removeAmounts } from './constants'; import { removeAmounts } from './constants';
import { Data } from './types'; import type { Data } from './types';
export const ChemDispenserBeaker = (props) => { export const ChemDispenserBeaker = (props) => {
const { act, data } = useBackend<Data>(); const { act, data } = useBackend<Data>();

View File

@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
import { useBackend } from 'tgui/backend'; import { useBackend } from 'tgui/backend';
import { Button, Icon, Section, Stack, Tooltip } from 'tgui-core/components'; import { Button, Icon, Section, Stack, Tooltip } from 'tgui-core/components';
import { Data } from './types'; import type { Data } from './types';
export const ChemDispenserChemicals = (props) => { export const ChemDispenserChemicals = (props) => {
const { act, data } = useBackend<Data>(); const { act, data } = useBackend<Data>();

View File

@@ -1,7 +1,7 @@
import { useBackend } from 'tgui/backend'; import { useBackend } from 'tgui/backend';
import { Box, Button, Section, Stack } from 'tgui-core/components'; import { Box, Button, Section, Stack } from 'tgui-core/components';
import { Data } from './types'; import type { Data } from './types';
export const ChemDispenserRecipes = (props) => { export const ChemDispenserRecipes = (props) => {
const { act, data } = useBackend<Data>(); const { act, data } = useBackend<Data>();

View File

@@ -2,7 +2,7 @@ import { useBackend } from 'tgui/backend';
import { Button, LabeledList, Section, Slider } from 'tgui-core/components'; import { Button, LabeledList, Section, Slider } from 'tgui-core/components';
import { dispenseAmounts } from './constants'; import { dispenseAmounts } from './constants';
import { Data } from './types'; import type { Data } from './types';
export const ChemDispenserSettings = (props) => { export const ChemDispenserSettings = (props) => {
const { act, data } = useBackend<Data>(); const { act, data } = useBackend<Data>();

View File

@@ -6,7 +6,7 @@ import { ChemDispenserBeaker } from './ChemDispenserBeaker';
import { ChemDispenserChemicals } from './ChemDispenserChemicals'; import { ChemDispenserChemicals } from './ChemDispenserChemicals';
import { ChemDispenserRecipes } from './ChemDispenserRecipes'; import { ChemDispenserRecipes } from './ChemDispenserRecipes';
import { ChemDispenserSettings } from './ChemDispenserSettings'; import { ChemDispenserSettings } from './ChemDispenserSettings';
import { Data } from './types'; import type { Data } from './types';
export const ChemDispenser = (props) => { export const ChemDispenser = (props) => {
const { data } = useBackend<Data>(); const { data } = useBackend<Data>();

View File

@@ -1,7 +1,7 @@
import { useBackend } from 'tgui/backend'; import { useBackend } from 'tgui/backend';
import { Box, Button, LabeledList, Section } from 'tgui-core/components'; import { Box, Button, LabeledList, Section } from 'tgui-core/components';
import { Data, modalData } from './types'; import type { Data, modalData } from './types';
export const analyzeModalBodyOverride = (modal: modalData) => { export const analyzeModalBodyOverride = (modal: modalData) => {
const { act, data } = useBackend<Data>(); const { act, data } = useBackend<Data>();

View File

@@ -1,11 +1,11 @@
import { useBackend } from 'tgui/backend'; import { useBackend } from 'tgui/backend';
import { Box, Button, Section } from 'tgui-core/components'; import { Box, Button, Section } from 'tgui-core/components';
import { BooleanLike } from 'tgui-core/react'; import type { BooleanLike } from 'tgui-core/react';
import { BeakerContents } from '../common/BeakerContents'; import { BeakerContents } from '../common/BeakerContents';
import { modalOpen } from '../common/ComplexModal'; import { modalOpen } from '../common/ComplexModal';
import { transferAmounts } from './constants'; import { transferAmounts } from './constants';
import { reagent } from './types'; import type { reagent } from './types';
export const ChemMasterBeaker = (props: { export const ChemMasterBeaker = (props: {
beaker: BooleanLike; beaker: BooleanLike;

View File

@@ -1,11 +1,11 @@
import { useBackend } from 'tgui/backend'; import { useBackend } from 'tgui/backend';
import { Box, Button, Section } from 'tgui-core/components'; import { Box, Button, Section } from 'tgui-core/components';
import { BooleanLike } from 'tgui-core/react'; import type { BooleanLike } from 'tgui-core/react';
import { BeakerContents } from '../common/BeakerContents'; import { BeakerContents } from '../common/BeakerContents';
import { modalOpen } from '../common/ComplexModal'; import { modalOpen } from '../common/ComplexModal';
import { transferAmounts } from './constants'; import { transferAmounts } from './constants';
import { reagent } from './types'; import type { reagent } from './types';
export const ChemMasterBuffer = (props: { export const ChemMasterBuffer = (props: {
mode: BooleanLike; mode: BooleanLike;

View File

@@ -1,6 +1,6 @@
import { useBackend } from 'tgui/backend'; import { useBackend } from 'tgui/backend';
import { Box, Button, Section } from 'tgui-core/components'; import { Box, Button, Section } from 'tgui-core/components';
import { BooleanLike } from 'tgui-core/react'; import type { BooleanLike } from 'tgui-core/react';
import { modalOpen } from '../common/ComplexModal'; import { modalOpen } from '../common/ComplexModal';

View File

@@ -1,6 +1,6 @@
import { useBackend } from 'tgui/backend'; import { useBackend } from 'tgui/backend';
import { Button, Icon, Section, Stack } from 'tgui-core/components'; import { Button, Icon, Section, Stack } from 'tgui-core/components';
import { BooleanLike } from 'tgui-core/react'; import type { BooleanLike } from 'tgui-core/react';
import { ChemMasterProductionChemical } from './ChemMasterProductionChemical'; import { ChemMasterProductionChemical } from './ChemMasterProductionChemical';
import { ChemMasterProductionCondiment } from './ChemMasterProductionCondiment'; import { ChemMasterProductionCondiment } from './ChemMasterProductionCondiment';

View File

@@ -9,7 +9,7 @@ import { analyzeModalBodyOverride } from './ChemMasterAnalyzeModalBodyOverride';
import { ChemMasterBeaker } from './ChemMasterBeaker'; import { ChemMasterBeaker } from './ChemMasterBeaker';
import { ChemMasterBuffer } from './ChemMasterBuffer'; import { ChemMasterBuffer } from './ChemMasterBuffer';
import { ChemMasterProduction } from './ChemMasterProduction'; import { ChemMasterProduction } from './ChemMasterProduction';
import { Data } from './types'; import type { Data } from './types';
export const ChemMaster = (props) => { export const ChemMaster = (props) => {
const { data } = useBackend<Data>(); const { data } = useBackend<Data>();

View File

@@ -1,7 +1,7 @@
import { useBackend } from 'tgui/backend'; import { useBackend } from 'tgui/backend';
import { Window } from 'tgui/layouts'; import { Window } from 'tgui/layouts';
import { Box, Button, LabeledList, Section } from 'tgui-core/components'; import { Box, Button, LabeledList, Section } from 'tgui-core/components';
import { BooleanLike } from 'tgui-core/react'; import type { BooleanLike } from 'tgui-core/react';
type Data = { type Data = {
on: BooleanLike; on: BooleanLike;

View File

@@ -2,7 +2,7 @@ import { useBackend } from 'tgui/backend';
import { COLORS } from 'tgui/constants'; import { COLORS } from 'tgui/constants';
import { Box, Button, LabeledList, Section } from 'tgui-core/components'; import { Box, Button, LabeledList, Section } from 'tgui-core/components';
import { Data, modalData } from './types'; import type { Data, modalData } from './types';
export const viewRecordModalBodyOverride = (modal: modalData) => { export const viewRecordModalBodyOverride = (modal: modalData) => {
const { act, data } = useBackend<Data>(); const { act, data } = useBackend<Data>();

View File

@@ -1,7 +1,7 @@
import { useBackend } from 'tgui/backend'; import { useBackend } from 'tgui/backend';
import { Tabs } from 'tgui-core/components'; import { Tabs } from 'tgui-core/components';
import { Data } from './types'; import type { Data } from './types';
export const CloningConsoleNavigation = (props) => { export const CloningConsoleNavigation = (props) => {
const { act, data } = useBackend<Data>(); const { act, data } = useBackend<Data>();

View File

@@ -7,7 +7,7 @@ import {
Section, Section,
} from 'tgui-core/components'; } from 'tgui-core/components';
import { Data } from './types'; import type { Data } from './types';
export const CloningConsoleTemp = (props) => { export const CloningConsoleTemp = (props) => {
const { act, data } = useBackend<Data>(); const { act, data } = useBackend<Data>();

View File

@@ -12,7 +12,7 @@ import {
} from 'tgui-core/components'; } from 'tgui-core/components';
import { toFixed } from 'tgui-core/math'; import { toFixed } from 'tgui-core/math';
import { Data } from './types'; import type { Data } from './types';
export const CloningConsoleMain = (props) => { export const CloningConsoleMain = (props) => {
const { act, data } = useBackend<Data>(); const { act, data } = useBackend<Data>();

View File

@@ -16,7 +16,7 @@ import {
CloningConsoleMain, CloningConsoleMain,
CloningConsoleRecords, CloningConsoleRecords,
} from './CloningConsoleTabs'; } from './CloningConsoleTabs';
import { Data } from './types'; import type { Data } from './types';
export const CloningConsole = (props) => { export const CloningConsole = (props) => {
const { data } = useBackend<Data>(); const { data } = useBackend<Data>();

View File

@@ -2,7 +2,7 @@ import { useBackend } from 'tgui/backend';
import { Button, Slider, Table } from 'tgui-core/components'; import { Button, Slider, Table } from 'tgui-core/components';
import { toFixed } from 'tgui-core/math'; import { toFixed } from 'tgui-core/math';
import { Data } from './types'; import type { Data } from './types';
export const ColorMateTint = (props) => { export const ColorMateTint = (props) => {
const { act } = useBackend(); const { act } = useBackend();

View File

@@ -9,7 +9,7 @@ import {
} from 'tgui-core/components'; } from 'tgui-core/components';
import { toFixed } from 'tgui-core/math'; import { toFixed } from 'tgui-core/math';
import { Data } from './types'; import type { Data } from './types';
export const ColorMateMatrix = (props) => { export const ColorMateMatrix = (props) => {
const { act, data } = useBackend<Data>(); const { act, data } = useBackend<Data>();

View File

@@ -12,7 +12,7 @@ import {
import { ColorMateHSV, ColorMateTint } from './ColorMateColor'; import { ColorMateHSV, ColorMateTint } from './ColorMateColor';
import { ColorMateMatrix } from './ColorMateMatrix'; import { ColorMateMatrix } from './ColorMateMatrix';
import { Data } from './types'; import type { Data } from './types';
export const ColorMate = (props) => { export const ColorMate = (props) => {
const { act, data } = useBackend<Data>(); const { act, data } = useBackend<Data>();

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