Merge pull request #15759 from VOREStation/tgui_fix_2

Some input updates
This commit is contained in:
Heroman3003
2024-02-13 18:42:26 +10:00
committed by GitHub
44 changed files with 843 additions and 738 deletions
+2 -2
View File
@@ -682,7 +682,7 @@
var/list/selected = TLV["temperature"]
var/max_temperature = min(selected[3] - T0C, MAX_TEMPERATURE)
var/min_temperature = max(selected[2] - T0C, MIN_TEMPERATURE)
var/input_temperature = tgui_input_number(usr, "What temperature would you like the system to mantain? (Capped between [min_temperature] and [max_temperature]C)", "Thermostat Controls", target_temperature - T0C, max_temperature, min_temperature)
var/input_temperature = tgui_input_number(usr, "What temperature would you like the system to mantain? (Capped between [min_temperature] and [max_temperature]C)", "Thermostat Controls", target_temperature - T0C, max_temperature, min_temperature, round_value = FALSE)
if(isnum(input_temperature))
if(input_temperature > max_temperature || input_temperature < min_temperature)
to_chat(usr, "Temperature must be between [min_temperature]C and [max_temperature]C")
@@ -736,7 +736,7 @@
var/env = params["env"]
var/name = params["var"]
var/value = tgui_input_number(usr, "New [name] for [env]:", name, TLV[env][name])
var/value = tgui_input_number(usr, "New [name] for [env]:", name, TLV[env][name], round_value = FALSE)
if(!isnull(value) && !..())
if(value < 0)
TLV[env][name] = -1
+2 -2
View File
@@ -47,7 +47,7 @@
turn_off()
return
if(istype(I, /obj/item/device/multitool))
var/new_temp = tgui_input_number(usr, "Input a new target temperature, in degrees C.","Target Temperature", 20)
var/new_temp = tgui_input_number(usr, "Input a new target temperature, in degrees C.","Target Temperature", convert_k2c(target_temp), round_value = FALSE)
if(!Adjacent(user) || user.incapacitated())
return
new_temp = convert_c2k(new_temp)
@@ -157,4 +157,4 @@
#undef MODE_IDLE
#undef MODE_HEATING
#undef MODE_COOLING
#undef MODE_COOLING
@@ -14,7 +14,6 @@
S["tgui_fancy"] >> pref.tgui_fancy
S["tgui_lock"] >> pref.tgui_lock
S["tgui_input_mode"] >> pref.tgui_input_mode
S["tgui_input_lock"] >> pref.tgui_input_lock
S["tgui_large_buttons"] >> pref.tgui_large_buttons
S["tgui_swapped_buttons"] >> pref.tgui_swapped_buttons
S["obfuscate_key"] >> pref.obfuscate_key
@@ -34,7 +33,6 @@
S["tgui_fancy"] << pref.tgui_fancy
S["tgui_lock"] << pref.tgui_lock
S["tgui_input_mode"] << pref.tgui_input_mode
S["tgui_input_lock"] << pref.tgui_input_lock
S["tgui_large_buttons"] << pref.tgui_large_buttons
S["tgui_swapped_buttons"] << pref.tgui_swapped_buttons
S["obfuscate_key"] << pref.obfuscate_key
@@ -54,7 +52,6 @@
pref.tgui_fancy = sanitize_integer(pref.tgui_fancy, 0, 1, initial(pref.tgui_fancy))
pref.tgui_lock = sanitize_integer(pref.tgui_lock, 0, 1, initial(pref.tgui_lock))
pref.tgui_input_mode = sanitize_integer(pref.tgui_input_mode, 0, 1, initial(pref.tgui_input_mode))
pref.tgui_input_lock = sanitize_integer(pref.tgui_input_lock, 0, 1, initial(pref.tgui_input_lock))
pref.tgui_large_buttons = sanitize_integer(pref.tgui_large_buttons, 0, 1, initial(pref.tgui_large_buttons))
pref.tgui_swapped_buttons = sanitize_integer(pref.tgui_swapped_buttons, 0, 1, initial(pref.tgui_swapped_buttons))
pref.obfuscate_key = sanitize_integer(pref.obfuscate_key, 0, 1, initial(pref.obfuscate_key))
@@ -74,7 +71,6 @@
. += "<b>TGUI Window Mode:</b> <a href='?src=\ref[src];tgui_fancy=1'><b>[(pref.tgui_fancy) ? "Fancy (default)" : "Compatible (slower)"]</b></a><br>"
. += "<b>TGUI Window Placement:</b> <a href='?src=\ref[src];tgui_lock=1'><b>[(pref.tgui_lock) ? "Primary Monitor" : "Free (default)"]</b></a><br>"
. += "<b>TGUI Input Framework:</b> <a href='?src=\ref[src];tgui_input_mode=1'><b>[(pref.tgui_input_mode) ? "Enabled" : "Disabled (default)"]</b></a><br>"
. += "<b>TGUI Input Lock:</b> <a href='?src=\ref[src];tgui_input_lock=1'><b>[(pref.tgui_input_lock) ? "Enabled" : "Disabled (default)"]</b></a><br>"
. += "<b>TGUI Large Buttons:</b> <a href='?src=\ref[src];tgui_large_buttons=1'><b>[(pref.tgui_large_buttons) ? "Enabled (default)" : "Disabled"]</b></a><br>"
. += "<b>TGUI Swapped Buttons:</b> <a href='?src=\ref[src];tgui_swapped_buttons=1'><b>[(pref.tgui_swapped_buttons) ? "Enabled" : "Disabled (default)"]</b></a><br>"
. += "<b>Obfuscate Ckey:</b> <a href='?src=\ref[src];obfuscate_key=1'><b>[(pref.obfuscate_key) ? "Enabled" : "Disabled (default)"]</b></a><br>"
@@ -154,10 +150,6 @@
pref.tgui_input_mode = !pref.tgui_input_mode
return TOPIC_REFRESH
else if(href_list["tgui_input_lock"])
pref.tgui_input_lock = !pref.tgui_input_lock
return TOPIC_REFRESH
else if(href_list["tgui_large_buttons"])
pref.tgui_large_buttons = !pref.tgui_large_buttons
return TOPIC_REFRESH
-1
View File
@@ -29,7 +29,6 @@ var/list/preferences_datums = list()
var/tgui_fancy = TRUE
var/tgui_lock = FALSE
var/tgui_input_mode = FALSE // All the Input Boxes (Text,Number,List,Alert)
var/tgui_input_lock = FALSE
var/tgui_large_buttons = TRUE
var/tgui_swapped_buttons = FALSE
var/obfuscate_key = FALSE
@@ -379,17 +379,6 @@
You will have to reload TGChat and/or reconnect to the server for these changes to take place. \
TGChat message persistence is not guaranteed if you change this again before the start of the next round.")
/client/verb/toggle_tgui_inputlock()
set name = "Toggle TGUI Input Lock"
set category = "Preferences"
set desc = "Toggles whether or not pressing the 'Enter' key in TGUI input sends the message or creates a new line."
prefs.tgui_input_lock = !prefs.tgui_input_lock //There is no preference datum for tgui input lock, nor for any TGUI prefs.
SScharacter_setup.queue_preferences_save(prefs)
to_chat(src, span_notice("You have toggled TGUI input lock: [prefs.tgui_input_lock ? "ON" : "OFF"] \n \
This setting determines whether pressing enter on TGUI input sends the input, or creates a newline."))
/client/verb/toggle_chat_timestamps()
set name = "Toggle Chat Timestamps"
set category = "Preferences"
@@ -204,7 +204,7 @@
OutputBeaker = null
if("adjust temp")
target_temp = tgui_input_number(usr, "Choose a target temperature.", "Temperature.", T20C)
target_temp = tgui_input_number(usr, "Choose a target temperature.", "Temperature.", T20C, round_value = FALSE)
target_temp = CLAMP(target_temp, min_temp, max_temp)
update_icon()
+1 -1
View File
@@ -438,7 +438,7 @@
/* END ENGINES */
/* SENSORS */
if("range")
var/nrange = tgui_input_number(usr, "Set new sensors range", "Sensor range", sensors.range)
var/nrange = tgui_input_number(usr, "Set new sensors range", "Sensor range", sensors.range, round_value = FALSE)
if(nrange)
sensors.set_range(CLAMP(nrange, 1, world.view))
. = TRUE
+7 -3
View File
@@ -14,13 +14,17 @@
if (!user)
user = usr
if(!length(items))
return
return null
if (!istype(user))
if (istype(user, /client))
var/client/client = user
user = client.mob
else
return
return null
if(isnull(user.client))
return null
if(!user.client.prefs.tgui_input_mode)
return input(user, message, title) as null|anything in items
var/datum/tgui_checkbox_input/input = new(user, message, title, items, min_checked, max_checked, timeout, ui_state)
@@ -66,7 +70,7 @@
start_time = world.time
QDEL_IN(src, timeout)
/datum/tgui_checkbox_input/Destroy(force, ...)
/datum/tgui_checkbox_input/Destroy(force)
SStgui.close_uis(src)
state = null
QDEL_NULL(items)
+29 -84
View File
@@ -7,28 +7,31 @@
* * message - The content of the input box, shown in the body of the TGUI window.
* * title - The title of the input box, shown on the top of the TGUI window.
* * items - The options that can be chosen by the user, each string is assigned a button on the UI.
* * default - The option with this value will be selected on first paint of the TGUI window.
* * timeout - The timeout of the input box, after which the input box will close and qdel itself. Set to zero for no timeout.
* * strict_modern - Disabled the preference check of the input box, only allowing the TGUI window to show.
* * default - If an option is already preselected on the UI. Current values, etc.
* * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout.
*/
/proc/tgui_input_list(mob/user, message, title = "Select", list/items, default, timeout = 0, strict_modern = FALSE)
if (istext(user))
stack_trace("tgui_alert() received text for user instead of mob")
return
/proc/tgui_input_list(mob/user, message, title = "Select", list/items, default, timeout = 0, strict_modern = FALSE, ui_state = GLOB.tgui_always_state)
if (!user)
user = usr
if(!length(items))
return
return null
if (!istype(user))
if (istype(user, /client))
var/client/client = user
user = client.mob
else
return
return null
if(isnull(user.client))
return null
/// Client does NOT have tgui_input on: Returns regular input
if(!user.client.prefs.tgui_input_mode && !strict_modern)
return input(user, message, title, default) as null|anything in items
var/datum/tgui_list_input/input = new(user, message, title, items, default, timeout)
var/datum/tgui_list_input/input = new(user, message, title, items, default, timeout, ui_state)
if(input.invalid)
qdel(input)
return
input.tgui_interact(user)
input.wait()
if (input)
@@ -48,11 +51,11 @@
var/message
/// The list of items (responses) provided on the TGUI window
var/list/items
/// Items (strings specifically) mapped to the actual value (e.g. a mob or a verb)
/// Buttons (strings specifically) mapped to the actual value (e.g. a mob or a verb)
var/list/items_map
/// The button that the user has pressed, null if no selection has been made
var/choice
/// The default item to be selected
/// The default button to be selected
var/default
/// The time at which the tgui_list_input was created, for displaying timeout progress.
var/start_time
@@ -60,41 +63,42 @@
var/timeout
/// Boolean field describing if the tgui_list_input was closed by the user.
var/closed
/// The TGUI UI state that will be returned in ui_state(). Default: always_state
var/datum/tgui_state/state
/// Whether the tgui list input is invalid or not (i.e. due to all list entries being null)
var/invalid = FALSE
/datum/tgui_list_input/New(mob/user, message, title, list/items, default, timeout)
/datum/tgui_list_input/New(mob/user, message, title, list/items, default, timeout, ui_state)
src.title = title
src.message = message
src.items = list()
src.items_map = list()
src.default = default
src.state = ui_state
var/list/repeat_items = list()
// Gets rid of illegal characters
var/static/regex/whitelistedWords = regex(@{"([^\u0020-\u8000]+)"})
for(var/i in items)
if(isnull(i))
stack_trace("Null in a tgui_input_list() items")
if(!i)
continue
var/string_key = whitelistedWords.Replace("[i]", "")
//avoids duplicated keys E.g: when areas have the same name
string_key = avoid_assoc_duplicate_keys(string_key, repeat_items)
src.items += string_key
src.items_map[string_key] = i
if(length(src.items) == 0)
invalid = TRUE
if (timeout)
src.timeout = timeout
start_time = world.time
QDEL_IN(src, timeout)
/datum/tgui_list_input/Destroy(force, ...)
/datum/tgui_list_input/Destroy(force)
SStgui.close_uis(src)
state = null
QDEL_NULL(items)
. = ..()
return ..()
/**
* Waits for a user's response to the tgui_list_input's prompt before returning. Returns early if
@@ -115,7 +119,7 @@
closed = TRUE
/datum/tgui_list_input/tgui_state(mob/user)
return GLOB.tgui_always_state
return state
/datum/tgui_list_input/tgui_static_data(mob/user)
var/list/data = list()
@@ -146,68 +150,9 @@
SStgui.close_uis(src)
return TRUE
if("cancel")
SStgui.close_uis(src)
closed = TRUE
SStgui.close_uis(src)
return TRUE
/datum/tgui_list_input/proc/set_choice(choice)
src.choice = choice
/**
* Creates an asynchronous TGUI input list window with an associated callback.
*
* This proc should be used to create inputs that invoke a callback with the user's chosen option.
* Arguments:
* * user - The user to show the input box to.
* * message - The content of the input box, shown in the body of the TGUI window.
* * title - The title of the input box, shown on the top of the TGUI window.
* * items - The options that can be chosen by the user, each string is assigned a button on the UI.
* * default - The option with this value will be selected on first paint of the TGUI window.
* * callback - The callback to be invoked when a choice is made.
* * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout.
*/
/proc/tgui_input_list_async(mob/user, message, title, list/items, default, datum/callback/callback, timeout = 60 SECONDS)
if (istext(user))
stack_trace("tgui_alert() received text for user instead of mob")
return
if (!user)
user = usr
if(!length(items))
return
if (!istype(user))
if (istype(user, /client))
var/client/client = user
user = client.mob
else
return
var/datum/tgui_list_input/async/input = new(user, message, title, items, default, callback, timeout)
input.tgui_interact(user)
/**
* # async tgui_list_input
*
* An asynchronous version of tgui_list_input to be used with callbacks instead of waiting on user responses.
*/
/datum/tgui_list_input/async
/// The callback to be invoked by the tgui_list_input upon having a choice made.
var/datum/callback/callback
/datum/tgui_list_input/async/New(mob/user, message, title, list/items, default, callback, timeout)
..(user, title, message, items, default, timeout)
src.callback = callback
/datum/tgui_list_input/async/Destroy(force, ...)
QDEL_NULL(callback)
. = ..()
/datum/tgui_list_input/async/tgui_close(mob/user)
. = ..()
qdel(src)
/datum/tgui_list_input/async/set_choice(choice)
. = ..()
if(!isnull(src.choice))
callback?.InvokeAsync(src.choice)
/datum/tgui_list_input/async/wait()
return
+16 -65
View File
@@ -15,7 +15,7 @@
* * timeout - The timeout of the number input, after which the modal will close and qdel itself. Set to zero for no timeout.
* * round_value - whether the inputted number is rounded down into an integer.
*/
/proc/tgui_input_number(mob/user, message, title = "Number Input", default = 0, max_value = INFINITY, min_value = -INFINITY, timeout = 0, round_value = FALSE)
/proc/tgui_input_number(mob/user, message, title = "Number Input", default = 0, max_value = INFINITY, min_value = -INFINITY, timeout = 0, round_value = TRUE, ui_state = GLOB.tgui_always_state)
if (!user)
user = usr
if (!istype(user))
@@ -23,12 +23,16 @@
var/client/client = user
user = client.mob
else
return
return null
if (isnull(user.client))
return null
// Client does NOT have tgui_input on: Returns regular input
if(!user.client.prefs.tgui_input_mode)
var/input_number = input(user, message, title, default) as null|num
return clamp(round_value ? round(input_number) : input_number, min_value, max_value)
var/datum/tgui_input_number/number_input = new(user, message, title, default, max_value, min_value, timeout, round_value)
var/datum/tgui_input_number/number_input = new(user, message, title, default, max_value, min_value, timeout, round_value, ui_state)
number_input.tgui_interact(user)
number_input.wait()
if (number_input)
@@ -62,14 +66,17 @@
var/timeout
/// The title of the TGUI window
var/title
/// The TGUI UI state that will be returned in ui_state(). Default: always_state
var/datum/tgui_state/state
/datum/tgui_input_number/New(mob/user, message, title, default, max_value, min_value, timeout, round_value)
/datum/tgui_input_number/New(mob/user, message, title, default, max_value, min_value, timeout, round_value, ui_state)
src.default = default
src.max_value = max_value
src.message = message
src.min_value = min_value
src.title = title
src.round_value = round_value
src.state = ui_state
if (timeout)
src.timeout = timeout
start_time = world.time
@@ -85,8 +92,9 @@
if(default > max_value)
CRASH("Default value is greater than max value.")
/datum/tgui_input_number/Destroy(force, ...)
/datum/tgui_input_number/Destroy(force)
SStgui.close_uis(src)
state = null
return ..()
/**
@@ -108,7 +116,7 @@
closed = TRUE
/datum/tgui_input_number/tgui_state(mob/user)
return GLOB.tgui_always_state
return state
/datum/tgui_input_number/tgui_static_data(mob/user)
var/list/data = list()
@@ -119,6 +127,7 @@
data["min_value"] = min_value
data["swapped_buttons"] = !user.client.prefs.tgui_swapped_buttons
data["title"] = title
data["round_value"] = round_value
return data
/datum/tgui_input_number/tgui_data(mob/user)
@@ -135,8 +144,7 @@
if("submit")
if(!isnum(params["entry"]))
CRASH("A non number was input into tgui input number by [usr]")
//var/choice = round_value ? round(params["entry"]) : params["entry"]
var/choice = params["entry"]
var/choice = round_value ? round(params["entry"]) : params["entry"]
if(choice > max_value)
CRASH("A number greater than the max value was input into tgui input number by [usr]")
if(choice < min_value)
@@ -152,60 +160,3 @@
/datum/tgui_input_number/proc/set_entry(entry)
src.entry = entry
/**
* Creates an asynchronous TGUI input num window with an associated callback.
*
* This proc should be used to create inputs that invoke a callback with the user's chosen option.
* Arguments:
* * user - The user to show the input box to.
* * message - The content of the input box, shown in the body of the TGUI window.
* * title - The title of the input box, shown on the top of the TGUI window.
* * default - The default value pre-populated in the input box.
* * callback - The callback to be invoked when a choice is made.
* * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout.
* * round_value - whether the inputted number is rounded down into an integer.
*/
/proc/tgui_input_number_async(mob/user, message, title, default, datum/callback/callback, timeout = 60 SECONDS, round_value = FALSE)
if (istext(user))
stack_trace("tgui_input_num_async() received text for user instead of mob")
return
if (!user)
user = usr
if (!istype(user))
if (istype(user, /client))
var/client/client = user
user = client.mob
else
return
var/datum/tgui_input_number/async/input = new(user, message, title, default, callback, timeout, round_value)
input.tgui_interact(user)
/**
* # async tgui_text_input
*
* An asynchronous version of tgui_text_input to be used with callbacks instead of waiting on user responses.
*/
/datum/tgui_input_number/async
/// The callback to be invoked by the tgui_text_input upon having a choice made.
var/datum/callback/callback
/datum/tgui_input_number/async/New(mob/user, message, title, default, callback, timeout, round_value)
..(user, title, message, default, timeout, round_value)
src.callback = callback
/datum/tgui_input_number/async/Destroy(force, ...)
QDEL_NULL(callback)
. = ..()
/datum/tgui_input_number/async/tgui_close(mob/user)
. = ..()
qdel(src)
/datum/tgui_input_number/async/set_entry(entry)
. = ..()
if(!isnull(src.entry))
callback?.InvokeAsync(src.entry)
/datum/tgui_input_number/async/wait()
return
+21 -78
View File
@@ -16,9 +16,6 @@
* * timeout - The timeout of the textbox, after which the modal will close and qdel itself. Set to zero for no timeout.
*/
/proc/tgui_input_text(mob/user, message = "", title = "Text Input", default, max_length = INFINITY, multiline = FALSE, encode = FALSE, timeout = 0, prevent_enter = FALSE)
if (istext(user))
stack_trace("tgui_input_text() received text for user instead of mob")
return
if (!user)
user = usr
if (!istype(user))
@@ -27,6 +24,10 @@
user = client.mob
else
return
if(isnull(user.client))
return null
// Client does NOT have tgui_input on: Returns regular input
if(!user.client.prefs.tgui_input_mode)
if(encode)
@@ -40,11 +41,7 @@
else
return input(user, message, title, default) as text|null
//Client has TGUI input lock on; override whatever prevent_enter was specified beforehand
if(user.client.prefs.tgui_input_lock)
prevent_enter = TRUE
var/datum/tgui_input_text/text_input = new(user, message, title, default, max_length, multiline, encode, timeout, prevent_enter)
var/datum/tgui_input_text/text_input = new(user, message, title, default, max_length, multiline, encode, timeout)
text_input.tgui_interact(user)
text_input.wait()
if (text_input)
@@ -79,9 +76,7 @@
/// The title of the TGUI window
var/title
var/prevent_enter
/datum/tgui_input_text/New(mob/user, message, title, default, max_length, multiline, encode, timeout, prevent_enter)
/datum/tgui_input_text/New(mob/user, message, title, default, max_length, multiline, encode, timeout)
src.default = default
src.encode = encode
src.max_length = max_length
@@ -92,18 +87,17 @@
src.timeout = timeout
start_time = world.time
QDEL_IN(src, timeout)
src.prevent_enter = prevent_enter
/datum/tgui_input_text/Destroy(force, ...)
/datum/tgui_input_text/Destroy(force)
SStgui.close_uis(src)
. = ..()
return ..()
/**
* Waits for a user's response to the tgui_text_input's prompt before returning. Returns early if
* the window was closed by the user.
*/
/datum/tgui_input_text/proc/wait()
while (!entry && !closed)
while (!entry && !closed && !QDELETED(src))
stoplag(1)
/datum/tgui_input_text/tgui_interact(mob/user, datum/tgui/ui)
@@ -128,7 +122,6 @@
data["placeholder"] = default // Default is a reserved keyword
data["swapped_buttons"] = !user.client.prefs.tgui_swapped_buttons
data["title"] = title
data["prevent_enter"] = prevent_enter
return data
/datum/tgui_input_text/tgui_data(mob/user)
@@ -143,77 +136,27 @@
return
switch(action)
if("submit")
if(length(params["entry"]) > max_length)
return
if(encode && (length(html_encode(params["entry"])) > max_length))
to_chat(usr, span_notice("Your message was clipped due to special character usage."))
if(max_length)
if(length(params["entry"]) > max_length)
CRASH("[usr] typed a text string longer than the max length")
if(encode && (length(html_encode(params["entry"])) > max_length))
to_chat(usr, span_notice("Your message was clipped due to special character usage."))
set_entry(params["entry"])
closed = TRUE
SStgui.close_uis(src)
return TRUE
if("cancel")
SStgui.close_uis(src)
closed = TRUE
SStgui.close_uis(src)
return TRUE
/**
* Sets the return value for the tgui text proc.
* If html encoding is enabled, the text will be encoded.
* This can sometimes result in a string that is longer than the max length.
* If the string is longer than the max length, it will be clipped.
*/
/datum/tgui_input_text/proc/set_entry(entry)
if(!isnull(entry))
var/converted_entry = encode ? html_encode(entry) : entry
//converted_entry = readd_quotes(converted_entry)
src.entry = trim(converted_entry, max_length)
/**
* Creates an asynchronous TGUI input text window with an associated callback.
*
* This proc should be used to create inputs that invoke a callback with the user's chosen option.
* Arguments:
* * user - The user to show the input box to.
* * message - The content of the input box, shown in the body of the TGUI window.
* * title - The title of the input box, shown on the top of the TGUI window.
* * default - The default value pre-populated in the input box.
* * callback - The callback to be invoked when a choice is made.
* * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout.
*/
/proc/tgui_input_text_async(mob/user, message, title, default, datum/callback/callback, max_length, multiline, encode, timeout = 60 SECONDS)
if (istext(user))
stack_trace("tgui_input_text_async() received text for user instead of mob")
return
if (!user)
user = usr
if (!istype(user))
if (istype(user, /client))
var/client/client = user
user = client.mob
else
return
var/datum/tgui_input_text/async/input = new(user, message, title, default, callback, max_length, multiline, encode, timeout)
input.tgui_interact(user)
/**
* # async tgui_text_input
*
* An asynchronous version of tgui_text_input to be used with callbacks instead of waiting on user responses.
*/
/datum/tgui_input_text/async
/// The callback to be invoked by the tgui_text_input upon having a choice made.
var/datum/callback/callback
/datum/tgui_input_text/async/New(mob/user, message, title, default, callback, max_length, multiline, encode, timeout)
..(user, title, message, default, max_length, multiline, encode, timeout)
src.callback = callback
/datum/tgui_input_text/async/Destroy(force, ...)
QDEL_NULL(callback)
. = ..()
/datum/tgui_input_text/async/tgui_close(mob/user)
. = ..()
qdel(src)
/datum/tgui_input_text/async/set_entry(entry)
. = ..()
if(!isnull(src.entry))
callback?.InvokeAsync(src.entry)
/datum/tgui_input_text/async/wait()
return
+1 -1
View File
@@ -8,7 +8,7 @@ logFilters:
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
spec: '@yarnpkg/plugin-interactive-tools'
pnpEnableEsmLoader: false
+3 -1
View File
@@ -17,7 +17,9 @@
"tgui:test": "jest --watch",
"tgui:test-simple": "CI=true jest --color",
"tgui:test-ci": "CI=true jest --color --collect-coverage",
"tgui:tsc": "tsc"
"tgui:tsc": "tsc",
"tgui:prettier-fix": "prettier --write .",
"tgui:eslint-fix": "eslint --fix packages --ext .js,.cjs,.ts,.tsx"
},
"dependencies": {
"@swc/core": "^1.3.100",
+1 -1
View File
@@ -95,7 +95,7 @@ export const applyMiddleware = (
return (reducer, ...args): Store => {
const store = createStoreFunction(reducer, ...args);
let dispatch: Dispatch = () => {
let dispatch: Dispatch = (action, ...args) => {
throw new Error(
'Dispatching while constructing your middleware is not allowed.',
);
+63 -78
View File
@@ -5,72 +5,56 @@
*/
import { BooleanLike, classes } from 'common/react';
import { createElement, ReactNode } from 'react';
import {
createElement,
KeyboardEventHandler,
MouseEventHandler,
ReactNode,
UIEventHandler,
} from 'react';
import { CSS_COLORS } from '../constants';
import { logger } from '../logging';
export type BoxProps = {
[key: string]: any;
as?: string;
className?: string | BooleanLike;
children?: ReactNode;
position?: string | BooleanLike;
overflow?: string | BooleanLike;
overflowX?: string | BooleanLike;
overflowY?: string | BooleanLike;
top?: string | BooleanLike;
bottom?: string | BooleanLike;
left?: string | BooleanLike;
right?: string | BooleanLike;
width?: string | BooleanLike;
minWidth?: string | BooleanLike;
maxWidth?: string | BooleanLike;
height?: string | BooleanLike;
minHeight?: string | BooleanLike;
maxHeight?: string | BooleanLike;
fontSize?: string | BooleanLike;
fontFamily?: string;
lineHeight?: string | BooleanLike;
opacity?: number;
textAlign?: string | BooleanLike;
verticalAlign?: string | BooleanLike;
textTransform?: string | BooleanLike; // VOREStation Addition
inline?: BooleanLike;
bold?: BooleanLike;
italic?: BooleanLike;
nowrap?: BooleanLike;
preserveWhitespace?: BooleanLike;
m?: string | BooleanLike;
mx?: string | BooleanLike;
my?: string | BooleanLike;
mt?: string | BooleanLike;
mb?: string | BooleanLike;
ml?: string | BooleanLike;
mr?: string | BooleanLike;
p?: string | BooleanLike;
px?: string | BooleanLike;
py?: string | BooleanLike;
pt?: string | BooleanLike;
pb?: string | BooleanLike;
pl?: string | BooleanLike;
pr?: string | BooleanLike;
color?: string | BooleanLike;
textColor?: string | BooleanLike;
backgroundColor?: string | BooleanLike;
// VOREStation Addition Start
// Flex props
flexGrow?: string | BooleanLike;
flexWrap?: string | BooleanLike;
flexBasis?: string | BooleanLike;
flex?: string | BooleanLike;
// VOREStation Addition End
fillPositionedParent?: boolean;
type BooleanProps = Partial<Record<keyof typeof booleanStyleMap, boolean>>;
type StringProps = Partial<
Record<keyof typeof stringStyleMap, string | BooleanLike>
>;
export type EventHandlers = Partial<{
onClick: MouseEventHandler<HTMLDivElement>;
onContextMenu: MouseEventHandler<HTMLDivElement>;
onDoubleClick: MouseEventHandler<HTMLDivElement>;
onKeyDown: KeyboardEventHandler<HTMLDivElement>;
onKeyUp: KeyboardEventHandler<HTMLDivElement>;
onMouseDown: MouseEventHandler<HTMLDivElement>;
onMouseMove: MouseEventHandler<HTMLDivElement>;
onMouseOver: MouseEventHandler<HTMLDivElement>;
onMouseUp: MouseEventHandler<HTMLDivElement>;
onScroll: UIEventHandler<HTMLDivElement>;
}>;
export type BoxProps = Partial<{
as: string;
children: ReactNode;
className: string | BooleanLike;
style: Partial<CSSStyleDeclaration>;
}> &
BooleanProps &
StringProps &
EventHandlers;
// Don't you dare put this elsewhere
type DangerDoNotUse = {
dangerouslySetInnerHTML?: {
__html: any;
};
};
/**
* Coverts our rem-like spacing unit into a CSS unit.
*/
export const unit = (value: unknown): string | undefined => {
export const unit = (value: unknown) => {
if (typeof value === 'string') {
// Transparently convert pixels into rem units
if (value.endsWith('px')) {
@@ -86,7 +70,7 @@ export const unit = (value: unknown): string | undefined => {
/**
* Same as `unit`, but half the size for integers numbers.
*/
export const halfUnit = (value: unknown): string | undefined => {
export const halfUnit = (value: unknown) => {
if (typeof value === 'string') {
return unit(value);
}
@@ -98,7 +82,7 @@ export const halfUnit = (value: unknown): string | undefined => {
const isColorCode = (str: unknown) => !isColorClass(str);
const isColorClass = (str: unknown): boolean => {
return typeof str === 'string' && CSS_COLORS.includes(str);
return typeof str === 'string' && CSS_COLORS.includes(str as any);
};
const mapRawPropTo = (attrName) => (style, value) => {
@@ -135,9 +119,12 @@ const mapColorPropTo = (attrName) => (style, value) => {
// String / number props
const stringStyleMap = {
align: mapRawPropTo('textAlign'),
bottom: mapUnitPropTo('bottom', unit),
colSpan: mapRawPropTo('colSpan'),
fontFamily: mapRawPropTo('fontFamily'),
fontSize: mapUnitPropTo('fontSize', unit),
fontWeight: mapRawPropTo('fontWeight'),
height: mapUnitPropTo('height', unit),
left: mapUnitPropTo('left', unit),
maxHeight: mapUnitPropTo('maxHeight', unit),
@@ -162,22 +149,19 @@ const stringStyleMap = {
style['lineHeight'] = unit(value);
}
},
textTransform: mapRawPropTo('text-transform'), // VOREStation Addition
// Margins
// Margin
m: mapDirectionalUnitPropTo('margin', halfUnit, [
'Top',
'Bottom',
'Left',
'Right',
]),
mx: mapDirectionalUnitPropTo('margin', halfUnit, ['Left', 'Right']),
my: mapDirectionalUnitPropTo('margin', halfUnit, ['Top', 'Bottom']),
mt: mapUnitPropTo('marginTop', halfUnit),
mb: mapUnitPropTo('marginBottom', halfUnit),
ml: mapUnitPropTo('marginLeft', halfUnit),
mr: mapUnitPropTo('marginRight', halfUnit),
mt: mapUnitPropTo('marginTop', halfUnit),
mx: mapDirectionalUnitPropTo('margin', halfUnit, ['Left', 'Right']),
my: mapDirectionalUnitPropTo('margin', halfUnit, ['Top', 'Bottom']),
// Padding
p: mapDirectionalUnitPropTo('padding', halfUnit, [
'Top',
@@ -185,12 +169,12 @@ const stringStyleMap = {
'Left',
'Right',
]),
px: mapDirectionalUnitPropTo('padding', halfUnit, ['Left', 'Right']),
py: mapDirectionalUnitPropTo('padding', halfUnit, ['Top', 'Bottom']),
pt: mapUnitPropTo('paddingTop', halfUnit),
pb: mapUnitPropTo('paddingBottom', halfUnit),
pl: mapUnitPropTo('paddingLeft', halfUnit),
pr: mapUnitPropTo('paddingRight', halfUnit),
pt: mapUnitPropTo('paddingTop', halfUnit),
px: mapDirectionalUnitPropTo('padding', halfUnit, ['Left', 'Right']),
py: mapDirectionalUnitPropTo('padding', halfUnit, ['Top', 'Bottom']),
// Color props
color: mapColorPropTo('color'),
textColor: mapColorPropTo('color'),
@@ -203,8 +187,11 @@ const stringStyleMap = {
flexBasis: mapRawPropTo('flex-basis'),
flex: mapRawPropTo('flex'),
// VOREStation Addition End
} as const;
// Utility props
// Boolean props
const booleanStyleMap = {
bold: mapBooleanPropTo('fontWeight', 'bold'),
fillPositionedParent: (style, value) => {
if (value) {
style['position'] = 'absolute';
@@ -214,11 +201,6 @@ const stringStyleMap = {
style['right'] = 0;
}
},
} as const;
// Boolean props
const booleanStyleMap = {
bold: mapBooleanPropTo('fontWeight', 'bold'),
inline: mapBooleanPropTo('display', 'inline-block'),
italic: mapBooleanPropTo('fontStyle', 'italic'),
nowrap: mapBooleanPropTo('whiteSpace', 'nowrap'),
@@ -262,7 +244,7 @@ export const computeBoxClassName = (props: BoxProps) => {
]);
};
export const Box = (props: BoxProps) => {
export const Box = (props: BoxProps & DangerDoNotUse) => {
const { as = 'div', className, children, ...rest } = props;
// Compute class name and styles
@@ -270,8 +252,11 @@ export const Box = (props: BoxProps) => {
? `${className} ${computeBoxClassName(rest)}`
: computeBoxClassName(rest);
const computedProps = computeBoxProps(rest);
if (as === 'img') {
computedProps.style['-ms-interpolation-mode'] = 'nearest-neighbor';
logger.error(
'Box component cannot be used as an image. Use Image component instead.',
);
}
// Render the component
+101 -68
View File
@@ -5,20 +5,24 @@
*/
import { canRender, classes } from 'common/react';
import { createRef, ReactNode, RefObject, useEffect } from 'react';
import { forwardRef, ReactNode, RefObject, useEffect } from 'react';
import { addScrollableNode, removeScrollableNode } from '../events';
import { BoxProps, computeBoxClassName, computeBoxProps } from './Box';
export type SectionProps = Partial<{
type Props = Partial<{
/** Buttons to render aside the section title. */
buttons: ReactNode;
/** If true, fills all available vertical space. */
fill: boolean;
/** If true, removes all section padding. */
fitted: boolean;
/** Shows or hides the scrollbar. */
scrollable: boolean;
/** Shows or hides the horizontal scrollbar. */
scrollableHorizontal: boolean;
/** Title of the section. */
title: ReactNode;
/** @member Allows external control of scrolling. */
scrollableRef: RefObject<HTMLDivElement>;
/** @member Callback function for the `scroll` event */
onScroll: ((this: GlobalEventHandlers, ev: Event) => any) | null;
@@ -28,73 +32,102 @@ export type SectionProps = Partial<{
}> &
BoxProps;
export const Section = (props: SectionProps) => {
const {
className,
title,
buttons,
fill,
fitted,
scrollable,
scrollableHorizontal,
flexGrow, // VOREStation Addition
noTopPadding, // VOREStation Addition
stretchContents, // VOREStation Addition
children,
onScroll,
...rest
} = props;
/**
* ## Section
* Section is a surface that displays content and actions on a single topic.
*
* They should be easy to scan for relevant and actionable information.
* Elements, like text and images, should be placed in them in a way that
* clearly indicates hierarchy.
*
* Sections can now be nested, and will automatically font size of the
* header according to their nesting level. Previously this was done via `level`
* prop, but now it is automatically calculated.
*
* Section can also be titled to clearly define its purpose.
*
* ```tsx
* <Section title="Cargo">Here you can order supply crates.</Section>
* ```
*
* If you want to have a button on the right side of an section title
* (for example, to perform some sort of action), there is a way to do that:
*
* ```tsx
* <Section title="Cargo" buttons={<Button>Send shuttle</Button>}>
* Here you can order supply crates.
* </Section>
* ```
*/
export const Section = forwardRef(
(props: Props, forwardedRef: RefObject<HTMLDivElement>) => {
const {
buttons,
children,
className,
fill,
fitted,
onScroll,
scrollable,
scrollableHorizontal,
title,
flexGrow, // VOREStation Addition
noTopPadding, // VOREStation Addition
stretchContents, // VOREStation Addition
...rest
} = props;
const scrollableRef = props.scrollableRef || createRef();
const hasTitle = canRender(title) || canRender(buttons);
const hasTitle = canRender(title) || canRender(buttons);
useEffect(() => {
if (scrollable || scrollableHorizontal) {
addScrollableNode(scrollableRef.current as HTMLElement);
if (onScroll && scrollableRef.current) {
scrollableRef.current.onscroll = onScroll;
}
}
/** We want to be able to scroll on hover, but using focus will steal it from inputs */
useEffect(() => {
if (!forwardedRef?.current) return;
if (!scrollable && !scrollableHorizontal) return;
return () => {
if (scrollable || scrollableHorizontal) {
removeScrollableNode(scrollableRef.current as HTMLElement);
}
};
}, []);
addScrollableNode(forwardedRef.current);
return (
<div
className={classes([
'Section',
fill && 'Section--fill',
fitted && 'Section--fitted',
scrollable && 'Section--scrollable',
scrollableHorizontal && 'Section--scrollableHorizontal',
flexGrow && 'Section--flex', // VOREStation Addition
className,
computeBoxClassName(rest),
])}
{...computeBoxProps(rest)}
>
{hasTitle && (
<div className="Section__title">
<span className="Section__titleText">{title}</span>
<div className="Section__buttons">{buttons}</div>
</div>
)}
<div className="Section__rest">
<div
onScroll={onScroll as any}
className={classes([
'Section__content',
!!stretchContents && 'Section__content--stretchContents', // VOREStation Addition
!!noTopPadding && 'Section__content--noTopPadding', // VOREStation Addition
])}
>
{children}
return () => {
if (!forwardedRef?.current) return;
removeScrollableNode(forwardedRef.current!);
};
}, []);
return (
<div
className={classes([
'Section',
fill && 'Section--fill',
fitted && 'Section--fitted',
scrollable && 'Section--scrollable',
scrollableHorizontal && 'Section--scrollableHorizontal',
flexGrow && 'Section--flex', // VOREStation Addition
className,
computeBoxClassName(rest),
])}
{...computeBoxProps(rest)}
>
{hasTitle && (
<div className="Section__title">
<span className="Section__titleText">{title}</span>
<div className="Section__buttons">{buttons}</div>
</div>
)}
<div className="Section__rest">
<div
className={classes([
'Section__content',
!!stretchContents && 'Section__content--stretchContents', // VOREStation Addition
!!noTopPadding && 'Section__content--noTopPadding', // VOREStation Addition
])}
onScroll={onScroll}
// For posterity: the forwarded ref needs to be here specifically
// to actually let things interact with the scrolling.
ref={forwardedRef}
>
{children}
</div>
</div>
</div>
</div>
);
};
);
},
);
@@ -373,7 +373,7 @@ const BodyScannerMainOrgansExternal = (props) => {
<Table.Cell textAlign="right">Injuries</Table.Cell>
</Table.Row>
{props.organs.map((o, i) => (
<Table.Row key={i} textTransform="capitalize">
<Table.Row key={i} style={{ textTransform: 'capitalize' }}>
<Table.Cell width="33%">{o.name}</Table.Cell>
<Table.Cell textAlign="center" q>
<ProgressBar
@@ -450,7 +450,7 @@ const BodyScannerMainOrgansInternal = (props) => {
<Table.Cell textAlign="right">Injuries</Table.Cell>
</Table.Row>
{props.organs.map((o, i) => (
<Table.Row key={i} textTransform="capitalize">
<Table.Row key={i} style={{ textTransform: 'capitalize' }}>
<Table.Cell width="33%">{o.name}</Table.Cell>
<Table.Cell textAlign="center">
<ProgressBar
+10 -10
View File
@@ -80,7 +80,7 @@ export const Communicator = (props) => {
height="88%"
mb={1}
style={{
'overflow-y': 'auto',
overflowY: 'auto',
}}
>
{TabToTemplate[currentTab] || <TemplateError />}
@@ -154,7 +154,7 @@ const VideoComm = (props) => {
position: 'absolute',
right: '5px',
bottom: '50px',
'z-index': 1,
zIndex: '1',
}}
>
<Section p={0} m={0}>
@@ -843,8 +843,8 @@ const MessagingThreadTab = (props) => {
<Box
inline
style={{
'white-space': 'nowrap',
'overflow-x': 'hidden',
whiteSpace: 'nowrap',
overflowX: 'hidden',
}}
width="90%"
>
@@ -870,7 +870,7 @@ const MessagingThreadTab = (props) => {
<Section
style={{
height: '95%',
'overflow-y': 'auto',
overflowY: 'auto',
}}
>
{imList.map(
@@ -905,8 +905,8 @@ const MessagingThreadTab = (props) => {
<Box
inline
style={{
'white-space': 'nowrap',
'overflow-x': 'hidden',
whiteSpace: 'nowrap',
overflowX: 'hidden',
}}
width="100%"
>
@@ -932,7 +932,7 @@ const MessagingThreadTab = (props) => {
<Section
style={{
height: '95%',
'overflow-y': 'auto',
overflowY: 'auto',
}}
>
{imList.map(
@@ -1126,8 +1126,8 @@ const NoteTab = (props) => {
width="100%"
height="100%"
style={{
'word-break': 'break-all',
'overflow-y': 'auto',
wordBreak: 'break-all',
overflowY: 'auto',
}}
>
{note}
@@ -9,7 +9,7 @@ import {
KEY_Z,
} from '../../common/keycodes';
import { useBackend } from '../backend';
import { Button, Input, Section, Stack } from '../components';
import { Autofocus, Button, Input, Section, Stack } from '../components';
import { Window } from '../layouts';
import { InputButtons } from './common/InputButtons';
import { Loader } from './common/Loader';
@@ -188,16 +188,16 @@ const ListDisplay = (props) => {
props;
return (
<Section fill scrollable tabIndex={0}>
<Section fill scrollable>
<Autofocus />
{filteredItems.map((item, index) => {
return (
<Button
color="transparent"
fluid
id={index}
key={index}
onClick={() => onClick(index)}
onDblClick={(event) => {
onDoubleClick={(event) => {
event.preventDefault();
act('submit', { entry: filteredItems[selected] });
}}
@@ -1,6 +1,6 @@
import { KEY } from 'common/keys';
import { useState } from 'react';
import { KEY_ENTER, KEY_ESCAPE } from '../../common/keycodes';
import { useBackend } from '../backend';
import { Box, Button, RestrictedInput, Section, Stack } from '../components';
import { Window } from '../layouts';
@@ -15,24 +15,21 @@ type NumberInputData = {
min_value: number | null;
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);
const onChange = (value: number) => {
if (value === input) {
return;
}
setInput(value);
};
const onClick = (value: number) => {
const setValue = (value: number) => {
if (value === input) {
return;
}
setInput(value);
};
// Dynamically changes the window height based on the message.
const windowHeight =
140 +
@@ -44,11 +41,10 @@ export const NumberInputModal = (props) => {
{timeout && <Loader value={timeout} />}
<Window.Content
onKeyDown={(event) => {
const keyCode = window.event ? event.which : event.keyCode;
if (keyCode === KEY_ENTER) {
if (event.key === KEY.Enter) {
act('submit', { entry: input });
}
if (keyCode === KEY_ESCAPE) {
if (event.key === KEY.Escape) {
act('cancel');
}
}}
@@ -59,7 +55,7 @@ export const NumberInputModal = (props) => {
<Box color="label">{message}</Box>
</Stack.Item>
<Stack.Item>
<InputArea input={input} onClick={onClick} onChange={onChange} />
<InputArea input={input} onClick={setValue} onChange={setValue} />
</Stack.Item>
<Stack.Item>
<InputButtons input={input} />
@@ -74,7 +70,7 @@ export const NumberInputModal = (props) => {
/** 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 } = data;
const { min_value, max_value, init_value, round_value } = data;
const { input, onClick, onChange } = props;
return (
@@ -89,10 +85,10 @@ const InputArea = (props) => {
</Stack.Item>
<Stack.Item grow>
<RestrictedInput
allowFloats
autoFocus
autoSelect
fluid
allowFloats={!round_value}
minValue={min_value}
maxValue={max_value}
onChange={(_, value) => onChange(value)}
@@ -1,4 +1,5 @@
import { useState } from 'react';
import { KEY } from 'common/keys';
import { KeyboardEvent, useState } from 'react';
import { useBackend } from '../backend';
import { Box, Section, Stack, TextArea } from '../components';
@@ -14,7 +15,14 @@ type TextInputData = {
placeholder: string;
timeout: number;
title: string;
prevent_enter: boolean;
};
export const sanitizeMultiline = (toSanitize: string) => {
return toSanitize.replace(/(\n|\r\n){3,}/, '\n\n');
};
export const removeAllSkiplines = (toSanitize: string) => {
return toSanitize.replace(/[\r\n]+/, '');
};
export const TextInputModal = (props) => {
@@ -27,31 +35,40 @@ export const TextInputModal = (props) => {
placeholder = '',
timeout,
title,
prevent_enter,
} = data;
const [input, setInput] = useState(placeholder || '');
const onType = (value: string) => {
if (value === input) {
return;
}
setInput(value);
const sanitizedInput = multiline
? sanitizeMultiline(value)
: removeAllSkiplines(value);
setInput(sanitizedInput);
};
const visualMultiline = multiline || input.length >= 30;
// Dynamically changes the window height based on the message.
const windowHeight =
135 +
(message.length > 30 ? Math.ceil(message.length / 4) : 0) +
(multiline || input.length >= 30 ? 75 : 0) +
(visualMultiline ? 75 : 0) +
(message.length && large_buttons ? 5 : 0);
return (
<Window title={title} width={325} height={windowHeight}>
{timeout && <Loader value={timeout} />}
<Window.Content
onEscape={() => act('cancel')}
onEnter={(event) => {
if (!prevent_enter) {
onKeyDown={(event) => {
if (
event.key === KEY.Enter &&
(!visualMultiline || !event.shiftKey)
) {
act('submit', { entry: input });
event.preventDefault();
}
if (event.key === KEY.Escape) {
act('cancel');
}
}}
>
@@ -82,9 +99,11 @@ const InputArea = (props: {
onType: (value: string) => void;
}) => {
const { act, data } = useBackend<TextInputData>();
const { max_length, multiline, prevent_enter } = data;
const { max_length, multiline } = data;
const { input, onType } = props;
const visualMultiline = multiline || input.length >= 30;
return (
<TextArea
autoFocus
@@ -92,12 +111,14 @@ const InputArea = (props: {
height={multiline || input.length >= 30 ? '100%' : '1.8rem'}
maxLength={max_length}
onEscape={() => act('cancel')}
onEnter={(event) => {
if (!prevent_enter) {
act('submit', { entry: input });
event.preventDefault();
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}
@@ -65,7 +65,7 @@ export const TraitDescription = (props) => {
const { descriptions, categories, tutorials } = data;
return (
<Section StackWrap>
<Section>
<b>Name:</b> {name}
<br />
<b>Category:</b> {categories[name]}
@@ -0,0 +1,7 @@
.centered-image {
position: absolute;
height: 100%;
left: 50%;
top: 50%;
transform: translateX(-50%) translateY(-50%) scale(0.8);
}
@@ -0,0 +1,14 @@
$mqIterations: 19;
@mixin fontResize($iterations) {
$i: 1;
@while $i <= $iterations {
@media all and (min-width: 100px * $i) {
.fit-text {
font-size: 0.1em * $i;
}
}
$i: $i + 1;
}
}
@include fontResize($mqIterations);
+10 -12
View File
@@ -22,6 +22,7 @@ $purple: #a333c8 !default;
$pink: #e03997 !default;
$brown: #a5673f !default;
$grey: #767676 !default;
$light-grey: #aaa !default;
$primary: #4972a1 !default;
$good: #5baa27 !default;
@@ -58,6 +59,7 @@ $_gen_map: (
'pink': $pink,
'brown': $brown,
'grey': $grey,
'light-grey': $light-grey,
'good': $good,
'average': $average,
'bad': $bad,
@@ -71,20 +73,16 @@ $bg-map-keys: map.keys($_gen_map) !default;
$fg-map: ();
@each $color-name in $fg-map-keys {
$fg-map: map-merge(
$fg-map,
(
$color-name: fg(map.get($_gen_map, $color-name)),
)
);
// prettier-ignore
$fg-map: map-merge($fg-map, (
$color-name: fg(map.get($_gen_map, $color-name)),
));
}
$bg-map: ();
@each $color-name in $bg-map-keys {
$bg-map: map-merge(
$bg-map,
(
$color-name: bg(map.get($_gen_map, $color-name)),
)
);
// prettier-ignore
$bg-map: map-merge($bg-map, (
$color-name: bg(map.get($_gen_map, $color-name)),
));
}
@@ -75,6 +75,12 @@ $bg-map: colors.$bg-map !default;
}
}
.Button--dropdown {
line-height: base.em(16px);
height: base.em(22px);
padding: 0.2rem 0.5rem;
}
.Button--hasContent {
// Add a margin to the icon to keep it separate from the text
.fa,
@@ -94,8 +100,8 @@ $bg-map: colors.$bg-map !default;
}
.Button--ellipsis {
overflow: hidden;
text-overflow: ellipsis;
overflow: hidden;
}
.Button--fluid {
@@ -170,3 +176,7 @@ $bg-map: colors.$bg-map !default;
display: block;
align-self: stretch;
}
.Button__textMargin {
margin-left: 0.4rem;
}
@@ -0,0 +1,105 @@
@use '../base';
$background-color: base.$color-bg !default;
.Dialog {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
}
.Dialog__content {
background-color: $background-color;
font-family: Consolas, monospace;
font-size: base.em(14px);
display: flex;
flex-direction: column;
}
.Dialog__header {
display: flex;
height: 2em;
line-height: 1.928em;
background-color: rgba(0, 0, 0, 0.5);
user-select: none;
-ms-user-select: none;
}
.Dialog__title {
display: inline;
font-style: italic;
margin-left: 1rem;
margin-right: 2rem;
flex-grow: 1;
opacity: 0.33;
}
.Dialog__body {
margin: 2rem 1rem 2rem 1rem;
flex-grow: 1;
}
.Dialog__footer {
display: flex;
flex-direction: row;
justify-content: flex-end;
padding: 1rem;
background-color: rgba(0, 0, 0, 0.25);
}
.Dialog__button {
margin: 0 1rem 0 1rem;
height: 2rem;
min-width: 6rem;
text-align: center;
}
.SaveAsDialog__inputs {
display: flex;
flex-direction: row;
align-items: center;
padding-left: 3rem;
justify-content: flex-end;
margin-right: 1rem;
}
.SaveAsDialog__input {
margin-left: 1rem;
width: 80%;
}
.SaveAsDialog__label {
vertical-align: center;
}
.Dialog__FileList {
position: relative;
display: flex;
flex-wrap: wrap;
flex-grow: 1;
align-content: flex-start;
max-height: 20rem;
overflow: auto;
overflow-y: scroll;
}
.Dialog__FileEntry {
text-align: center;
margin: 1rem;
}
.Dialog__FileIcon {
display: inline-block;
margin: 0 0 1rem 0;
position: relative;
width: 6vh;
height: auto;
text-align: center;
cursor: default;
}
@@ -2,6 +2,7 @@
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/
$background-dimness: 0.75 !default;
.Dimmer {
// Align everything in the middle.
@@ -16,6 +17,6 @@
left: 0;
right: 0;
// Dim everything around it
background-color: rgba(0, 0, 0, 0.75);
background-color: rgba(0, 0, 0, $background-dimness);
z-index: 1;
}
@@ -6,51 +6,38 @@
@use '../base.scss';
.Dropdown {
position: relative;
display: flex;
align-items: flex-start;
}
.Dropdown__control {
position: relative;
display: inline-block;
flex: 1;
font-family: Verdana, sans-serif;
font-size: base.em(12px);
width: base.em(100px);
line-height: base.em(17px);
overflow: hidden;
user-select: none;
width: base.em(100px);
}
.Dropdown__arrow-button {
float: right;
padding-left: 0.35em;
width: 1.2em;
height: base.em(22px);
border-left: base.em(1px) solid #000;
border-left: base.em(1px) solid rgba(0, 0, 0, 0.25);
}
.Dropdown__menu {
position: absolute;
overflow-y: auto;
z-index: 5;
width: base.em(100px);
align-items: center;
max-height: base.em(200px);
overflow-y: scroll;
border-radius: 0 0 base.em(2px) base.em(2px);
color: #fff;
background-color: #000;
background-color: rgba(0, 0, 0, 0.75);
}
.Dropdown__menu-noscroll {
position: absolute;
overflow-y: auto;
z-index: 5;
width: base.em(100px);
max-height: base.em(200px);
border-radius: 0 0 base.em(2px) base.em(2px);
color: #fff;
background-color: #000;
background-color: rgba(0, 0, 0, 0.75);
.Dropdown__menu-scroll {
overflow-y: scroll;
}
.Dropdown__menuentry {
@@ -60,6 +47,11 @@
line-height: base.em(17px);
transition: background-color 100ms ease-out;
&.selected {
background-color: rgba(255, 255, 255, 0.5) !important;
transition: background-color 0ms;
}
&:hover {
background-color: rgba(255, 255, 255, 0.2);
transition: background-color 0ms;
@@ -74,7 +66,6 @@
.Dropdown__selected-text {
display: inline-block;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
height: base.em(17px);
width: calc(100% - 1.2em);
@@ -0,0 +1,75 @@
/**
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/
@use '../base.scss';
$separator-color: base.$color-bg-section;
$background-color: base.$color-bg !default;
$dropdown-z-index: 5;
.MenuBar {
display: flex;
}
.MenuBar__font {
font-family: Verdana, sans-serif;
font-size: base.em(12px);
line-height: base.em(17px);
}
.MenuBar__hover {
&:hover {
background-color: lighten($background-color, 30%);
transition: background-color 0ms;
}
}
.MenuBar__MenuBarButton {
padding: 0.2rem 0.5rem 0.2rem 0.5rem;
}
.MenuBar__menu {
position: absolute;
z-index: $dropdown-z-index;
background-color: $background-color;
padding: 0.3rem 0.3rem 0.3rem 0.3rem;
box-shadow: 4px 6px 5px -2px rgba(0, 0, 0, 0.55);
}
.MenuBar__MenuItem {
z-index: $dropdown-z-index;
transition: background-color 100ms ease-out;
background-color: $background-color;
white-space: nowrap;
padding: 0.3rem 2rem 0.3rem 3rem;
}
.MenuBar__MenuItemToggle {
padding: 0.3rem 2rem 0.3rem 0;
}
.MenuBar__MenuItemToggle__check {
display: inline-block;
vertical-align: middle;
min-width: 3rem;
margin-left: 0.3rem;
}
.MenuBar__over {
top: auto;
bottom: 100%;
}
.MenuBar__MenuBarButton-text {
text-overflow: clip;
white-space: nowrap;
height: base.em(17px);
}
.MenuBar__Separator {
display: block;
margin: 0.3rem 0.3rem 0.3rem 2.3rem;
border-top: 1px solid $separator-color;
}
@@ -17,6 +17,8 @@ $bg-map: colors.$bg-map !default;
position: relative;
width: 100%;
padding: 0 0.5em;
border-width: base.em(1px) !important;
border-style: solid !important;
border-radius: $border-radius;
background-color: $background-color;
transition: border-color 900ms ease-out;
@@ -52,7 +54,7 @@ $bg-map: colors.$bg-map !default;
@each $color-name, $color-value in $bg-map {
.ProgressBar--color--#{$color-name} {
border: base.em(1px) solid $color-value !important;
border-color: $color-value !important;
.ProgressBar__fill {
background-color: $color-value;
@@ -6,6 +6,8 @@
@use '../base.scss';
@use './Divider.scss';
$zebra-background-color: base.$color-bg-section !default;
.Stack--fill {
height: 100%;
}
@@ -26,6 +28,10 @@
}
}
.Stack--zebra > .Stack__item:nth-child(even) {
background-color: $zebra-background-color;
}
.Stack--horizontal > .Stack__divider:not(.Stack__divider--hidden) {
border-left: Divider.$thickness solid Divider.$color;
}
@@ -74,12 +74,10 @@ $fg-map: colors.$fg-map !default;
color: $text-color;
min-height: 2.25em;
min-width: 4em;
transition: background-color 50ms ease-out;
}
.Tab:not(.Tab--selected):hover {
background-color: $tab-color-hovered;
transition: background-color 0;
}
.Tab--selected {
@@ -106,28 +104,28 @@ $fg-map: colors.$fg-map !default;
.Tabs--horizontal {
.Tab {
border-top: (math.div(1em, 6)) solid transparent;
border-bottom: (math.div(1em, 6)) solid transparent;
border-top: math.div(1em, 6) solid transparent;
border-bottom: math.div(1em, 6) solid transparent;
border-top-left-radius: 0.25em;
border-top-right-radius: 0.25em;
}
.Tab--selected {
border-bottom: (math.div(1em, 6)) solid $color-default;
border-bottom: math.div(1em, 6) solid $color-default;
}
}
.Tabs--vertical {
.Tab {
min-height: 2em;
border-left: (math.div(1em, 6)) solid transparent;
border-right: (math.div(1em, 6)) solid transparent;
border-left: math.div(1em, 6) solid transparent;
border-right: math.div(1em, 6) solid transparent;
border-top-left-radius: 0.25em;
border-bottom-left-radius: 0.25em;
}
.Tab--selected {
border-right: (math.div(1em, 6)) solid $color-default;
border-right: math.div(1em, 6) solid $color-default;
}
}
@@ -31,6 +31,16 @@ $border-radius: Input.$border-radius !default;
height: auto;
}
.TextArea--noborder {
border: 0px;
}
.TextArea__textarea.TextArea__textarea--scrollable {
overflow: auto;
overflow-x: hidden;
overflow-y: scroll;
}
.TextArea__textarea {
display: block;
position: absolute;
@@ -61,3 +71,14 @@ $border-radius: Input.$border-radius !default;
color: rgba(255, 255, 255, 0.45);
}
}
.TextArea__textarea_custom {
overflow: visible;
white-space: pre-wrap;
}
.TextArea__nowrap {
white-space: nowrap;
overflow-wrap: normal;
overflow-x: scroll;
}
+10 -10
View File
@@ -20,7 +20,7 @@
@if math.unit($value) == '%' {
@return math.div($value, 100%);
}
@return math.div($value, ($value * 0 + 1));
@return math.div($value, $value * 0 + 1);
}
// Color
@@ -52,19 +52,19 @@
@if $value < 0.03928 {
$value: math.div($value, 12.92);
} @else {
$value: math.div(($value + 0.055), 1.055);
$value: math.div($value + 0.055, 1.055);
$value: math.pow($value, 2.4);
}
$colors: map.merge(
$colors,
(
$name: $value,
)
);
// prettier-ignore
$colors: map.merge($colors, (
$name: $value,
));
}
@return (map.get($colors, 'red') * 0.2126) +
(map.get($colors, 'green') * 0.7152) + (map.get($colors, 'blue') * 0.0722);
// prettier-ignore
@return (map.get($colors, 'red') * .2126)
+ (map.get($colors, 'green') * .7152)
+ (map.get($colors, 'blue') * .0722);
}
// Blends an RGBA color with a static background color based on its
@@ -8,7 +8,6 @@
.AlertModal__Message {
text-align: center;
justify-content: center;
white-space: pre-line;
}
.AlertModal__Buttons {
@@ -12,7 +12,6 @@
.ListInput__Section .Section__titleText {
font-size: base.em(12px);
white-space: pre-line;
}
.ListInput__Loader {
+6
View File
@@ -11,8 +11,10 @@
// Atomic classes
@include meta.load-css('./atomic/candystripe.scss');
@include meta.load-css('./atomic/centered-image.scss');
@include meta.load-css('./atomic/color.scss');
@include meta.load-css('./atomic/debug-layout.scss');
@include meta.load-css('./atomic/fit-text.scss');
@include meta.load-css('./atomic/links.scss');
@include meta.load-css('./atomic/outline.scss');
@include meta.load-css('./atomic/text.scss');
@@ -21,6 +23,7 @@
@include meta.load-css('./components/BlockQuote.scss');
@include meta.load-css('./components/Button.scss');
@include meta.load-css('./components/ColorBox.scss');
@include meta.load-css('./components/Dialog.scss');
@include meta.load-css('./components/Dimmer.scss');
@include meta.load-css('./components/Divider.scss');
@include meta.load-css('./components/Dropdown.scss');
@@ -29,6 +32,7 @@
@include meta.load-css('./components/Input.scss');
@include meta.load-css('./components/Knob.scss');
@include meta.load-css('./components/LabeledList.scss');
@include meta.load-css('./components/MenuBar.scss');
@include meta.load-css('./components/Modal.scss');
@include meta.load-css('./components/NanoMap.scss');
@include meta.load-css('./components/NoticeBox.scss');
@@ -69,6 +73,8 @@
@include meta.load-css('./layouts/TitleBar.scss');
@include meta.load-css('./layouts/Window.scss');
@include meta.load-css('highlight.js/scss/github-dark.scss');
// NT Theme
.Layout__content {
background-image: url('../assets/bg-nanotrasen.svg');
+7 -11
View File
@@ -29,17 +29,13 @@ body {
box-sizing: inherit;
}
h1,
h2,
h3,
h4,
h5,
h6 {
display: block;
margin: 0;
padding: 6px 0;
padding: 0.5rem 0;
}
// prettier-ignore
h1, h2, h3, h4, h5, h6 {
display: block;
margin: 0;
padding: 6px 0;
padding: 0.5rem 0;
}
h1 {
font-size: 18px;
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+9 -1
View File
@@ -41,6 +41,7 @@ module.exports = (env = {}, argv) => {
filename: '[name].bundle.js',
chunkFilename: '[name].bundle.js',
chunkLoadTimeout: 15000,
publicPath: '/',
},
resolve: {
extensions: ['.tsx', '.ts', '.js', '.jsx'],
@@ -78,7 +79,14 @@ module.exports = (env = {}, argv) => {
},
{
test: /\.(png|jpg|svg)$/,
type: 'asset/inline',
use: [
{
loader: require.resolve('url-loader'),
options: {
esModule: false,
},
},
],
},
],
},