tgui-core 1.0.2 → 1.0.4

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 (158) hide show
  1. package/{src/components → components}/AnimatedNumber.tsx +185 -185
  2. package/{src/components → components}/BlockQuote.tsx +15 -15
  3. package/{src/components → components}/BodyZoneSelector.tsx +149 -149
  4. package/{src/components → components}/Box.tsx +255 -255
  5. package/{src/components → components}/Button.tsx +415 -415
  6. package/{src/components → components}/ByondUi.jsx +121 -121
  7. package/{src/components → components}/Chart.tsx +160 -160
  8. package/{src/components → components}/ColorBox.tsx +30 -30
  9. package/{src/components → components}/Dimmer.tsx +19 -19
  10. package/{src/components → components}/Divider.tsx +26 -26
  11. package/{src/components → components}/DmIcon.tsx +72 -72
  12. package/{src/components → components}/DraggableControl.jsx +282 -282
  13. package/{src/components → components}/Dropdown.tsx +246 -246
  14. package/{src/components → components}/Flex.tsx +105 -105
  15. package/{src/components → components}/Icon.tsx +91 -91
  16. package/{src/components → components}/Input.tsx +181 -181
  17. package/{src/components → components}/KeyListener.tsx +40 -40
  18. package/{src/components → components}/Knob.tsx +185 -185
  19. package/{src/components → components}/LabeledList.tsx +130 -130
  20. package/{src/components → components}/MenuBar.tsx +233 -238
  21. package/{src/components → components}/Modal.tsx +25 -25
  22. package/{src/components → components}/NoticeBox.tsx +48 -48
  23. package/{src/components → components}/NumberInput.tsx +328 -328
  24. package/{src/components → components}/ProgressBar.tsx +79 -79
  25. package/{src/components → components}/RestrictedInput.jsx +301 -301
  26. package/{src/components → components}/RoundGauge.tsx +189 -189
  27. package/{src/components → components}/Section.tsx +125 -125
  28. package/{src/components → components}/Slider.tsx +173 -173
  29. package/{src/components → components}/Stack.tsx +101 -101
  30. package/{src/components → components}/Table.tsx +90 -90
  31. package/{src/components → components}/Tabs.tsx +90 -90
  32. package/{src/components → components}/TextArea.tsx +198 -198
  33. package/{src/components → components}/TimeDisplay.jsx +64 -64
  34. package/components/index.ts +51 -0
  35. package/{src/debug/KitchenSink.jsx → debug/KitchenSink.tsx} +56 -56
  36. package/{src/debug/actions.js → debug/actions.ts} +11 -11
  37. package/{src/debug/hooks.js → debug/hooks.ts} +10 -10
  38. package/{src/debug/middleware.js → debug/middleware.ts} +67 -86
  39. package/{src/debug/reducer.js → debug/reducer.ts} +27 -22
  40. package/{src/debug/selectors.js → debug/selectors.ts} +7 -7
  41. package/{src/layouts → layouts}/Layout.tsx +75 -75
  42. package/{src/layouts → layouts}/NtosWindow.tsx +162 -162
  43. package/{src/layouts → layouts}/Pane.tsx +56 -56
  44. package/{src/layouts → layouts}/Window.tsx +227 -227
  45. package/layouts/index.ts +10 -0
  46. package/package.json +3 -2
  47. package/src/assets.ts +43 -43
  48. package/src/backend.ts +368 -369
  49. package/src/drag.ts +280 -280
  50. package/src/events.ts +237 -237
  51. package/src/hotkeys.ts +212 -212
  52. package/src/renderer.ts +50 -50
  53. package/stories/Blink.stories.tsx +20 -0
  54. package/stories/BlockQuote.stories.tsx +23 -0
  55. package/stories/Box.stories.tsx +27 -0
  56. package/stories/Button.stories.tsx +68 -0
  57. package/stories/ByondUi.stories.tsx +45 -0
  58. package/stories/Collapsible.stories.tsx +23 -0
  59. package/stories/Flex.stories.tsx +68 -0
  60. package/stories/Input.stories.tsx +124 -0
  61. package/stories/LabeledList.stories.tsx +73 -0
  62. package/stories/Popper.stories.tsx +58 -0
  63. package/stories/ProgressBar.stories.tsx +58 -0
  64. package/stories/Stack.stories.tsx +55 -0
  65. package/stories/Storage.stories.tsx +46 -0
  66. package/stories/Themes.stories.tsx +30 -0
  67. package/stories/Tooltip.stories.tsx +48 -0
  68. package/stories/common.tsx +19 -0
  69. package/tsconfig.json +0 -21
  70. package/src/components/Grid.tsx +0 -44
  71. /package/{src/common → common}/collections.ts +0 -0
  72. /package/{src/common → common}/color.ts +0 -0
  73. /package/{src/common → common}/events.ts +0 -0
  74. /package/{src/common → common}/exhaustive.ts +0 -0
  75. /package/{src/common → common}/fp.ts +0 -0
  76. /package/{src/common → common}/keycodes.ts +0 -0
  77. /package/{src/common → common}/keys.ts +0 -0
  78. /package/{src/common → common}/math.ts +0 -0
  79. /package/{src/common → common}/perf.ts +0 -0
  80. /package/{src/common → common}/random.ts +0 -0
  81. /package/{src/common → common}/react.ts +0 -0
  82. /package/{src/common → common}/redux.ts +0 -0
  83. /package/{src/common → common}/storage.js +0 -0
  84. /package/{src/common → common}/string.ts +0 -0
  85. /package/{src/common → common}/timer.ts +0 -0
  86. /package/{src/common → common}/type-utils.ts +0 -0
  87. /package/{src/common → common}/types.ts +0 -0
  88. /package/{src/common → common}/uuid.ts +0 -0
  89. /package/{src/common → common}/vector.ts +0 -0
  90. /package/{src/components → components}/Autofocus.tsx +0 -0
  91. /package/{src/components → components}/Blink.jsx +0 -0
  92. /package/{src/components → components}/Collapsible.tsx +0 -0
  93. /package/{src/components → components}/Dialog.tsx +0 -0
  94. /package/{src/components → components}/FakeTerminal.jsx +0 -0
  95. /package/{src/components → components}/FitText.tsx +0 -0
  96. /package/{src/components → components}/Image.tsx +0 -0
  97. /package/{src/components → components}/InfinitePlane.jsx +0 -0
  98. /package/{src/components → components}/LabeledControls.tsx +0 -0
  99. /package/{src/components → components}/Popper.tsx +0 -0
  100. /package/{src/components → components}/StyleableSection.tsx +0 -0
  101. /package/{src/components → components}/Tooltip.tsx +0 -0
  102. /package/{src/components → components}/TrackOutsideClicks.tsx +0 -0
  103. /package/{src/components → components}/VirtualList.tsx +0 -0
  104. /package/{src/debug → debug}/index.ts +0 -0
  105. /package/{src/styles → styles}/base.scss +0 -0
  106. /package/{src/styles → styles}/colors.scss +0 -0
  107. /package/{src/styles → styles}/components/BlockQuote.scss +0 -0
  108. /package/{src/styles → styles}/components/Button.scss +0 -0
  109. /package/{src/styles → styles}/components/ColorBox.scss +0 -0
  110. /package/{src/styles → styles}/components/Dialog.scss +0 -0
  111. /package/{src/styles → styles}/components/Dimmer.scss +0 -0
  112. /package/{src/styles → styles}/components/Divider.scss +0 -0
  113. /package/{src/styles → styles}/components/Dropdown.scss +0 -0
  114. /package/{src/styles → styles}/components/Flex.scss +0 -0
  115. /package/{src/styles → styles}/components/Icon.scss +0 -0
  116. /package/{src/styles → styles}/components/Input.scss +0 -0
  117. /package/{src/styles → styles}/components/Knob.scss +0 -0
  118. /package/{src/styles → styles}/components/LabeledList.scss +0 -0
  119. /package/{src/styles → styles}/components/MenuBar.scss +0 -0
  120. /package/{src/styles → styles}/components/Modal.scss +0 -0
  121. /package/{src/styles → styles}/components/NoticeBox.scss +0 -0
  122. /package/{src/styles → styles}/components/NumberInput.scss +0 -0
  123. /package/{src/styles → styles}/components/ProgressBar.scss +0 -0
  124. /package/{src/styles → styles}/components/RoundGauge.scss +0 -0
  125. /package/{src/styles → styles}/components/Section.scss +0 -0
  126. /package/{src/styles → styles}/components/Slider.scss +0 -0
  127. /package/{src/styles → styles}/components/Stack.scss +0 -0
  128. /package/{src/styles → styles}/components/Table.scss +0 -0
  129. /package/{src/styles → styles}/components/Tabs.scss +0 -0
  130. /package/{src/styles → styles}/components/TextArea.scss +0 -0
  131. /package/{src/styles → styles}/components/Tooltip.scss +0 -0
  132. /package/{src/styles → styles}/functions.scss +0 -0
  133. /package/{src/styles → styles}/layouts/Layout.scss +0 -0
  134. /package/{src/styles → styles}/layouts/NtosHeader.scss +0 -0
  135. /package/{src/styles → styles}/layouts/NtosWindow.scss +0 -0
  136. /package/{src/styles → styles}/layouts/TitleBar.scss +0 -0
  137. /package/{src/styles → styles}/layouts/Window.scss +0 -0
  138. /package/{src/styles → styles}/main.scss +0 -0
  139. /package/{src/styles → styles}/reset.scss +0 -0
  140. /package/{src/styles → styles}/themes/abductor.scss +0 -0
  141. /package/{src/styles → styles}/themes/admin.scss +0 -0
  142. /package/{src/styles → styles}/themes/cardtable.scss +0 -0
  143. /package/{src/styles → styles}/themes/hackerman.scss +0 -0
  144. /package/{src/styles → styles}/themes/malfunction.scss +0 -0
  145. /package/{src/styles → styles}/themes/neutral.scss +0 -0
  146. /package/{src/styles → styles}/themes/ntOS95.scss +0 -0
  147. /package/{src/styles → styles}/themes/ntos.scss +0 -0
  148. /package/{src/styles → styles}/themes/ntos_cat.scss +0 -0
  149. /package/{src/styles → styles}/themes/ntos_darkmode.scss +0 -0
  150. /package/{src/styles → styles}/themes/ntos_lightmode.scss +0 -0
  151. /package/{src/styles → styles}/themes/ntos_spooky.scss +0 -0
  152. /package/{src/styles → styles}/themes/ntos_synth.scss +0 -0
  153. /package/{src/styles → styles}/themes/ntos_terminal.scss +0 -0
  154. /package/{src/styles → styles}/themes/paper.scss +0 -0
  155. /package/{src/styles → styles}/themes/retro.scss +0 -0
  156. /package/{src/styles → styles}/themes/spookyconsole.scss +0 -0
  157. /package/{src/styles → styles}/themes/syndicate.scss +0 -0
  158. /package/{src/styles → styles}/themes/wizard.scss +0 -0
