mirror of
https://github.com/VOREStation/VOREStation.git
synced 2026-08-23 03:57:48 +01:00
Merge pull request #15759 from VOREStation/tgui_fix_2
Some input updates
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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.',
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
+127
-127
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+90
-90
File diff suppressed because one or more lines are too long
@@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user