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,328 @@
1
+ import { KEY } from '../common/keys';
2
+ import { clamp } from '../common/math';
3
+ import { BooleanLike, classes } from '../common/react';
4
+ import {
5
+ Component,
6
+ createRef,
7
+ FocusEventHandler,
8
+ KeyboardEventHandler,
9
+ MouseEventHandler,
10
+ RefObject,
11
+ } from 'react';
12
+
13
+ import { AnimatedNumber } from './AnimatedNumber';
14
+ import { Box } from './Box';
15
+
16
+ type Props = Required<{
17
+ value: number | string;
18
+ minValue: number;
19
+ maxValue: number;
20
+ step: number;
21
+ }> &
22
+ Partial<{
23
+ stepPixelSize: number;
24
+ disabled: BooleanLike;
25
+
26
+ className: string;
27
+ fluid: BooleanLike;
28
+ animated: BooleanLike;
29
+ unit: string;
30
+ height: string;
31
+ width: string;
32
+ lineHeight: string;
33
+ fontSize: string;
34
+ format: (value: number) => string;
35
+ onChange: (value: number) => void;
36
+ onDrag: (value: number) => void;
37
+ }>;
38
+
39
+ type State = {
40
+ editing: BooleanLike;
41
+ dragging: BooleanLike;
42
+ currentValue: number;
43
+ previousValue: number;
44
+ origin: number;
45
+ };
46
+
47
+ export class NumberInput extends Component<Props, State> {
48
+ // Ref to the input field to set focus & highlight
49
+ inputRef: RefObject<HTMLInputElement> = createRef();
50
+
51
+ // After this time has elapsed we are in drag mode so no editing when dragging ends
52
+ dragTimeout: NodeJS.Timeout;
53
+
54
+ // Call onDrag at this interval
55
+ dragInterval: NodeJS.Timeout;
56
+
57
+ // default values for the number input state
58
+ state: State = {
59
+ editing: false,
60
+ dragging: false,
61
+ currentValue: 0,
62
+ previousValue: 0,
63
+ origin: 0,
64
+ };
65
+
66
+ constructor(props: Props) {
67
+ super(props);
68
+ }
69
+
70
+ componentDidMount(): void {
71
+ let displayValue = parseFloat(this.props.value.toString());
72
+
73
+ this.setState({
74
+ currentValue: displayValue,
75
+ previousValue: displayValue,
76
+ });
77
+ }
78
+
79
+ handleDragStart: MouseEventHandler<HTMLDivElement> = (event) => {
80
+ const { value, disabled } = this.props;
81
+ const { editing } = this.state;
82
+ if (disabled || editing) {
83
+ return;
84
+ }
85
+ document.body.style['pointer-events'] = 'none';
86
+
87
+ const parsedValue = parseFloat(value.toString());
88
+ this.setState({
89
+ dragging: false,
90
+ origin: event.screenY,
91
+ currentValue: parsedValue,
92
+ previousValue: parsedValue,
93
+ });
94
+
95
+ this.dragTimeout = setTimeout(() => {
96
+ this.setState({
97
+ dragging: true,
98
+ });
99
+ }, 250);
100
+ this.dragInterval = setInterval(() => {
101
+ const { dragging, currentValue, previousValue } = this.state;
102
+ const { onDrag } = this.props;
103
+ if (dragging && currentValue !== previousValue) {
104
+ this.setState({
105
+ previousValue: currentValue,
106
+ });
107
+ onDrag?.(currentValue);
108
+ }
109
+ }, 400);
110
+
111
+ document.addEventListener('mousemove', this.handleDragMove);
112
+ document.addEventListener('mouseup', this.handleDragEnd);
113
+ };
114
+
115
+ handleDragMove = (event: MouseEvent) => {
116
+ const { minValue, maxValue, step, stepPixelSize, disabled } = this.props;
117
+ if (disabled) {
118
+ return;
119
+ }
120
+
121
+ this.setState((prevState) => {
122
+ const state = { ...prevState };
123
+
124
+ const offset = state.origin - event.screenY;
125
+ if (prevState.dragging) {
126
+ const stepOffset = isFinite(minValue) ? minValue % step : 0;
127
+ // Translate mouse movement to value
128
+ // Give it some headroom (by increasing clamp range by 1 step)
129
+ state.currentValue = clamp(
130
+ state.currentValue + (offset * step) / (stepPixelSize || 1),
131
+ minValue - step,
132
+ maxValue + step
133
+ );
134
+ // Clamp the final value
135
+ state.currentValue = clamp(
136
+ state.currentValue - (state.currentValue % step) + stepOffset,
137
+ minValue,
138
+ maxValue
139
+ );
140
+ // Set the new origin
141
+ state.origin = event.screenY;
142
+ } else if (Math.abs(offset) > 4) {
143
+ state.dragging = true;
144
+ }
145
+ return state;
146
+ });
147
+ };
148
+
149
+ handleDragEnd = (event: MouseEvent) => {
150
+ const { dragging, currentValue } = this.state;
151
+ const { onDrag, onChange, disabled } = this.props;
152
+ if (disabled) {
153
+ return;
154
+ }
155
+ document.body.style['pointer-events'] = 'auto';
156
+
157
+ clearInterval(this.dragInterval);
158
+ clearTimeout(this.dragTimeout);
159
+
160
+ this.setState({
161
+ dragging: false,
162
+ editing: !dragging,
163
+ previousValue: currentValue,
164
+ });
165
+ if (dragging) {
166
+ onChange?.(currentValue);
167
+ onDrag?.(currentValue);
168
+ } else if (this.inputRef) {
169
+ const input = this.inputRef.current;
170
+ if (input) {
171
+ input.value = `${currentValue}`;
172
+ setTimeout(() => {
173
+ input.focus();
174
+ input.select();
175
+ }, 1);
176
+ }
177
+ }
178
+
179
+ document.removeEventListener('mousemove', this.handleDragMove);
180
+ document.removeEventListener('mouseup', this.handleDragEnd);
181
+ };
182
+
183
+ handleBlur: FocusEventHandler<HTMLInputElement> = (event) => {
184
+ const { editing, previousValue } = this.state;
185
+ const { minValue, maxValue, onChange, onDrag, disabled } = this.props;
186
+ if (disabled || !editing) {
187
+ return;
188
+ }
189
+
190
+ const targetValue = clamp(
191
+ parseFloat(event.target.value),
192
+ minValue,
193
+ maxValue
194
+ );
195
+ if (isNaN(targetValue)) {
196
+ this.setState({
197
+ editing: false,
198
+ });
199
+ return;
200
+ }
201
+
202
+ this.setState({
203
+ editing: false,
204
+ currentValue: targetValue,
205
+ previousValue: targetValue,
206
+ });
207
+ if (previousValue !== targetValue) {
208
+ onChange?.(targetValue);
209
+ onDrag?.(targetValue);
210
+ }
211
+ };
212
+
213
+ handleKeyDown: KeyboardEventHandler<HTMLInputElement> = (event) => {
214
+ const { minValue, maxValue, onChange, onDrag, disabled } = this.props;
215
+ if (disabled) {
216
+ return;
217
+ }
218
+ const { previousValue } = this.state;
219
+
220
+ if (event.key === KEY.Enter) {
221
+ const targetValue = clamp(
222
+ parseFloat(event.currentTarget.value),
223
+ minValue,
224
+ maxValue
225
+ );
226
+ if (isNaN(targetValue)) {
227
+ this.setState({
228
+ editing: false,
229
+ });
230
+ return;
231
+ }
232
+
233
+ this.setState({
234
+ editing: false,
235
+ currentValue: targetValue,
236
+ previousValue: targetValue,
237
+ });
238
+ if (previousValue !== targetValue) {
239
+ onChange?.(targetValue);
240
+ onDrag?.(targetValue);
241
+ }
242
+ } else if (event.key === KEY.Escape) {
243
+ this.setState({
244
+ editing: false,
245
+ });
246
+ }
247
+ };
248
+
249
+ render() {
250
+ const { dragging, editing, currentValue } = this.state;
251
+
252
+ const {
253
+ className,
254
+ fluid,
255
+ animated,
256
+ unit,
257
+ value,
258
+ minValue,
259
+ maxValue,
260
+ height,
261
+ width,
262
+ lineHeight,
263
+ fontSize,
264
+ format,
265
+ } = this.props;
266
+
267
+ let displayValue = parseFloat(value.toString());
268
+ if (dragging) {
269
+ displayValue = currentValue;
270
+ }
271
+
272
+ const contentElement = (
273
+ <div className="NumberInput__content">
274
+ {animated && !dragging ? (
275
+ <AnimatedNumber value={displayValue} format={format} />
276
+ ) : format ? (
277
+ format(displayValue)
278
+ ) : (
279
+ displayValue
280
+ )}
281
+
282
+ {unit ? ' ' + unit : ''}
283
+ </div>
284
+ );
285
+
286
+ return (
287
+ <Box
288
+ className={classes([
289
+ 'NumberInput',
290
+ fluid && 'NumberInput--fluid',
291
+ className,
292
+ ])}
293
+ minWidth={width}
294
+ minHeight={height}
295
+ lineHeight={lineHeight}
296
+ fontSize={fontSize}
297
+ onMouseDown={this.handleDragStart}
298
+ >
299
+ <div className="NumberInput__barContainer">
300
+ <div
301
+ className="NumberInput__bar"
302
+ style={{
303
+ height:
304
+ clamp(
305
+ ((displayValue - minValue) / (maxValue - minValue)) * 100,
306
+ 0,
307
+ 100
308
+ ) + '%',
309
+ }}
310
+ />
311
+ </div>
312
+ {contentElement}
313
+ <input
314
+ ref={this.inputRef}
315
+ className="NumberInput__input"
316
+ style={{
317
+ display: !editing ? 'none' : 'inline',
318
+ height: height,
319
+ lineHeight: lineHeight,
320
+ fontSize: fontSize,
321
+ }}
322
+ onBlur={this.handleBlur}
323
+ onKeyDown={this.handleKeyDown}
324
+ />
325
+ </Box>
326
+ );
327
+ }
328
+ }
@@ -0,0 +1,100 @@
1
+ import { Placement } from '@popperjs/core';
2
+ import {
3
+ PropsWithChildren,
4
+ ReactNode,
5
+ useEffect,
6
+ useRef,
7
+ useState,
8
+ } from 'react';
9
+ import { usePopper } from 'react-popper';
10
+
11
+ type RequiredProps = {
12
+ /** The content to display in the popper */
13
+ content: ReactNode;
14
+ /** Whether the popper is open */
15
+ isOpen: boolean;
16
+ };
17
+
18
+ type OptionalProps = Partial<{
19
+ /** Called when the user clicks outside the popper */
20
+ onClickOutside: () => void;
21
+ /** Where to place the popper relative to the reference element */
22
+ placement: Placement;
23
+ /** Base z-index of the popper div
24
+ * @default 5
25
+ */
26
+ baseZIndex: number;
27
+ }>;
28
+
29
+ type Props = RequiredProps & OptionalProps;
30
+
31
+ /**
32
+ * ## Popper
33
+ * Popper lets you position elements so that they don't go out of the bounds of the window.
34
+ * @url https://popper.js.org/react-popper/ for more information.
35
+ */
36
+ export function Popper(props: PropsWithChildren<Props>) {
37
+ const { children, content, isOpen, onClickOutside, placement } = props;
38
+
39
+ const [referenceElement, setReferenceElement] =
40
+ useState<HTMLDivElement | null>(null);
41
+ const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(
42
+ null,
43
+ );
44
+
45
+ // One would imagine we could just use useref here, but it's against react-popper documentation and causes a positioning bug
46
+ // We still need them to call focus and clickoutside events :(
47
+ const popperRef = useRef<HTMLDivElement | null>(null);
48
+ const parentRef = useRef<HTMLDivElement | null>(null);
49
+
50
+ const { styles, attributes } = usePopper(referenceElement, popperElement, {
51
+ placement,
52
+ });
53
+
54
+ /** Close the popper when the user clicks outside */
55
+ function handleClickOutside(event: MouseEvent) {
56
+ if (
57
+ !popperRef.current?.contains(event.target as Node) &&
58
+ !parentRef.current?.contains(event.target as Node)
59
+ ) {
60
+ onClickOutside?.();
61
+ }
62
+ }
63
+
64
+ useEffect(() => {
65
+ if (isOpen) {
66
+ document.addEventListener('mousedown', handleClickOutside);
67
+ } else {
68
+ document.removeEventListener('mousedown', handleClickOutside);
69
+ }
70
+
71
+ return () => {
72
+ document.removeEventListener('mousedown', handleClickOutside);
73
+ };
74
+ }, [isOpen]);
75
+
76
+ return (
77
+ <>
78
+ <div
79
+ ref={(node) => {
80
+ setReferenceElement(node);
81
+ parentRef.current = node;
82
+ }}
83
+ >
84
+ {children}
85
+ </div>
86
+ {isOpen && (
87
+ <div
88
+ ref={(node) => {
89
+ setPopperElement(node);
90
+ popperRef.current = node;
91
+ }}
92
+ style={{ ...styles.popper, zIndex: props.baseZIndex ?? 5 }}
93
+ {...attributes.popper}
94
+ >
95
+ {content}
96
+ </div>
97
+ )}
98
+ </>
99
+ );
100
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * @file
3
+ * @copyright 2020 Aleksej Komarov
4
+ * @license MIT
5
+ */
6
+
7
+ import { clamp01, keyOfMatchingRange, scale, toFixed } from '../common/math';
8
+ import { classes } from '../common/react';
9
+ import { PropsWithChildren } from 'react';
10
+
11
+ import { CSS_COLORS } from '../constants';
12
+ import { BoxProps, computeBoxClassName, computeBoxProps } from './Box';
13
+
14
+ type Props = {
15
+ value: number;
16
+ } & Partial<{
17
+ backgroundColor: string;
18
+ className: string;
19
+ color: string;
20
+ height: string | number;
21
+ maxValue: number;
22
+ minValue: number;
23
+ ranges: Record<string, [number, number]>;
24
+ style: Partial<HTMLDivElement['style']>;
25
+ title: string;
26
+ width: string | number;
27
+ }> &
28
+ Partial<BoxProps> &
29
+ PropsWithChildren;
30
+
31
+ export const ProgressBar = (props: Props) => {
32
+ const {
33
+ className,
34
+ value,
35
+ minValue = 0,
36
+ maxValue = 1,
37
+ color,
38
+ ranges = {},
39
+ children,
40
+ ...rest
41
+ } = props;
42
+ const scaledValue = scale(value, minValue, maxValue);
43
+ const hasContent = children !== undefined;
44
+
45
+ const effectiveColor =
46
+ color || keyOfMatchingRange(value, ranges) || 'default';
47
+
48
+ // We permit colors to be in hex format, rgb()/rgba() format,
49
+ // a name for a color-<name> class, or a base CSS class.
50
+ const outerProps = computeBoxProps(rest);
51
+
52
+ const outerClasses = ['ProgressBar', className, computeBoxClassName(rest)];
53
+ const fillStyles = {
54
+ width: clamp01(scaledValue) * 100 + '%',
55
+ };
56
+ if (
57
+ CSS_COLORS.includes(effectiveColor as any) ||
58
+ effectiveColor === 'default'
59
+ ) {
60
+ // If the color is a color-<name> class, just use that.
61
+ outerClasses.push('ProgressBar--color--' + effectiveColor);
62
+ } else {
63
+ // Otherwise, set styles directly.
64
+ outerProps.style = { ...outerProps.style, borderColor: effectiveColor };
65
+ fillStyles['backgroundColor'] = effectiveColor;
66
+ }
67
+
68
+ return (
69
+ <div className={classes(outerClasses)} {...outerProps}>
70
+ <div
71
+ className="ProgressBar__fill ProgressBar__fill--animated"
72
+ style={fillStyles}
73
+ />
74
+ <div className="ProgressBar__content">
75
+ {hasContent ? children : toFixed(scaledValue * 100) + '%'}
76
+ </div>
77
+ </div>
78
+ );
79
+ };