tgui-core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/.editorconfig +10 -0
  2. package/.eslintrc.cjs +78 -0
  3. package/.gitattributes +4 -0
  4. package/.prettierrc.yml +1 -0
  5. package/.vscode/extensions.json +6 -0
  6. package/.vscode/settings.json +5 -0
  7. package/README.md +1 -0
  8. package/global.d.ts +173 -0
  9. package/package.json +25 -0
  10. package/src/assets.ts +43 -0
  11. package/src/backend.ts +369 -0
  12. package/src/common/collections.ts +349 -0
  13. package/src/common/color.ts +94 -0
  14. package/src/common/events.ts +45 -0
  15. package/src/common/exhaustive.ts +19 -0
  16. package/src/common/fp.ts +38 -0
  17. package/src/common/keycodes.ts +86 -0
  18. package/src/common/keys.ts +39 -0
  19. package/src/common/math.ts +98 -0
  20. package/src/common/perf.ts +72 -0
  21. package/src/common/random.ts +32 -0
  22. package/src/common/react.ts +65 -0
  23. package/src/common/redux.ts +196 -0
  24. package/src/common/storage.js +196 -0
  25. package/src/common/string.ts +173 -0
  26. package/src/common/timer.ts +68 -0
  27. package/src/common/type-utils.ts +41 -0
  28. package/src/common/types.ts +9 -0
  29. package/src/common/uuid.ts +24 -0
  30. package/src/common/vector.ts +51 -0
  31. package/src/components/AnimatedNumber.tsx +185 -0
  32. package/src/components/Autofocus.tsx +23 -0
  33. package/src/components/Blink.jsx +69 -0
  34. package/src/components/BlockQuote.tsx +15 -0
  35. package/src/components/BodyZoneSelector.tsx +149 -0
  36. package/src/components/Box.tsx +255 -0
  37. package/src/components/Button.tsx +415 -0
  38. package/src/components/ByondUi.jsx +121 -0
  39. package/src/components/Chart.tsx +160 -0
  40. package/src/components/Collapsible.tsx +45 -0
  41. package/src/components/ColorBox.tsx +30 -0
  42. package/src/components/Dialog.tsx +85 -0
  43. package/src/components/Dimmer.tsx +19 -0
  44. package/src/components/Divider.tsx +26 -0
  45. package/src/components/DmIcon.tsx +72 -0
  46. package/src/components/DraggableControl.jsx +282 -0
  47. package/src/components/Dropdown.tsx +246 -0
  48. package/src/components/FakeTerminal.jsx +52 -0
  49. package/src/components/FitText.tsx +99 -0
  50. package/src/components/Flex.tsx +105 -0
  51. package/src/components/Grid.tsx +44 -0
  52. package/src/components/Icon.tsx +91 -0
  53. package/src/components/Image.tsx +63 -0
  54. package/src/components/InfinitePlane.jsx +192 -0
  55. package/src/components/Input.tsx +181 -0
  56. package/src/components/KeyListener.tsx +40 -0
  57. package/src/components/Knob.tsx +185 -0
  58. package/src/components/LabeledControls.tsx +50 -0
  59. package/src/components/LabeledList.tsx +130 -0
  60. package/src/components/MenuBar.tsx +238 -0
  61. package/src/components/Modal.tsx +25 -0
  62. package/src/components/NoticeBox.tsx +48 -0
  63. package/src/components/NumberInput.tsx +328 -0
  64. package/src/components/Popper.tsx +100 -0
  65. package/src/components/ProgressBar.tsx +79 -0
  66. package/src/components/RestrictedInput.jsx +301 -0
  67. package/src/components/RoundGauge.tsx +189 -0
  68. package/src/components/Section.tsx +125 -0
  69. package/src/components/Slider.tsx +173 -0
  70. package/src/components/Stack.tsx +101 -0
  71. package/src/components/StyleableSection.tsx +30 -0
  72. package/src/components/Table.tsx +90 -0
  73. package/src/components/Tabs.tsx +90 -0
  74. package/src/components/TextArea.tsx +198 -0
  75. package/src/components/TimeDisplay.jsx +64 -0
  76. package/src/components/Tooltip.tsx +147 -0
  77. package/src/components/TrackOutsideClicks.tsx +35 -0
  78. package/src/components/VirtualList.tsx +69 -0
  79. package/src/constants.ts +355 -0
  80. package/src/debug/KitchenSink.jsx +56 -0
  81. package/src/debug/actions.js +11 -0
  82. package/src/debug/hooks.js +10 -0
  83. package/src/debug/index.ts +10 -0
  84. package/src/debug/middleware.js +86 -0
  85. package/src/debug/reducer.js +22 -0
  86. package/src/debug/selectors.js +7 -0
  87. package/src/drag.ts +280 -0
  88. package/src/events.ts +237 -0
  89. package/src/focus.ts +25 -0
  90. package/src/format.ts +173 -0
  91. package/src/hotkeys.ts +212 -0
  92. package/src/http.ts +16 -0
  93. package/src/layouts/Layout.tsx +75 -0
  94. package/src/layouts/NtosWindow.tsx +162 -0
  95. package/src/layouts/Pane.tsx +56 -0
  96. package/src/layouts/Window.tsx +227 -0
  97. package/src/renderer.ts +50 -0
  98. package/styles/base.scss +32 -0
  99. package/styles/colors.scss +92 -0
  100. package/styles/components/BlockQuote.scss +20 -0
  101. package/styles/components/Button.scss +175 -0
  102. package/styles/components/ColorBox.scss +12 -0
  103. package/styles/components/Dialog.scss +105 -0
  104. package/styles/components/Dimmer.scss +22 -0
  105. package/styles/components/Divider.scss +27 -0
  106. package/styles/components/Dropdown.scss +72 -0
  107. package/styles/components/Flex.scss +31 -0
  108. package/styles/components/Icon.scss +25 -0
  109. package/styles/components/Input.scss +68 -0
  110. package/styles/components/Knob.scss +131 -0
  111. package/styles/components/LabeledList.scss +49 -0
  112. package/styles/components/MenuBar.scss +75 -0
  113. package/styles/components/Modal.scss +14 -0
  114. package/styles/components/NoticeBox.scss +65 -0
  115. package/styles/components/NumberInput.scss +76 -0
  116. package/styles/components/ProgressBar.scss +63 -0
  117. package/styles/components/RoundGauge.scss +88 -0
  118. package/styles/components/Section.scss +143 -0
  119. package/styles/components/Slider.scss +54 -0
  120. package/styles/components/Stack.scss +59 -0
  121. package/styles/components/Table.scss +44 -0
  122. package/styles/components/Tabs.scss +144 -0
  123. package/styles/components/TextArea.scss +84 -0
  124. package/styles/components/Tooltip.scss +24 -0
  125. package/styles/functions.scss +79 -0
  126. package/styles/layouts/Layout.scss +57 -0
  127. package/styles/layouts/NtosHeader.scss +20 -0
  128. package/styles/layouts/NtosWindow.scss +26 -0
  129. package/styles/layouts/TitleBar.scss +111 -0
  130. package/styles/layouts/Window.scss +103 -0
  131. package/styles/main.scss +97 -0
  132. package/styles/reset.scss +68 -0
  133. package/styles/themes/abductor.scss +68 -0
  134. package/styles/themes/admin.scss +38 -0
  135. package/styles/themes/cardtable.scss +57 -0
  136. package/styles/themes/hackerman.scss +70 -0
  137. package/styles/themes/malfunction.scss +67 -0
  138. package/styles/themes/neutral.scss +50 -0
  139. package/styles/themes/ntOS95.scss +166 -0
  140. package/styles/themes/ntos.scss +44 -0
  141. package/styles/themes/ntos_cat.scss +148 -0
  142. package/styles/themes/ntos_darkmode.scss +44 -0
  143. package/styles/themes/ntos_lightmode.scss +67 -0
  144. package/styles/themes/ntos_spooky.scss +69 -0
  145. package/styles/themes/ntos_synth.scss +99 -0
  146. package/styles/themes/ntos_terminal.scss +112 -0
  147. package/styles/themes/paper.scss +184 -0
  148. package/styles/themes/retro.scss +72 -0
  149. package/styles/themes/spookyconsole.scss +73 -0
  150. package/styles/themes/syndicate.scss +67 -0
  151. package/styles/themes/wizard.scss +68 -0
  152. package/tsconfig.json +34 -0