@@ -1,415 +1,415 @@
1
- /**
2
- * @file
3
- * @copyright 2020 Aleksej Komarov
4
- * @license MIT
5
- */
6
-
7
- import { Placement } from '@popperjs/core';
8
- import { KEY } from '../common/keys';
9
- import { BooleanLike, classes } from '../common/react';
10
- import {
11
- ChangeEvent,
12
- createRef,
13
- MouseEvent,
14
- ReactNode,
15
- useEffect,
16
- useRef,
17
- useState,
18
- } from 'react';
19
-
20
- import { Box, BoxProps, computeBoxClassName, computeBoxProps } from './Box';
21
- import { Icon } from './Icon';
22
- import { Tooltip } from './Tooltip';
23
-
24
- /**
25
- * Getting ellipses to work requires that you use:
26
- * 1. A string rather than a node
27
- * 2. A fixed width here or in a parent
28
- * 3. Children prop rather than content
29
- */
30
- type EllipsisUnion =
31
- | {
32
- ellipsis: true;
33
- children: string;
34
- /** @deprecated use children instead */
35
- content?: never;
36
- }
37
- | Partial<{
38
- ellipsis: undefined;
39
- children: ReactNode;
40
- /** @deprecated use children instead */
41
- content: ReactNode;
42
- }>;
43
-
44
- type Props = Partial<{
45
- captureKeys: boolean;
46
- circular: boolean;
47
- compact: boolean;
48
- disabled: BooleanLike;
49
- fluid: boolean;
50
- icon: string | false;
51
- iconColor: string;
52
- iconPosition: string;
53
- iconRotation: number;
54
- iconSpin: BooleanLike;
55
- onClick: (e: any) => void;
56
- selected: BooleanLike;
57
- tooltip: ReactNode;
58
- tooltipPosition: Placement;
59
- verticalAlignContent: string;
60
- }> &
61
- EllipsisUnion &
62
- BoxProps;
63
-
64
- /** Clickable button. Comes with variants. Read more in the documentation. */
65
- export const Button = (props: Props) => {
66
- const {
67
- captureKeys = true,
68
- children,
69
- circular,
70
- className,
71
- color,
72
- compact,
73
- content,
74
- disabled,
75
- ellipsis,
76
- fluid,
77
- icon,
78
- iconColor,
79
- iconPosition,
80
- iconRotation,
81
- iconSpin,
82
- onClick,
83
- selected,
84
- tooltip,
85
- tooltipPosition,
86
- verticalAlignContent,
87
- ...rest
88
- } = props;
89
-
90
- const toDisplay: ReactNode = content || children;
91
-
92
- let buttonContent = (
93
- <div
94
- className={classes([
95
- 'Button',
96
- fluid && 'Button--fluid',
97
- disabled && 'Button--disabled',
98
- selected && 'Button--selected',
99
- !!toDisplay && 'Button--hasContent',
100
- circular && 'Button--circular',
101
- compact && 'Button--compact',
102
- iconPosition && 'Button--iconPosition--' + iconPosition,
103
- verticalAlignContent && 'Button--flex',
104
- verticalAlignContent && fluid && 'Button--flex--fluid',
105
- verticalAlignContent &&
106
- 'Button--verticalAlignContent--' + verticalAlignContent,
107
- color && typeof color === 'string'
108
- ? 'Button--color--' + color
109
- : 'Button--color--default',
110
- className,
111
- computeBoxClassName(rest),
112
- ])}
113
- tabIndex={!disabled ? 0 : undefined}
114
- onClick={(event) => {
115
- if (!disabled && onClick) {
116
- onClick(event);
117
- }
118
- }}
119
- onKeyDown={(event) => {
120
- if (!captureKeys) {
121
- return;
122
- }
123
-
124
- // Simulate a click when pressing space or enter.
125
- if (event.key === KEY.Space || event.key === KEY.Enter) {
126
- event.preventDefault();
127
- if (!disabled && onClick) {
128
- onClick(event);
129
- }
130
- return;
131
- }
132
-
133
- // Refocus layout on pressing escape.
134
- if (event.key === KEY.Escape) {
135
- event.preventDefault();
136
- }
137
- }}
138
- {...computeBoxProps(rest)}
139
- >
140
- <div className="Button__content">
141
- {icon && iconPosition !== 'right' && (
142
- <Icon
143
- name={icon}
144
- color={iconColor}
145
- rotation={iconRotation}
146
- spin={iconSpin}
147
- />
148
- )}
149
- {!ellipsis ? (
150
- toDisplay
151
- ) : (
152
- <span
153
- className={classes([
154
- 'Button--ellipsis',
155
- icon && 'Button__textMargin',
156
- ])}
157
- >
158
- {toDisplay}
159
- </span>
160
- )}
161
- {icon && iconPosition === 'right' && (
162
- <Icon
163
- name={icon}
164
- color={iconColor}
165
- rotation={iconRotation}
166
- spin={iconSpin}
167
- />
168
- )}
169
- </div>
170
- </div>
171
- );
172
-
173
- if (tooltip) {
174
- buttonContent = (
175
- <Tooltip content={tooltip} position={tooltipPosition as Placement}>
176
- {buttonContent}
177
- </Tooltip>
178
- );
179
- }
180
-
181
- return buttonContent;
182
- };
183
-
184
- type CheckProps = Partial<{
185
- checked: BooleanLike;
186
- }> &
187
- Props;
188
-
189
- /** Visually toggles between checked and unchecked states. */
190
- export const ButtonCheckbox = (props: CheckProps) => {
191
- const { checked, ...rest } = props;
192
-
193
- return (
194
- <Button
195
- color="transparent"
196
- icon={checked ? 'check-square-o' : 'square-o'}
197
- selected={checked}
198
- {...rest}
199
- />
200
- );
201
- };
202
-
203
- Button.Checkbox = ButtonCheckbox;
204
-
205
- type ConfirmProps = Partial<{
206
- confirmColor: string;
207
- confirmContent: ReactNode;
208
- confirmIcon: string;
209
- }> &
210
- Props;
211
-
212
- /** Requires user confirmation before triggering its action. */
213
- const ButtonConfirm = (props: ConfirmProps) => {
214
- const {
215
- children,
216
- color,
217
- confirmColor = 'bad',
218
- confirmContent = 'Confirm?',
219
- confirmIcon,
220
- ellipsis = true,
221
- icon,
222
- onClick,
223
- ...rest
224
- } = props;
225
- const [clickedOnce, setClickedOnce] = useState(false);
226
-
227
- const handleClick = (event: MouseEvent<HTMLDivElement>) => {
228
- if (!clickedOnce) {
229
- setClickedOnce(true);
230
- return;
231
- }
232
-
233
- onClick?.(event);
234
- setClickedOnce(false);
235
- };
236
-
237
- return (
238
- <Button
239
- icon={clickedOnce ? confirmIcon : icon}
240
- color={clickedOnce ? confirmColor : color}
241
- onClick={handleClick}
242
- {...rest}
243
- >
244
- {clickedOnce ? confirmContent : children}
245
- </Button>
246
- );
247
- };
248
-
249
- Button.Confirm = ButtonConfirm;
250
-
251
- type InputProps = Partial<{
252
- currentValue: string;
253
- defaultValue: string;
254
- fluid: boolean;
255
- maxLength: number;
256
- onCommit: (e: any, value: string) => void;
257
- placeholder: string;
258
- }> &
259
- Props;
260
-
261
- /** Accepts and handles user input. */
262
- const ButtonInput = (props: InputProps) => {
263
- const {
264
- children,
265
- color = 'default',
266
- content,
267
- currentValue,
268
- defaultValue,
269
- disabled,
270
- fluid,
271
- icon,
272
- iconRotation,
273
- iconSpin,
274
- maxLength,
275
- onCommit = () => null,
276
- placeholder,
277
- tooltip,
278
- tooltipPosition,
279
- ...rest
280
- } = props;
281
- const [inInput, setInInput] = useState(false);
282
- const inputRef = createRef<HTMLInputElement>();
283
-
284
- const toDisplay = content || children;
285
-
286
- const commitResult = (e) => {
287
- const input = inputRef.current;
288
- if (!input) return;
289
-
290
- const hasValue = input.value !== '';
291
- if (hasValue) {
292
- onCommit(e, input.value);
293
- } else {
294
- if (defaultValue) {
295
- onCommit(e, defaultValue);
296
- }
297
- }
298
- };
299
-
300
- useEffect(() => {
301
- const input = inputRef.current;
302
- if (!input) return;
303
-
304
- if (inInput) {
305
- input.value = currentValue || '';
306
- try {
307
- input.focus();
308
- input.select();
309
- } catch {}
310
- }
311
- }, [inInput, currentValue]);
312
-
313
- let buttonContent = (
314
- <Box
315
- className={classes([
316
- 'Button',
317
- fluid && 'Button--fluid',
318
- 'Button--color--' + color,
319
- ])}
320
- {...rest}
321
- onClick={() => setInInput(true)}
322
- >
323
- {icon && <Icon name={icon} rotation={iconRotation} spin={iconSpin} />}
324
- <div>{toDisplay}</div>
325
- <input
326
- disabled={!!disabled}
327
- ref={inputRef}
328
- className="NumberInput__input"
329
- style={{
330
- display: !inInput ? 'none' : '',
331
- textAlign: 'left',
332
- }}
333
- onBlur={(event) => {
334
- if (!inInput) {
335
- return;
336
- }
337
- setInInput(false);
338
- commitResult(event);
339
- }}
340
- onKeyDown={(event) => {
341
- if (event.key === KEY.Enter) {
342
- setInInput(false);
343
- commitResult(event);
344
- return;
345
- }
346
- if (event.key === KEY.Escape) {
347
- setInInput(false);
348
- }
349
- }}
350
- />
351
- </Box>
352
- );
353
-
354
- if (tooltip) {
355
- buttonContent = (
356
- <Tooltip content={tooltip} position={tooltipPosition as Placement}>
357
- {buttonContent}
358
- </Tooltip>
359
- );
360
- }
361
-
362
- return buttonContent;
363
- };
364
-
365
- Button.Input = ButtonInput;
366
-
367
- type FileProps = {
368
- accept: string;
369
- multiple?: boolean;
370
- onSelectFiles: (files: string | string[]) => void;
371
- } & Props;
372
-
373
- /** Accepts file input */
374
- function ButtonFile(props: FileProps) {
375
- const { accept, multiple, onSelectFiles, ...rest } = props;
376
-
377
- const inputRef = useRef<HTMLInputElement>(null);
378
-
379
- async function read(files: FileList) {
380
- const promises = Array.from(files).map((file) => {
381
- const reader = new FileReader();
382
-
383
- return new Promise<string>((resolve) => {
384
- reader.onload = () => resolve(reader.result as string);
385
- reader.readAsText(file);
386
- });
387
- });
388
-
389
- return await Promise.all(promises);
390
- }
391
-
392
- async function handleChange(event: ChangeEvent<HTMLInputElement>) {
393
- const files = event.target.files;
394
- if (files?.length) {
395
- const readFiles = await read(files);
396
- onSelectFiles(multiple ? readFiles : readFiles[0]);
397
- }
398
- }
399
-
400
- return (
401
- <>
402
- <Button onClick={() => inputRef.current?.click()} {...rest} />
403
- <input
404
- hidden
405
- type="file"
406
- ref={inputRef}
407
- accept={accept}
408
- multiple={multiple}
409
- onChange={handleChange}
410
- />
411
- </>
412
- );
413
- }
414
-
415
- Button.File = ButtonFile;
1
+ /**
2
+ * @file
3
+ * @copyright 2020 Aleksej Komarov
4
+ * @license MIT
5
+ */
6
+
7
+ import { Placement } from '@popperjs/core';
8
+ import { KEY } from '../common/keys';
9
+ import { BooleanLike, classes } from '../common/react';
10
+ import {
11
+ ChangeEvent,
12
+ createRef,
13
+ MouseEvent,
14
+ ReactNode,
15
+ useEffect,
16
+ useRef,
17
+ useState,
18
+ } from 'react';
19
+
20
+ import { Box, BoxProps, computeBoxClassName, computeBoxProps } from './Box';
21
+ import { Icon } from './Icon';
22
+ import { Tooltip } from './Tooltip';
23
+
24
+ /**
25
+ * Getting ellipses to work requires that you use:
26
+ * 1. A string rather than a node
27
+ * 2. A fixed width here or in a parent
28
+ * 3. Children prop rather than content
29
+ */
30
+ type EllipsisUnion =
31
+ | {
32
+ ellipsis: true;
33
+ children: string;
34
+ /** @deprecated use children instead */
35
+ content?: never;
36
+ }
37
+ | Partial<{
38
+ ellipsis: undefined;
39
+ children: ReactNode;
40
+ /** @deprecated use children instead */
41
+ content: ReactNode;
42
+ }>;
43
+
44
+ type Props = Partial<{
45
+ captureKeys: boolean;
46
+ circular: boolean;
47
+ compact: boolean;
48
+ disabled: BooleanLike;
49
+ fluid: boolean;
50
+ icon: string | false;
51
+ iconColor: string;
52
+ iconPosition: string;
53
+ iconRotation: number;
54
+ iconSpin: BooleanLike;
55
+ onClick: (e: any) => void;
56
+ selected: BooleanLike;
57
+ tooltip: ReactNode;
58
+ tooltipPosition: Placement;
59
+ verticalAlignContent: string;
60
+ }> &
61
+ EllipsisUnion &
62
+ BoxProps;
63
+
64
+ /** Clickable button. Comes with variants. Read more in the documentation. */
65
+ export const Button = (props: Props) => {
66
+ const {
67
+ captureKeys = true,
68
+ children,
69
+ circular,
70
+ className,
71
+ color,
72
+ compact,
73
+ content,
74
+ disabled,
75
+ ellipsis,
76
+ fluid,
77
+ icon,
78
+ iconColor,
79
+ iconPosition,
80
+ iconRotation,
81
+ iconSpin,
82
+ onClick,
83
+ selected,
84
+ tooltip,
85
+ tooltipPosition,
86
+ verticalAlignContent,
87
+ ...rest
88
+ } = props;
89
+
90
+ const toDisplay: ReactNode = content || children;
91
+
92
+ let buttonContent = (
93
+ <div
94
+ className={classes([
95
+ 'Button',
96
+ fluid && 'Button--fluid',
97
+ disabled && 'Button--disabled',
98
+ selected && 'Button--selected',
99
+ !!toDisplay && 'Button--hasContent',
100
+ circular && 'Button--circular',
101
+ compact && 'Button--compact',
102
+ iconPosition && 'Button--iconPosition--' + iconPosition,
103
+ verticalAlignContent && 'Button--flex',
104
+ verticalAlignContent && fluid && 'Button--flex--fluid',
105
+ verticalAlignContent &&
106
+ 'Button--verticalAlignContent--' + verticalAlignContent,
107
+ color && typeof color === 'string'
108
+ ? 'Button--color--' + color
109
+ : 'Button--color--default',
110
+ className,
111
+ computeBoxClassName(rest),
112
+ ])}
113
+ tabIndex={!disabled ? 0 : undefined}
114
+ onClick={(event) => {
115
+ if (!disabled && onClick) {
116
+ onClick(event);
117
+ }
118
+ }}
119
+ onKeyDown={(event) => {
120
+ if (!captureKeys) {
121
+ return;
122
+ }
123
+
124
+ // Simulate a click when pressing space or enter.
125
+ if (event.key === KEY.Space || event.key === KEY.Enter) {
126
+ event.preventDefault();
127
+ if (!disabled && onClick) {
128
+ onClick(event);
129
+ }
130
+ return;
131
+ }
132
+
133
+ // Refocus layout on pressing escape.
134
+ if (event.key === KEY.Escape) {
135
+ event.preventDefault();
136
+ }
137
+ }}
138
+ {...computeBoxProps(rest)}
139
+ >
140
+ <div className="Button__content">
141
+ {icon && iconPosition !== 'right' && (
142
+ <Icon
143
+ name={icon}
144
+ color={iconColor}
145
+ rotation={iconRotation}
146
+ spin={iconSpin}
147
+ />
148
+ )}
149
+ {!ellipsis ? (
150
+ toDisplay
151
+ ) : (
152
+ <span
153
+ className={classes([
154
+ 'Button--ellipsis',
155
+ icon && 'Button__textMargin',
156
+ ])}
157
+ >
158
+ {toDisplay}
159
+ </span>
160
+ )}
161
+ {icon && iconPosition === 'right' && (
162
+ <Icon
163
+ name={icon}
164
+ color={iconColor}
165
+ rotation={iconRotation}
166
+ spin={iconSpin}
167
+ />
168
+ )}
169
+ </div>
170
+ </div>
171
+ );
172
+
173
+ if (tooltip) {
174
+ buttonContent = (
175
+ <Tooltip content={tooltip} position={tooltipPosition as Placement}>
176
+ {buttonContent}
177
+ </Tooltip>
178
+ );
179
+ }
180
+
181
+ return buttonContent;
182
+ };
183
+
184
+ type CheckProps = Partial<{
185
+ checked: BooleanLike;
186
+ }> &
187
+ Props;
188
+
189
+ /** Visually toggles between checked and unchecked states. */
190
+ export const ButtonCheckbox = (props: CheckProps) => {
191
+ const { checked, ...rest } = props;
192
+
193
+ return (
194
+ <Button
195
+ color="transparent"
196
+ icon={checked ? 'check-square-o' : 'square-o'}
197
+ selected={checked}
198
+ {...rest}
199
+ />
200
+ );
201
+ };
202
+
203
+ Button.Checkbox = ButtonCheckbox;
204
+
205
+ type ConfirmProps = Partial<{
206
+ confirmColor: string;
207
+ confirmContent: ReactNode;
208
+ confirmIcon: string;
209
+ }> &
210
+ Props;
211
+
212
+ /** Requires user confirmation before triggering its action. */
213
+ const ButtonConfirm = (props: ConfirmProps) => {
214
+ const {
215
+ children,
216
+ color,
217
+ confirmColor = 'bad',
218
+ confirmContent = 'Confirm?',
219
+ confirmIcon,
220
+ ellipsis = true,
221
+ icon,
222
+ onClick,
223
+ ...rest
224
+ } = props;
225
+ const [clickedOnce, setClickedOnce] = useState(false);
226
+
227
+ const handleClick = (event: MouseEvent<HTMLDivElement>) => {
228
+ if (!clickedOnce) {
229
+ setClickedOnce(true);
230
+ return;
231
+ }
232
+
233
+ onClick?.(event);
234
+ setClickedOnce(false);
235
+ };
236
+
237
+ return (
238
+ <Button
239
+ icon={clickedOnce ? confirmIcon : icon}
240
+ color={clickedOnce ? confirmColor : color}
241
+ onClick={handleClick}
242
+ {...rest}
243
+ >
244
+ {clickedOnce ? confirmContent : children}
245
+ </Button>
246
+ );
247
+ };
248
+
249
+ Button.Confirm = ButtonConfirm;
250
+
251
+ type InputProps = Partial<{
252
+ currentValue: string;
253
+ defaultValue: string;
254
+ fluid: boolean;
255
+ maxLength: number;
256
+ onCommit: (e: any, value: string) => void;
257
+ placeholder: string;
258
+ }> &
259
+ Props;
260
+
261
+ /** Accepts and handles user input. */
262
+ const ButtonInput = (props: InputProps) => {
263
+ const {
264
+ children,
265
+ color = 'default',
266
+ content,
267
+ currentValue,
268
+ defaultValue,
269
+ disabled,
270
+ fluid,
271
+ icon,
272
+ iconRotation,
273
+ iconSpin,
274
+ maxLength,
275
+ onCommit = () => null,
276
+ placeholder,
277
+ tooltip,
278
+ tooltipPosition,
279
+ ...rest
280
+ } = props;
281
+ const [inInput, setInInput] = useState(false);
282
+ const inputRef = createRef<HTMLInputElement>();
283
+
284
+ const toDisplay = content || children;
285
+
286
+ const commitResult = (e) => {
287
+ const input = inputRef.current;
288
+ if (!input) return;
289
+
290
+ const hasValue = input.value !== '';
291
+ if (hasValue) {
292
+ onCommit(e, input.value);
293
+ } else {
294
+ if (defaultValue) {
295
+ onCommit(e, defaultValue);
296
+ }
297
+ }
298
+ };
299
+
300
+ useEffect(() => {
301
+ const input = inputRef.current;
302
+ if (!input) return;
303
+
304
+ if (inInput) {
305
+ input.value = currentValue || '';
306
+ try {
307
+ input.focus();
308
+ input.select();
309
+ } catch {}
310
+ }
311
+ }, [inInput, currentValue]);
312
+
313
+ let buttonContent = (
314
+ <Box
315
+ className={classes([
316
+ 'Button',
317
+ fluid && 'Button--fluid',
318
+ 'Button--color--' + color,
319
+ ])}
320
+ {...rest}
321
+ onClick={() => setInInput(true)}
322
+ >
323
+ {icon && <Icon name={icon} rotation={iconRotation} spin={iconSpin} />}
324
+ <div>{toDisplay}</div>
325
+ <input
326
+ disabled={!!disabled}
327
+ ref={inputRef}
328
+ className="NumberInput__input"
329
+ style={{
330
+ display: !inInput ? 'none' : '',
331
+ textAlign: 'left',
332
+ }}
333
+ onBlur={(event) => {
334
+ if (!inInput) {
335
+ return;
336
+ }
337
+ setInInput(false);
338
+ commitResult(event);
339
+ }}
340
+ onKeyDown={(event) => {
341
+ if (event.key === KEY.Enter) {
342
+ setInInput(false);
343
+ commitResult(event);
344
+ return;
345
+ }
346
+ if (event.key === KEY.Escape) {
347
+ setInInput(false);
348
+ }
349
+ }}
350
+ />
351
+ </Box>
352
+ );
353
+
354
+ if (tooltip) {
355
+ buttonContent = (
356
+ <Tooltip content={tooltip} position={tooltipPosition as Placement}>
357
+ {buttonContent}
358
+ </Tooltip>
359
+ );
360
+ }
361
+
362
+ return buttonContent;
363
+ };
364
+
365
+ Button.Input = ButtonInput;
366
+
367
+ type FileProps = {
368
+ accept: string;
369
+ multiple?: boolean;
370
+ onSelectFiles: (files: string | string[]) => void;
371
+ } & Props;
372
+
373
+ /** Accepts file input */
374
+ function ButtonFile(props: FileProps) {
375
+ const { accept, multiple, onSelectFiles, ...rest } = props;
376
+
377
+ const inputRef = useRef<HTMLInputElement>(null);
378
+
379
+ async function read(files: FileList) {
380
+ const promises = Array.from(files).map((file) => {
381
+ const reader = new FileReader();
382
+
383
+ return new Promise<string>((resolve) => {
384
+ reader.onload = () => resolve(reader.result as string);
385
+ reader.readAsText(file);
386
+ });
387
+ });
388
+
389
+ return await Promise.all(promises);
390
+ }
391
+
392
+ async function handleChange(event: ChangeEvent<HTMLInputElement>) {
393
+ const files = event.target.files;
394
+ if (files?.length) {
395
+ const readFiles = await read(files);
396
+ onSelectFiles(multiple ? readFiles : readFiles[0]);
397
+ }
398
+ }
399
+
400
+ return (
401
+ <>
402
+ <Button onClick={() => inputRef.current?.click()} {...rest} />
403
+ <input
404
+ hidden
405
+ type="file"
406
+ ref={inputRef}
407
+ accept={accept}
408
+ multiple={multiple}
409
+ onChange={handleChange}
410
+ />
411
+ </>
412
+ );
413
+ }
414
+
415
+ Button.File = ButtonFile;