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,282 @@
1
+ /**
2
+ * @file
3
+ * @copyright 2020 Aleksej Komarov
4
+ * @license MIT
5
+ */
6
+
7
+ import { clamp } from '../common/math';
8
+ import { Component, createRef } from 'react';
9
+
10
+ import { AnimatedNumber } from './AnimatedNumber';
11
+
12
+ const DEFAULT_UPDATE_RATE = 400;
13
+
14
+ /**
15
+ * Reduces screen offset to a single number based on the matrix provided.
16
+ */
17
+ const getScalarScreenOffset = (e, matrix) => {
18
+ return e.screenX * matrix[0] + e.screenY * matrix[1];
19
+ };
20
+
21
+ export class DraggableControl extends Component {
22
+ constructor(props) {
23
+ super(props);
24
+ this.inputRef = createRef();
25
+ this.state = {
26
+ value: props.value,
27
+ dragging: false,
28
+ editing: false,
29
+ internalValue: null,
30
+ origin: null,
31
+ suppressingFlicker: false,
32
+ };
33
+
34
+ // Suppresses flickering while the value propagates through the backend
35
+ this.flickerTimer = null;
36
+ this.suppressFlicker = () => {
37
+ const { suppressFlicker } = this.props;
38
+ if (suppressFlicker > 0) {
39
+ this.setState({
40
+ suppressingFlicker: true,
41
+ });
42
+ clearTimeout(this.flickerTimer);
43
+ this.flickerTimer = setTimeout(() => {
44
+ this.setState({
45
+ suppressingFlicker: false,
46
+ });
47
+ }, suppressFlicker);
48
+ }
49
+ };
50
+
51
+ this.handleDragStart = (e) => {
52
+ const { value, dragMatrix } = this.props;
53
+ const { editing } = this.state;
54
+ if (editing) {
55
+ return;
56
+ }
57
+ document.body.style['pointer-events'] = 'none';
58
+ this.ref = e.target;
59
+ this.setState({
60
+ dragging: false,
61
+ origin: getScalarScreenOffset(e, dragMatrix),
62
+ value,
63
+ internalValue: value,
64
+ });
65
+ this.timer = setTimeout(() => {
66
+ this.setState({
67
+ dragging: true,
68
+ });
69
+ }, 250);
70
+ this.dragInterval = setInterval(() => {
71
+ const { dragging, value } = this.state;
72
+ const { onDrag } = this.props;
73
+ if (dragging && onDrag) {
74
+ onDrag(e, value);
75
+ }
76
+ }, this.props.updateRate || DEFAULT_UPDATE_RATE);
77
+ document.addEventListener('mousemove', this.handleDragMove);
78
+ document.addEventListener('mouseup', this.handleDragEnd);
79
+ };
80
+
81
+ this.handleDragMove = (e) => {
82
+ const { minValue, maxValue, step, stepPixelSize, dragMatrix } =
83
+ this.props;
84
+ this.setState((prevState) => {
85
+ const state = { ...prevState };
86
+ const offset = getScalarScreenOffset(e, dragMatrix) - state.origin;
87
+ if (prevState.dragging) {
88
+ const stepOffset = Number.isFinite(minValue) ? minValue % step : 0;
89
+ // Translate mouse movement to value
90
+ // Give it some headroom (by increasing clamp range by 1 step)
91
+ state.internalValue = clamp(
92
+ state.internalValue + (offset * step) / stepPixelSize,
93
+ minValue - step,
94
+ maxValue + step
95
+ );
96
+ // Clamp the final value
97
+ state.value = clamp(
98
+ state.internalValue - (state.internalValue % step) + stepOffset,
99
+ minValue,
100
+ maxValue
101
+ );
102
+ state.origin = getScalarScreenOffset(e, dragMatrix);
103
+ } else if (Math.abs(offset) > 4) {
104
+ state.dragging = true;
105
+ }
106
+ return state;
107
+ });
108
+ };
109
+
110
+ this.handleDragEnd = (e) => {
111
+ const { onChange, onDrag } = this.props;
112
+ const { dragging, value, internalValue } = this.state;
113
+ document.body.style['pointer-events'] = 'auto';
114
+ clearTimeout(this.timer);
115
+ clearInterval(this.dragInterval);
116
+ this.setState({
117
+ dragging: false,
118
+ editing: !dragging,
119
+ origin: null,
120
+ });
121
+ document.removeEventListener('mousemove', this.handleDragMove);
122
+ document.removeEventListener('mouseup', this.handleDragEnd);
123
+ if (dragging) {
124
+ this.suppressFlicker();
125
+ if (onChange) {
126
+ onChange(e, value);
127
+ }
128
+ if (onDrag) {
129
+ onDrag(e, value);
130
+ }
131
+ } else if (this.inputRef) {
132
+ const input = this.inputRef.current;
133
+ input.value = internalValue;
134
+ // IE8: Dies when trying to focus a hidden element
135
+ // (Error: Object does not support this action)
136
+ try {
137
+ input.focus();
138
+ input.select();
139
+ } catch {}
140
+ }
141
+ };
142
+ }
143
+
144
+ render() {
145
+ const {
146
+ dragging,
147
+ editing,
148
+ value: intermediateValue,
149
+ suppressingFlicker,
150
+ } = this.state;
151
+ const {
152
+ animated,
153
+ value,
154
+ unit,
155
+ minValue,
156
+ maxValue,
157
+ unclamped,
158
+ format,
159
+ onChange,
160
+ onDrag,
161
+ children,
162
+ // Input props
163
+ height,
164
+ lineHeight,
165
+ fontSize,
166
+ } = this.props;
167
+ let displayValue = value;
168
+ if (dragging || suppressingFlicker) {
169
+ displayValue = intermediateValue;
170
+ }
171
+
172
+ const displayElement = (
173
+ <>
174
+ {animated && !dragging && !suppressingFlicker ? (
175
+ <AnimatedNumber value={displayValue} format={format} />
176
+ ) : format ? (
177
+ format(displayValue)
178
+ ) : (
179
+ displayValue
180
+ )}
181
+
182
+ {unit ? ' ' + unit : ''}
183
+ </>
184
+ );
185
+
186
+ // Setup an input element
187
+ // Handles direct input via the keyboard
188
+ const inputElement = (
189
+ <input
190
+ ref={this.inputRef}
191
+ className="NumberInput__input"
192
+ style={{
193
+ display: !editing ? 'none' : undefined,
194
+ height: height,
195
+ lineHeight: lineHeight,
196
+ fontsize: fontSize,
197
+ }}
198
+ onBlur={(e) => {
199
+ if (!editing) {
200
+ return;
201
+ }
202
+ let value;
203
+ if (unclamped) {
204
+ value = parseFloat(e.target.value);
205
+ } else {
206
+ value = clamp(parseFloat(e.target.value), minValue, maxValue);
207
+ }
208
+ if (Number.isNaN(value)) {
209
+ this.setState({
210
+ editing: false,
211
+ });
212
+ return;
213
+ }
214
+ this.setState({
215
+ editing: false,
216
+ value,
217
+ });
218
+ this.suppressFlicker();
219
+ if (onChange) {
220
+ onChange(e, value);
221
+ }
222
+ if (onDrag) {
223
+ onDrag(e, value);
224
+ }
225
+ }}
226
+ onKeyDown={(e) => {
227
+ if (e.keyCode === 13) {
228
+ let value;
229
+ if (unclamped) {
230
+ value = parseFloat(e.target.value);
231
+ } else {
232
+ value = clamp(parseFloat(e.target.value), minValue, maxValue);
233
+ }
234
+ if (Number.isNaN(value)) {
235
+ this.setState({
236
+ editing: false,
237
+ });
238
+ return;
239
+ }
240
+ this.setState({
241
+ editing: false,
242
+ value,
243
+ });
244
+ this.suppressFlicker();
245
+ if (onChange) {
246
+ onChange(e, value);
247
+ }
248
+ if (onDrag) {
249
+ onDrag(e, value);
250
+ }
251
+ return;
252
+ }
253
+ if (e.keyCode === 27) {
254
+ this.setState({
255
+ editing: false,
256
+ });
257
+ return;
258
+ }
259
+ }}
260
+ />
261
+ );
262
+ // Return a part of the state for higher-level components to use.
263
+ return children({
264
+ dragging,
265
+ editing,
266
+ value,
267
+ displayValue,
268
+ displayElement,
269
+ inputElement,
270
+ handleDragStart: this.handleDragStart,
271
+ });
272
+ }
273
+ }
274
+
275
+ DraggableControl.defaultProps = {
276
+ minValue: -Infinity,
277
+ maxValue: +Infinity,
278
+ step: 1,
279
+ stepPixelSize: 1,
280
+ suppressFlicker: 50,
281
+ dragMatrix: [1, 0],
282
+ };
@@ -0,0 +1,246 @@
1
+ import { classes } from '../common/react';
2
+ import { ReactNode, useEffect, useRef, useState } from 'react';
3
+
4
+ import { BoxProps, unit } from './Box';
5
+ import { Button } from './Button';
6
+ import { Icon } from './Icon';
7
+ import { Popper } from './Popper';
8
+
9
+ export type DropdownEntry = {
10
+ displayText: ReactNode;
11
+ value: string | number;
12
+ };
13
+
14
+ type DropdownOption = string | DropdownEntry;
15
+
16
+ type Props = {
17
+ /** An array of strings which will be displayed in the
18
+ dropdown when open. See Dropdown.tsx for more advanced usage with DropdownEntry */
19
+ options: DropdownOption[];
20
+ /** Called when a value is picked from the list, `value` is the value that was picked */
21
+ onSelected: (value: any) => void;
22
+ /** Currently selected entry to display. Can be left stateless to permanently display this value. */
23
+ selected: DropdownOption | null | undefined;
24
+ } & Partial<{
25
+ /** Whether to scroll automatically on open. Defaults to true */
26
+ autoScroll: boolean;
27
+ /** Whether to display previous / next buttons */
28
+ buttons: boolean;
29
+ /** Whether to clip the selected text */
30
+ clipSelectedText: boolean;
31
+ /** Color of dropdown button */
32
+ color: string;
33
+ /** Disables the dropdown */
34
+ disabled: boolean;
35
+ /** Overwrites selection text with this. Good for objects etc. */
36
+ displayText: ReactNode;
37
+ /** Icon to display in dropdown button */
38
+ icon: string;
39
+ /** Angle of the icon */
40
+ iconRotation: number;
41
+ /** Whether or not the icon should spin */
42
+ iconSpin: boolean;
43
+ /** Width of the dropdown menu. Default: 15rem */
44
+ menuWidth: string;
45
+ /** Whether or not the arrow on the right hand side of the dropdown button is visible */
46
+ noChevron: boolean;
47
+ /** Called when dropdown button is clicked */
48
+ onClick: (event) => void;
49
+ /** Dropdown renders over instead of below */
50
+ over: boolean;
51
+ /** Text to show when nothing has been selected. */
52
+ placeholder: string;
53
+ }> &
54
+ BoxProps;
55
+
56
+ enum DIRECTION {
57
+ Previous = 'previous',
58
+ Next = 'next',
59
+ Current = 'current',
60
+ }
61
+
62
+ const NONE = -1;
63
+
64
+ function getOptionValue(option: DropdownOption) {
65
+ return typeof option === 'string' ? option : option.value;
66
+ }
67
+
68
+ export function Dropdown(props: Props) {
69
+ const {
70
+ autoScroll = true,
71
+ buttons,
72
+ className,
73
+ clipSelectedText = true,
74
+ color = 'default',
75
+ disabled,
76
+ displayText,
77
+ icon,
78
+ iconRotation,
79
+ iconSpin,
80
+ menuWidth = '15rem',
81
+ noChevron,
82
+ onClick,
83
+ onSelected,
84
+ options = [],
85
+ over,
86
+ placeholder = 'Select...',
87
+ selected,
88
+ width = '15rem',
89
+ } = props;
90
+
91
+ const [open, setOpen] = useState(false);
92
+ const adjustedOpen = over ? !open : open;
93
+ const innerRef = useRef<HTMLDivElement>(null);
94
+
95
+ const selectedIndex =
96
+ options.findIndex((option) => getOptionValue(option) === selected) || 0;
97
+
98
+ function scrollTo(position: number) {
99
+ let scrollPos = position;
100
+ if (position < selectedIndex) {
101
+ scrollPos = position < 2 ? 0 : position - 2;
102
+ } else {
103
+ scrollPos =
104
+ position > options.length - 3 ? options.length - 1 : position - 2;
105
+ }
106
+
107
+ const element = innerRef.current?.children[scrollPos];
108
+ element?.scrollIntoView({ block: 'nearest' });
109
+ }
110
+
111
+ /** Update the selected value when clicking the left/right buttons */
112
+ function updateSelected(direction: DIRECTION) {
113
+ if (options.length < 1 || disabled) {
114
+ return;
115
+ }
116
+
117
+ const startIndex = 0;
118
+ const endIndex = options.length - 1;
119
+
120
+ let newIndex: number;
121
+ if (selectedIndex < 0) {
122
+ newIndex = direction === 'next' ? endIndex : startIndex; // No selection yet
123
+ } else if (direction === 'next') {
124
+ newIndex = selectedIndex === endIndex ? startIndex : selectedIndex + 1; // Move to next option
125
+ } else {
126
+ newIndex = selectedIndex === startIndex ? endIndex : selectedIndex - 1; // Move to previous option
127
+ }
128
+
129
+ if (open && autoScroll) {
130
+ scrollTo(newIndex);
131
+ }
132
+ onSelected?.(getOptionValue(options[newIndex]));
133
+ }
134
+
135
+ /** Allows the menu to be scrollable on open */
136
+ useEffect(() => {
137
+ if (!open) {
138
+ return;
139
+ }
140
+
141
+ if (autoScroll && selectedIndex !== NONE) {
142
+ scrollTo(selectedIndex);
143
+ }
144
+
145
+ innerRef.current?.focus();
146
+ }, [open]);
147
+
148
+ return (
149
+ <Popper
150
+ isOpen={open}
151
+ onClickOutside={() => setOpen(false)}
152
+ placement={over ? 'top-start' : 'bottom-start'}
153
+ content={
154
+ <div
155
+ className="Layout Dropdown__menu"
156
+ style={{ minWidth: menuWidth }}
157
+ ref={innerRef}
158
+ >
159
+ {options.length === 0 && (
160
+ <div className="Dropdown__menuentry">No options</div>
161
+ )}
162
+
163
+ {options.map((option, index) => {
164
+ const value = getOptionValue(option);
165
+
166
+ return (
167
+ <div
168
+ className={classes([
169
+ 'Dropdown__menuentry',
170
+ selected === value && 'selected',
171
+ ])}
172
+ key={index}
173
+ onClick={() => {
174
+ setOpen(false);
175
+ onSelected?.(value);
176
+ }}
177
+ >
178
+ {typeof option === 'string' ? option : option.displayText}
179
+ </div>
180
+ );
181
+ })}
182
+ </div>
183
+ }
184
+ >
185
+ <div className="Dropdown" style={{ width: unit(width) }}>
186
+ <div
187
+ className={classes([
188
+ 'Dropdown__control',
189
+ 'Button',
190
+ 'Button--dropdown',
191
+ 'Button--color--' + color,
192
+ disabled && 'Button--disabled',
193
+ className,
194
+ ])}
195
+ onClick={(event) => {
196
+ if (disabled && !open) {
197
+ return;
198
+ }
199
+ setOpen(!open);
200
+ onClick?.(event);
201
+ }}
202
+ >
203
+ {icon && (
204
+ <Icon mr={1} name={icon} rotation={iconRotation} spin={iconSpin} />
205
+ )}
206
+ <span
207
+ className="Dropdown__selected-text"
208
+ style={{
209
+ overflow: clipSelectedText ? 'hidden' : 'visible',
210
+ }}
211
+ >
212
+ {displayText ||
213
+ (selected && getOptionValue(selected)) ||
214
+ placeholder}
215
+ </span>
216
+ {!noChevron && (
217
+ <span className="Dropdown__arrow-button">
218
+ <Icon name={adjustedOpen ? 'chevron-up' : 'chevron-down'} />
219
+ </span>
220
+ )}
221
+ </div>
222
+ {buttons && (
223
+ <>
224
+ <Button
225
+ disabled={disabled}
226
+ height={1.8}
227
+ icon="chevron-left"
228
+ onClick={() => {
229
+ updateSelected(DIRECTION.Previous);
230
+ }}
231
+ />
232
+
233
+ <Button
234
+ disabled={disabled}
235
+ height={1.8}
236
+ icon="chevron-right"
237
+ onClick={() => {
238
+ updateSelected(DIRECTION.Next);
239
+ }}
240
+ />
241
+ </>
242
+ )}
243
+ </div>
244
+ </Popper>
245
+ );
246
+ }
@@ -0,0 +1,52 @@
1
+ import { Component, Fragment } from 'react';
2
+
3
+ import { Box } from './Box';
4
+
5
+ export class FakeTerminal extends Component {
6
+ constructor(props) {
7
+ super(props);
8
+ this.timer = null;
9
+ this.state = {
10
+ currentIndex: 0,
11
+ currentDisplay: [],
12
+ };
13
+ }
14
+
15
+ tick() {
16
+ const { props, state } = this;
17
+ if (state.currentIndex <= props.allMessages.length) {
18
+ this.setState((prevState) => {
19
+ return {
20
+ currentIndex: prevState.currentIndex + 1,
21
+ };
22
+ });
23
+ const { currentDisplay } = state;
24
+ currentDisplay.push(props.allMessages[state.currentIndex]);
25
+ } else {
26
+ clearTimeout(this.timer);
27
+ setTimeout(props.onFinished, props.finishedTimeout);
28
+ }
29
+ }
30
+
31
+ componentDidMount() {
32
+ const { linesPerSecond = 2.5 } = this.props;
33
+ this.timer = setInterval(() => this.tick(), 1000 / linesPerSecond);
34
+ }
35
+
36
+ componentWillUnmount() {
37
+ clearTimeout(this.timer);
38
+ }
39
+
40
+ render() {
41
+ return (
42
+ <Box m={1}>
43
+ {this.state.currentDisplay.map((value) => (
44
+ <Fragment key={value}>
45
+ {value}
46
+ <br />
47
+ </Fragment>
48
+ ))}
49
+ </Box>
50
+ );
51
+ }
52
+ }
@@ -0,0 +1,99 @@
1
+ import {
2
+ Component,
3
+ createRef,
4
+ HTMLAttributes,
5
+ PropsWithChildren,
6
+ RefObject,
7
+ } from 'react';
8
+
9
+ const DEFAULT_ACCEPTABLE_DIFFERENCE = 5;
10
+
11
+ type Props = {
12
+ acceptableDifference?: number;
13
+ maxWidth: number;
14
+ maxFontSize: number;
15
+ native?: HTMLAttributes<HTMLDivElement>;
16
+ } & PropsWithChildren;
17
+
18
+ type State = {
19
+ fontSize: number;
20
+ };
21
+
22
+ export class FitText extends Component<Props, State> {
23
+ ref: RefObject<HTMLDivElement> = createRef();
24
+ state: State = {
25
+ fontSize: 0,
26
+ };
27
+
28
+ constructor(props: Props) {
29
+ super(props);
30
+
31
+ this.resize = this.resize.bind(this);
32
+
33
+ window.addEventListener('resize', this.resize);
34
+ }
35
+
36
+ componentDidUpdate(prevProps) {
37
+ if (prevProps.children !== this.props.children) {
38
+ this.resize();
39
+ }
40
+ }
41
+
42
+ componentWillUnmount() {
43
+ window.removeEventListener('resize', this.resize);
44
+ }
45
+
46
+ resize() {
47
+ const element = this.ref.current;
48
+ if (!element) {
49
+ return;
50
+ }
51
+
52
+ const maxWidth = this.props.maxWidth;
53
+
54
+ let start = 0;
55
+ let end = this.props.maxFontSize;
56
+
57
+ for (let _ = 0; _ < 10; _++) {
58
+ const middle = Math.round((start + end) / 2);
59
+ element.style.fontSize = `${middle}px`;
60
+
61
+ const difference = element.offsetWidth - maxWidth;
62
+
63
+ if (difference > 0) {
64
+ end = middle;
65
+ } else if (
66
+ difference <
67
+ (this.props.acceptableDifference ?? DEFAULT_ACCEPTABLE_DIFFERENCE)
68
+ ) {
69
+ start = middle;
70
+ } else {
71
+ break;
72
+ }
73
+ }
74
+
75
+ this.setState({
76
+ fontSize: Math.round((start + end) / 2),
77
+ });
78
+ }
79
+
80
+ componentDidMount() {
81
+ this.resize();
82
+ }
83
+
84
+ render() {
85
+ return (
86
+ <span
87
+ ref={this.ref}
88
+ style={{
89
+ fontSize: `${this.state.fontSize}px`,
90
+ ...(typeof this.props.native?.style === 'object'
91
+ ? this.props.native.style
92
+ : {}),
93
+ }}
94
+ >
95
+ {this.props.children}
96
+ </span>
97
+ );
98
+ }
99
+ }