@@ -0,0 +1,196 @@
1
+ /**
2
+ * @file
3
+ * @copyright 2020 Aleksej Komarov
4
+ * @license MIT
5
+ */
6
+
7
+ export type Reducer<State = any, ActionType extends Action = AnyAction> = (
8
+ state: State | undefined,
9
+ action: ActionType,
10
+ ) => State;
11
+
12
+ export type Store<State = any, ActionType extends Action = AnyAction> = {
13
+ dispatch: Dispatch<ActionType>;
14
+ subscribe: (listener: () => void) => void;
15
+ getState: () => State;
16
+ };
17
+
18
+ type MiddlewareAPI<State = any, ActionType extends Action = AnyAction> = {
19
+ getState: () => State;
20
+ dispatch: Dispatch<ActionType>;
21
+ };
22
+
23
+ export type Middleware = <State = any, ActionType extends Action = AnyAction>(
24
+ storeApi: MiddlewareAPI<State, ActionType>,
25
+ ) => (next: Dispatch<ActionType>) => Dispatch<ActionType>;
26
+
27
+ export type Action<TType = any> = {
28
+ type: TType;
29
+ };
30
+
31
+ export type AnyAction = Action & {
32
+ [extraProps: string]: any;
33
+ };
34
+
35
+ export type Dispatch<ActionType extends Action = AnyAction> = (
36
+ action: ActionType,
37
+ ) => void;
38
+
39
+ type StoreEnhancer = (createStoreFunction: Function) => Function;
40
+
41
+ type PreparedAction = {
42
+ payload?: any;
43
+ meta?: any;
44
+ };
45
+
46
+ /**
47
+ * Creates a Redux store.
48
+ */
49
+ export const createStore = <State, ActionType extends Action = AnyAction>(
50
+ reducer: Reducer<State, ActionType>,
51
+ enhancer?: StoreEnhancer,
52
+ ): Store<State, ActionType> => {
53
+ // Apply a store enhancer (applyMiddleware is one of them).
54
+ if (enhancer) {
55
+ return enhancer(createStore)(reducer);
56
+ }
57
+
58
+ let currentState: State;
59
+ let listeners: Array<() => void> = [];
60
+
61
+ const getState = (): State => currentState;
62
+
63
+ const subscribe = (listener: () => void): void => {
64
+ listeners.push(listener);
65
+ };
66
+
67
+ const dispatch = (action: ActionType): void => {
68
+ currentState = reducer(currentState, action);
69
+ for (let i = 0; i < listeners.length; i++) {
70
+ listeners[i]();
71
+ }
72
+ };
73
+
74
+ // This creates the initial store by causing each reducer to be called
75
+ // with an undefined state
76
+ dispatch({ type: '@@INIT' } as ActionType);
77
+
78
+ return {
79
+ dispatch,
80
+ subscribe,
81
+ getState,
82
+ };
83
+ };
84
+
85
+ /**
86
+ * Creates a store enhancer which applies middleware to all dispatched
87
+ * actions.
88
+ */
89
+ export const applyMiddleware = (
90
+ ...middlewares: Middleware[]
91
+ ): StoreEnhancer => {
92
+ return (
93
+ createStoreFunction: (reducer: Reducer, enhancer?: StoreEnhancer) => Store,
94
+ ) => {
95
+ return (reducer, ...args): Store => {
96
+ const store = createStoreFunction(reducer, ...args);
97
+
98
+ let dispatch: Dispatch = (action, ...args) => {
99
+ throw new Error(
100
+ 'Dispatching while constructing your middleware is not allowed.',
101
+ );
102
+ };
103
+
104
+ const storeApi: MiddlewareAPI = {
105
+ getState: store.getState,
106
+ dispatch: (action, ...args) => dispatch(action, ...args),
107
+ };
108
+
109
+ const chain = middlewares.map((middleware) => middleware(storeApi));
110
+ dispatch = chain.reduceRight(
111
+ (next, middleware) => middleware(next),
112
+ store.dispatch,
113
+ );
114
+
115
+ return {
116
+ ...store,
117
+ dispatch,
118
+ };
119
+ };
120
+ };
121
+ };
122
+
123
+ /**
124
+ * Combines reducers by running them in their own object namespaces as
125
+ * defined in reducersObj paramter.
126
+ *
127
+ * Main difference from redux/combineReducers is that it preserves keys
128
+ * in the state that are not present in the reducers object. This function
129
+ * is also more flexible than the redux counterpart.
130
+ */
131
+ export const combineReducers = (
132
+ reducersObj: Record<string, Reducer>,
133
+ ): Reducer => {
134
+ const keys = Object.keys(reducersObj);
135
+
136
+ return (prevState = {}, action) => {
137
+ const nextState = { ...prevState };
138
+ let hasChanged = false;
139
+
140
+ for (const key of keys) {
141
+ const reducer = reducersObj[key];
142
+ const prevDomainState = prevState[key];
143
+ const nextDomainState = reducer(prevDomainState, action);
144
+
145
+ if (prevDomainState !== nextDomainState) {
146
+ hasChanged = true;
147
+ nextState[key] = nextDomainState;
148
+ }
149
+ }
150
+
151
+ return hasChanged ? nextState : prevState;
152
+ };
153
+ };
154
+
155
+ /**
156
+ * A utility function to create an action creator for the given action
157
+ * type string. The action creator accepts a single argument, which will
158
+ * be included in the action object as a field called payload. The action
159
+ * creator function will also have its toString() overriden so that it
160
+ * returns the action type, allowing it to be used in reducer logic that
161
+ * is looking for that action type.
162
+ *
163
+ * @param {string} type The action type to use for created actions.
164
+ * @param {any} prepare (optional) a method that takes any number of arguments
165
+ * and returns { payload } or { payload, meta }. If this is given, the
166
+ * resulting action creator will pass it's arguments to this method to
167
+ * calculate payload & meta.
168
+ *
169
+ * @public
170
+ */
171
+ export const createAction = <TAction extends string>(
172
+ type: TAction,
173
+ prepare?: (...args: any[]) => PreparedAction,
174
+ ) => {
175
+ const actionCreator = (...args: any[]) => {
176
+ let action: Action<TAction> & PreparedAction = { type };
177
+
178
+ if (prepare) {
179
+ const prepared = prepare(...args);
180
+ if (!prepared) {
181
+ throw new Error('prepare function did not return an object');
182
+ }
183
+ action = { ...action, ...prepared };
184
+ } else {
185
+ action.payload = args[0];
186
+ }
187
+
188
+ return action;
189
+ };
190
+
191
+ actionCreator.toString = () => type;
192
+ actionCreator.type = type;
193
+ actionCreator.match = (action) => action.type === type;
194
+
195
+ return actionCreator;
196
+ };
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Browser-agnostic abstraction of key-value web storage.
3
+ *
4
+ * @file
5
+ * @copyright 2020 Aleksej Komarov
6
+ * @license MIT
7
+ */
8
+
9
+ export const IMPL_MEMORY = 0;
10
+ export const IMPL_LOCAL_STORAGE = 1;
11
+ export const IMPL_INDEXED_DB = 2;
12
+
13
+ const INDEXED_DB_VERSION = 1;
14
+ const INDEXED_DB_NAME = 'tgui';
15
+ const INDEXED_DB_STORE_NAME = 'storage-v1';
16
+
17
+ const READ_ONLY = 'readonly';
18
+ const READ_WRITE = 'readwrite';
19
+
20
+ const testGeneric = (testFn) => () => {
21
+ try {
22
+ return Boolean(testFn());
23
+ } catch {
24
+ return false;
25
+ }
26
+ };
27
+
28
+ // Localstorage can sometimes throw an error, even if DOM storage is not
29
+ // disabled in IE11 settings.
30
+ // See: https://superuser.com/questions/1080011
31
+
32
+ const testLocalStorage = testGeneric(
33
+ () => window.localStorage && window.localStorage.getItem
34
+ );
35
+
36
+ const testIndexedDb = testGeneric(
37
+ () =>
38
+ (window.indexedDB || window.msIndexedDB) &&
39
+ (window.IDBTransaction || window.msIDBTransaction)
40
+ );
41
+
42
+ class MemoryBackend {
43
+ constructor() {
44
+ this.impl = IMPL_MEMORY;
45
+ this.store = {};
46
+ }
47
+
48
+ get(key) {
49
+ return this.store[key];
50
+ }
51
+
52
+ set(key, value) {
53
+ this.store[key] = value;
54
+ }
55
+
56
+ remove(key) {
57
+ this.store[key] = undefined;
58
+ }
59
+
60
+ clear() {
61
+ this.store = {};
62
+ }
63
+ }
64
+
65
+ class LocalStorageBackend {
66
+ constructor() {
67
+ this.impl = IMPL_LOCAL_STORAGE;
68
+ }
69
+
70
+ get(key) {
71
+ const value = localStorage.getItem(key);
72
+ if (typeof value === 'string') {
73
+ return JSON.parse(value);
74
+ }
75
+ }
76
+
77
+ set(key, value) {
78
+ localStorage.setItem(key, JSON.stringify(value));
79
+ }
80
+
81
+ remove(key) {
82
+ localStorage.removeItem(key);
83
+ }
84
+
85
+ clear() {
86
+ localStorage.clear();
87
+ }
88
+ }
89
+
90
+ class IndexedDbBackend {
91
+ constructor() {
92
+ this.impl = IMPL_INDEXED_DB;
93
+ /** @type {Promise<IDBDatabase>} */
94
+ this.dbPromise = new Promise((resolve, reject) => {
95
+ const indexedDB = window.indexedDB || window.msIndexedDB;
96
+ const req = indexedDB.open(INDEXED_DB_NAME, INDEXED_DB_VERSION);
97
+ req.onupgradeneeded = () => {
98
+ try {
99
+ req.result.createObjectStore(INDEXED_DB_STORE_NAME);
100
+ } catch (err) {
101
+ reject(new Error('Failed to upgrade IDB: ' + req.error));
102
+ }
103
+ };
104
+ req.onsuccess = () => resolve(req.result);
105
+ req.onerror = () => {
106
+ reject(new Error('Failed to open IDB: ' + req.error));
107
+ };
108
+ });
109
+ }
110
+
111
+ getStore(mode) {
112
+ return this.dbPromise.then((db) =>
113
+ db
114
+ .transaction(INDEXED_DB_STORE_NAME, mode)
115
+ .objectStore(INDEXED_DB_STORE_NAME)
116
+ );
117
+ }
118
+
119
+ async get(key) {
120
+ const store = await this.getStore(READ_ONLY);
121
+ return new Promise((resolve, reject) => {
122
+ const req = store.get(key);
123
+ req.onsuccess = () => resolve(req.result);
124
+ req.onerror = () => reject(req.error);
125
+ });
126
+ }
127
+
128
+ async set(key, value) {
129
+ // The reason we don't _save_ null is because IE 10 does
130
+ // not support saving the `null` type in IndexedDB. How
131
+ // ironic, given the bug below!
132
+ // See: https://github.com/mozilla/localForage/issues/161
133
+ if (value === null) {
134
+ value = undefined;
135
+ }
136
+ // NOTE: We deliberately make this operation transactionless
137
+ const store = await this.getStore(READ_WRITE);
138
+ store.put(value, key);
139
+ }
140
+
141
+ async remove(key) {
142
+ // NOTE: We deliberately make this operation transactionless
143
+ const store = await this.getStore(READ_WRITE);
144
+ store.delete(key);
145
+ }
146
+
147
+ async clear() {
148
+ // NOTE: We deliberately make this operation transactionless
149
+ const store = await this.getStore(READ_WRITE);
150
+ store.clear();
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Web Storage Proxy object, which selects the best backend available
156
+ * depending on the environment.
157
+ */
158
+ class StorageProxy {
159
+ constructor() {
160
+ this.backendPromise = (async () => {
161
+ if (testIndexedDb()) {
162
+ try {
163
+ const backend = new IndexedDbBackend();
164
+ await backend.dbPromise;
165
+ return backend;
166
+ } catch {}
167
+ }
168
+ if (testLocalStorage()) {
169
+ return new LocalStorageBackend();
170
+ }
171
+ return new MemoryBackend();
172
+ })();
173
+ }
174
+
175
+ async get(key) {
176
+ const backend = await this.backendPromise;
177
+ return backend.get(key);
178
+ }
179
+
180
+ async set(key, value) {
181
+ const backend = await this.backendPromise;
182
+ return backend.set(key, value);
183
+ }
184
+
185
+ async remove(key) {
186
+ const backend = await this.backendPromise;
187
+ return backend.remove(key);
188
+ }
189
+
190
+ async clear() {
191
+ const backend = await this.backendPromise;
192
+ return backend.clear();
193
+ }
194
+ }
195
+
196
+ export const storage = new StorageProxy();
@@ -0,0 +1,173 @@
1
+ /**
2
+ * @file
3
+ * @copyright 2020 Aleksej Komarov
4
+ * @license MIT
5
+ */
6
+
7
+ /**
8
+ * Creates a search terms matcher. Returns true if given string matches the search text.
9
+ *
10
+ * @example
11
+ * ```tsx
12
+ * type Thing = { id: string; name: string };
13
+ *
14
+ * const objects = [
15
+ * { id: '123', name: 'Test' },
16
+ * { id: '456', name: 'Test' },
17
+ * ];
18
+ *
19
+ * const search = createSearch('123', (obj: Thing) => obj.id);
20
+ *
21
+ * objects.filter(search); // returns [{ id: '123', name: 'Test' }]
22
+ * ```
23
+ */
24
+ export function createSearch<TObj>(
25
+ searchText: string,
26
+ stringifier = (obj: TObj) => JSON.stringify(obj),
27
+ ): (obj: TObj) => boolean {
28
+ const preparedSearchText = searchText.toLowerCase().trim();
29
+
30
+ return (obj) => {
31
+ if (!preparedSearchText) {
32
+ return true;
33
+ }
34
+ const str = stringifier(obj);
35
+ if (!str) {
36
+ return false;
37
+ }
38
+ return str.toLowerCase().includes(preparedSearchText);
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Capitalizes a word and lowercases the rest.
44
+ *
45
+ * @example
46
+ * ```tsx
47
+ * capitalize('heLLo') // Hello
48
+ * ```
49
+ */
50
+ export function capitalize(str: string): string {
51
+ return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
52
+ }
53
+
54
+ /**
55
+ * Similar to capitalize, this takes a string and replaces all first letters
56
+ * of any words.
57
+ *
58
+ * @example
59
+ * ```tsx
60
+ * capitalizeAll('heLLo woRLd') // 'HeLLo WoRLd'
61
+ * ```
62
+ */
63
+ export function capitalizeAll(str: string): string {
64
+ return str.replace(/(^\w{1})|(\s+\w{1})/g, (letter) => letter.toUpperCase());
65
+ }
66
+
67
+ /**
68
+ * Capitalizes only the first letter of the str, leaving others untouched.
69
+ *
70
+ * @example
71
+ * ```tsx
72
+ * capitalizeFirst('heLLo woRLd') // 'HeLLo woRLd'
73
+ * ```
74
+ */
75
+ export function capitalizeFirst(str: string): string {
76
+ return str.replace(/^\w/, (letter) => letter.toUpperCase());
77
+ }
78
+
79
+ const WORDS_UPPER = ['Id', 'Tv'] as const;
80
+
81
+ const WORDS_LOWER = [
82
+ 'A',
83
+ 'An',
84
+ 'And',
85
+ 'As',
86
+ 'At',
87
+ 'But',
88
+ 'By',
89
+ 'For',
90
+ 'For',
91
+ 'From',
92
+ 'In',
93
+ 'Into',
94
+ 'Near',
95
+ 'Nor',
96
+ 'Of',
97
+ 'On',
98
+ 'Onto',
99
+ 'Or',
100
+ 'The',
101
+ 'To',
102
+ 'With',
103
+ ] as const;
104
+
105
+ /**
106
+ * Converts a string to title case.
107
+ *
108
+ * @example
109
+ * ```tsx
110
+ * toTitleCase('a tale of two cities') // 'A Tale of Two Cities'
111
+ * ```
112
+ */
113
+ export function toTitleCase(str: string): string {
114
+ if (!str) return str;
115
+
116
+ let currentStr = str.replace(/([^\W_]+[^\s-]*) */g, (str) => {
117
+ return capitalize(str);
118
+ });
119
+
120
+ for (let word of WORDS_LOWER) {
121
+ const regex = new RegExp('\\s' + word + '\\s', 'g');
122
+ currentStr = currentStr.replace(regex, (str) => str.toLowerCase());
123
+ }
124
+
125
+ for (let word of WORDS_UPPER) {
126
+ const regex = new RegExp('\\b' + word + '\\b', 'g');
127
+ currentStr = currentStr.replace(regex, (str) => str.toLowerCase());
128
+ }
129
+
130
+ return currentStr;
131
+ }
132
+
133
+ const TRANSLATE_REGEX = /&(nbsp|amp|quot|lt|gt|apos);/g;
134
+ const TRANSLATIONS = {
135
+ amp: '&',
136
+ apos: "'",
137
+ gt: '>',
138
+ lt: '<',
139
+ nbsp: ' ',
140
+ quot: '"',
141
+ } as const;
142
+
143
+ /**
144
+ * Decodes HTML entities and removes unnecessary HTML tags.
145
+ *
146
+ * @example
147
+ * ```tsx
148
+ * decodeHtmlEntities('&amp;') // returns '&'
149
+ * decodeHtmlEntities('&lt;') // returns '<'
150
+ * ```
151
+ */
152
+ export function decodeHtmlEntities(str: string): string {
153
+ if (!str) return str;
154
+
155
+ return (
156
+ str
157
+ // Newline tags
158
+ .replace(/<br>/gi, '\n')
159
+ .replace(/<\/?[a-z0-9-_]+[^>]*>/gi, '')
160
+ // Basic entities
161
+ .replace(TRANSLATE_REGEX, (match, entity) => TRANSLATIONS[entity])
162
+ // Decimal entities
163
+ .replace(/&#?([0-9]+);/gi, (match, numStr) => {
164
+ const num = parseInt(numStr, 10);
165
+ return String.fromCharCode(num);
166
+ })
167
+ // Hex entities
168
+ .replace(/&#x?([0-9a-f]+);/gi, (match, numStr) => {
169
+ const num = parseInt(numStr, 16);
170
+ return String.fromCharCode(num);
171
+ })
172
+ );
173
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * @file
3
+ * @copyright 2020 Aleksej Komarov
4
+ * @license MIT
5
+ */
6
+
7
+ /**
8
+ * Returns a function, that, as long as it continues to be invoked, will
9
+ * not be triggered. The function will be called after it stops being
10
+ * called for N milliseconds. If `immediate` is passed, trigger the
11
+ * function on the leading edge, instead of the trailing.
12
+ */
13
+ export const debounce = <F extends (...args: any[]) => any>(
14
+ fn: F,
15
+ time: number,
16
+ immediate = false,
17
+ ): ((...args: Parameters<F>) => void) => {
18
+ let timeout: ReturnType<typeof setTimeout> | null;
19
+ return (...args: Parameters<F>) => {
20
+ const later = () => {
21
+ timeout = null;
22
+ if (!immediate) {
23
+ fn(...args);
24
+ }
25
+ };
26
+ const callNow = immediate && !timeout;
27
+ clearTimeout(timeout!);
28
+ timeout = setTimeout(later, time);
29
+ if (callNow) {
30
+ fn(...args);
31
+ }
32
+ };
33
+ };
34
+
35
+ /**
36
+ * Returns a function, that, when invoked, will only be triggered at most once
37
+ * during a given window of time.
38
+ */
39
+ export const throttle = <F extends (...args: any[]) => any>(
40
+ fn: F,
41
+ time: number,
42
+ ): ((...args: Parameters<F>) => void) => {
43
+ let previouslyRun: number | null,
44
+ queuedToRun: ReturnType<typeof setTimeout> | null;
45
+ return function invokeFn(...args: Parameters<F>) {
46
+ const now = Date.now();
47
+ if (queuedToRun) {
48
+ clearTimeout(queuedToRun);
49
+ }
50
+ if (!previouslyRun || now - previouslyRun >= time) {
51
+ fn.apply(null, args);
52
+ previouslyRun = now;
53
+ } else {
54
+ queuedToRun = setTimeout(
55
+ () => invokeFn(...args),
56
+ time - (now - (previouslyRun ?? 0)),
57
+ );
58
+ }
59
+ };
60
+ };
61
+
62
+ /**
63
+ * Suspends an asynchronous function for N milliseconds.
64
+ *
65
+ * @param {number} time
66
+ */
67
+ export const sleep = (time: number): Promise<void> =>
68
+ new Promise((resolve) => setTimeout(resolve, time));
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Helps visualize highly complex ui data on the fly.
3
+ * @example
4
+ * ```tsx
5
+ * const { data } = useBackend<CargoData>();
6
+ * logger.log(getShallowTypes(data));
7
+ * ```
8
+ */
9
+ export function getShallowTypes(
10
+ data: Record<string, any>,
11
+ ): Record<string, any> {
12
+ const output = {};
13
+
14
+ for (const key in data) {
15
+ if (Array.isArray(data[key])) {
16
+ const arr: any[] = data[key];
17
+
18
+ // Return the first array item if it exists
19
+ if (data[key].length > 0) {
20
+ output[key] = arr[0];
21
+ continue;
22
+ }
23
+
24
+ output[key] = 'emptyarray';
25
+ } else if (typeof data[key] === 'object' && data[key] !== null) {
26
+ // Please inspect it further and make a new type for it
27
+ output[key] = 'object (inspect) || Record<string, any>';
28
+ } else if (typeof data[key] === 'number') {
29
+ const num = Number(data[key]);
30
+
31
+ // 0 and 1 could be booleans from byond
32
+ if (num === 1 || num === 0) {
33
+ output[key] = `${num}, BooleanLike?`;
34
+ continue;
35
+ }
36
+ output[key] = data[key];
37
+ }
38
+ }
39
+
40
+ return output;
41
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Returns the arguments of a function F as an array.
3
+ */
4
+
5
+ export type ArgumentsOf<F extends Function> = F extends (
6
+ ...args: infer A
7
+ ) => unknown
8
+ ? A
9
+ : never;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @file
3
+ * @copyright 2020 Aleksej Komarov
4
+ * @license MIT
5
+ */
6
+
7
+ /**
8
+ * Creates a UUID v4 string
9
+ *
10
+ * @example
11
+ * ```tsx
12
+ * createUuid(); // 'f47ac10b-58cc-4372-a567-0e02b2c3d479'
13
+ * ```
14
+ */
15
+ export function createUuid(): string {
16
+ let d = new Date().getTime();
17
+
18
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
19
+ const r = (d + Math.random() * 16) % 16 | 0;
20
+ d = Math.floor(d / 16);
21
+
22
+ return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
23
+ });
24
+ }