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,255 @@
1
+ /**
2
+ * @file
3
+ * @copyright 2020 Aleksej Komarov
4
+ * @license MIT
5
+ */
6
+
7
+ import { BooleanLike, classes } from '../common/react';
8
+ import {
9
+ createElement,
10
+ KeyboardEventHandler,
11
+ MouseEventHandler,
12
+ ReactNode,
13
+ UIEventHandler,
14
+ } from 'react';
15
+
16
+ import { CSS_COLORS } from '../constants';
17
+
18
+ type BooleanProps = Partial<Record<keyof typeof booleanStyleMap, boolean>>;
19
+ type StringProps = Partial<
20
+ Record<keyof typeof stringStyleMap, string | BooleanLike>
21
+ >;
22
+
23
+ export type EventHandlers = Partial<{
24
+ onClick: MouseEventHandler<HTMLDivElement>;
25
+ onContextMenu: MouseEventHandler<HTMLDivElement>;
26
+ onDoubleClick: MouseEventHandler<HTMLDivElement>;
27
+ onKeyDown: KeyboardEventHandler<HTMLDivElement>;
28
+ onKeyUp: KeyboardEventHandler<HTMLDivElement>;
29
+ onMouseDown: MouseEventHandler<HTMLDivElement>;
30
+ onMouseMove: MouseEventHandler<HTMLDivElement>;
31
+ onMouseOver: MouseEventHandler<HTMLDivElement>;
32
+ onMouseUp: MouseEventHandler<HTMLDivElement>;
33
+ onScroll: UIEventHandler<HTMLDivElement>;
34
+ }>;
35
+
36
+ export type BoxProps = Partial<{
37
+ as: string;
38
+ children: ReactNode;
39
+ className: string | BooleanLike;
40
+ style: Partial<CSSStyleDeclaration>;
41
+ }> &
42
+ BooleanProps &
43
+ StringProps &
44
+ EventHandlers;
45
+
46
+ // Don't you dare put this elsewhere
47
+ type DangerDoNotUse = {
48
+ dangerouslySetInnerHTML?: {
49
+ __html: any;
50
+ };
51
+ };
52
+
53
+ /**
54
+ * Coverts our rem-like spacing unit into a CSS unit.
55
+ */
56
+ export const unit = (value: unknown) => {
57
+ if (typeof value === 'string') {
58
+ // Transparently convert pixels into rem units
59
+ if (value.endsWith('px')) {
60
+ return parseFloat(value) / 12 + 'rem';
61
+ }
62
+ return value;
63
+ }
64
+ if (typeof value === 'number') {
65
+ return value + 'rem';
66
+ }
67
+ };
68
+
69
+ /**
70
+ * Same as `unit`, but half the size for integers numbers.
71
+ */
72
+ export const halfUnit = (value: unknown) => {
73
+ if (typeof value === 'string') {
74
+ return unit(value);
75
+ }
76
+ if (typeof value === 'number') {
77
+ return unit(value * 0.5);
78
+ }
79
+ };
80
+
81
+ const isColorCode = (str: unknown) => !isColorClass(str);
82
+
83
+ const isColorClass = (str: unknown): boolean => {
84
+ return typeof str === 'string' && CSS_COLORS.includes(str as any);
85
+ };
86
+
87
+ const mapRawPropTo = (attrName) => (style, value) => {
88
+ if (typeof value === 'number' || typeof value === 'string') {
89
+ style[attrName] = value;
90
+ }
91
+ };
92
+
93
+ const mapUnitPropTo = (attrName, unit) => (style, value) => {
94
+ if (typeof value === 'number' || typeof value === 'string') {
95
+ style[attrName] = unit(value);
96
+ }
97
+ };
98
+
99
+ const mapBooleanPropTo = (attrName, attrValue) => (style, value) => {
100
+ if (value) {
101
+ style[attrName] = attrValue;
102
+ }
103
+ };
104
+
105
+ const mapDirectionalUnitPropTo = (attrName, unit, dirs) => (style, value) => {
106
+ if (typeof value === 'number' || typeof value === 'string') {
107
+ for (let i = 0; i < dirs.length; i++) {
108
+ style[attrName + '-' + dirs[i]] = unit(value);
109
+ }
110
+ }
111
+ };
112
+
113
+ const mapColorPropTo = (attrName) => (style, value) => {
114
+ if (isColorCode(value)) {
115
+ style[attrName] = value;
116
+ }
117
+ };
118
+
119
+ // String / number props
120
+ const stringStyleMap = {
121
+ align: mapRawPropTo('textAlign'),
122
+ bottom: mapUnitPropTo('bottom', unit),
123
+ fontFamily: mapRawPropTo('fontFamily'),
124
+ fontSize: mapUnitPropTo('fontSize', unit),
125
+ fontWeight: mapRawPropTo('fontWeight'),
126
+ height: mapUnitPropTo('height', unit),
127
+ left: mapUnitPropTo('left', unit),
128
+ maxHeight: mapUnitPropTo('maxHeight', unit),
129
+ maxWidth: mapUnitPropTo('maxWidth', unit),
130
+ minHeight: mapUnitPropTo('minHeight', unit),
131
+ minWidth: mapUnitPropTo('minWidth', unit),
132
+ opacity: mapRawPropTo('opacity'),
133
+ overflow: mapRawPropTo('overflow'),
134
+ overflowX: mapRawPropTo('overflowX'),
135
+ overflowY: mapRawPropTo('overflowY'),
136
+ position: mapRawPropTo('position'),
137
+ right: mapUnitPropTo('right', unit),
138
+ textAlign: mapRawPropTo('textAlign'),
139
+ top: mapUnitPropTo('top', unit),
140
+ verticalAlign: mapRawPropTo('verticalAlign'),
141
+ width: mapUnitPropTo('width', unit),
142
+
143
+ lineHeight: (style, value) => {
144
+ if (typeof value === 'number') {
145
+ style['lineHeight'] = value;
146
+ } else if (typeof value === 'string') {
147
+ style['lineHeight'] = unit(value);
148
+ }
149
+ },
150
+ // Margin
151
+ m: mapDirectionalUnitPropTo('margin', halfUnit, [
152
+ 'Top',
153
+ 'Bottom',
154
+ 'Left',
155
+ 'Right',
156
+ ]),
157
+ mb: mapUnitPropTo('marginBottom', halfUnit),
158
+ ml: mapUnitPropTo('marginLeft', halfUnit),
159
+ mr: mapUnitPropTo('marginRight', halfUnit),
160
+ mt: mapUnitPropTo('marginTop', halfUnit),
161
+ mx: mapDirectionalUnitPropTo('margin', halfUnit, ['Left', 'Right']),
162
+ my: mapDirectionalUnitPropTo('margin', halfUnit, ['Top', 'Bottom']),
163
+ // Padding
164
+ p: mapDirectionalUnitPropTo('padding', halfUnit, [
165
+ 'Top',
166
+ 'Bottom',
167
+ 'Left',
168
+ 'Right',
169
+ ]),
170
+ pb: mapUnitPropTo('paddingBottom', halfUnit),
171
+ pl: mapUnitPropTo('paddingLeft', halfUnit),
172
+ pr: mapUnitPropTo('paddingRight', halfUnit),
173
+ pt: mapUnitPropTo('paddingTop', halfUnit),
174
+ px: mapDirectionalUnitPropTo('padding', halfUnit, ['Left', 'Right']),
175
+ py: mapDirectionalUnitPropTo('padding', halfUnit, ['Top', 'Bottom']),
176
+ // Color props
177
+ color: mapColorPropTo('color'),
178
+ textColor: mapColorPropTo('color'),
179
+ backgroundColor: mapColorPropTo('backgroundColor'),
180
+ } as const;
181
+
182
+ // Boolean props
183
+ const booleanStyleMap = {
184
+ bold: mapBooleanPropTo('fontWeight', 'bold'),
185
+ fillPositionedParent: (style, value) => {
186
+ if (value) {
187
+ style['position'] = 'absolute';
188
+ style['top'] = 0;
189
+ style['bottom'] = 0;
190
+ style['left'] = 0;
191
+ style['right'] = 0;
192
+ }
193
+ },
194
+ inline: mapBooleanPropTo('display', 'inline-block'),
195
+ italic: mapBooleanPropTo('fontStyle', 'italic'),
196
+ nowrap: mapBooleanPropTo('whiteSpace', 'nowrap'),
197
+ preserveWhitespace: mapBooleanPropTo('whiteSpace', 'pre-wrap'),
198
+ } as const;
199
+
200
+ export const computeBoxProps = (props) => {
201
+ const computedProps: Record<string, any> = {};
202
+ const computedStyles: Record<string, string | number> = {};
203
+
204
+ // Compute props
205
+ for (let propName of Object.keys(props)) {
206
+ if (propName === 'style') {
207
+ continue;
208
+ }
209
+
210
+ const propValue = props[propName];
211
+
212
+ const mapPropToStyle =
213
+ stringStyleMap[propName] || booleanStyleMap[propName];
214
+
215
+ if (mapPropToStyle) {
216
+ mapPropToStyle(computedStyles, propValue);
217
+ } else {
218
+ computedProps[propName] = propValue;
219
+ }
220
+ }
221
+
222
+ // Merge computed styles and any directly provided styles
223
+ computedProps.style = { ...computedStyles, ...props.style };
224
+
225
+ return computedProps;
226
+ };
227
+
228
+ export const computeBoxClassName = (props: BoxProps) => {
229
+ const color = props.textColor || props.color;
230
+ const backgroundColor = props.backgroundColor;
231
+ return classes([
232
+ isColorClass(color) && 'color-' + color,
233
+ isColorClass(backgroundColor) && 'color-bg-' + backgroundColor,
234
+ ]);
235
+ };
236
+
237
+ export const Box = (props: BoxProps & DangerDoNotUse) => {
238
+ const { as = 'div', className, children, ...rest } = props;
239
+
240
+ // Compute class name and styles
241
+ const computedClassName = className
242
+ ? `${className} ${computeBoxClassName(rest)}`
243
+ : computeBoxClassName(rest);
244
+ const computedProps = computeBoxProps(rest);
245
+
246
+ // Render the component
247
+ return createElement(
248
+ typeof as === 'string' ? as : 'div',
249
+ {
250
+ ...computedProps,
251
+ className: computedClassName,
252
+ },
253
+ children
254
+ );
255
+ };
@@ -0,0 +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;