sliftutils 0.1.1

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 (47) hide show
  1. package/.cursorrules +161 -0
  2. package/.eslintrc.js +38 -0
  3. package/.vscode/settings.json +39 -0
  4. package/bundler/buffer.js +2370 -0
  5. package/bundler/bundleEntry.ts +32 -0
  6. package/bundler/bundleEntryCaller.ts +8 -0
  7. package/bundler/bundleRequire.ts +244 -0
  8. package/bundler/bundleWrapper.ts +115 -0
  9. package/bundler/bundler.ts +72 -0
  10. package/bundler/flattenSourceMaps.ts +0 -0
  11. package/bundler/sourceMaps.ts +261 -0
  12. package/misc/environment.ts +11 -0
  13. package/misc/types.ts +3 -0
  14. package/misc/zip.ts +37 -0
  15. package/package.json +24 -0
  16. package/spec.txt +33 -0
  17. package/storage/CachedStorage.ts +32 -0
  18. package/storage/DelayedStorage.ts +30 -0
  19. package/storage/DiskCollection.ts +272 -0
  20. package/storage/FileFolderAPI.tsx +427 -0
  21. package/storage/IStorage.ts +40 -0
  22. package/storage/IndexedDBFileFolderAPI.ts +170 -0
  23. package/storage/JSONStorage.ts +35 -0
  24. package/storage/PendingManager.tsx +63 -0
  25. package/storage/PendingStorage.ts +47 -0
  26. package/storage/PrivateFileSystemStorage.ts +192 -0
  27. package/storage/StorageObservable.ts +122 -0
  28. package/storage/TransactionStorage.ts +485 -0
  29. package/storage/fileSystemPointer.ts +81 -0
  30. package/storage/storage.d.ts +41 -0
  31. package/tsconfig.json +31 -0
  32. package/web/DropdownCustom.tsx +150 -0
  33. package/web/FullscreenModal.tsx +75 -0
  34. package/web/GenericFormat.tsx +186 -0
  35. package/web/Input.tsx +350 -0
  36. package/web/InputLabel.tsx +288 -0
  37. package/web/InputPicker.tsx +158 -0
  38. package/web/LocalStorageParam.ts +56 -0
  39. package/web/SyncedController.ts +405 -0
  40. package/web/SyncedLoadingIndicator.tsx +37 -0
  41. package/web/Table.tsx +188 -0
  42. package/web/URLParam.ts +84 -0
  43. package/web/asyncObservable.ts +40 -0
  44. package/web/colors.tsx +14 -0
  45. package/web/mobxTyped.ts +29 -0
  46. package/web/modal.tsx +18 -0
  47. package/web/observer.tsx +35 -0
package/web/Input.tsx ADDED
@@ -0,0 +1,350 @@
1
+ import preact from "preact";
2
+ import { css } from "typesafecss";
3
+ import { observer } from "./observer";
4
+ import { throttleFunction } from "socket-function/src/misc";
5
+
6
+ // TODO: Autogrow mode while typing
7
+
8
+ // NOTE: "value" is optional. If you don't pass "value", we will preserve the value.
9
+ // This is useful for inputs which you want to run an action on, such as "add new item",
10
+ // as it allows you to remove a local state value to cache the value, by just
11
+ // doing the add on "onChangeValue".
12
+ export type InputProps = (
13
+ preact.JSX.HTMLAttributes<HTMLInputElement>
14
+ & {
15
+ /** ONLY throttles onChangeValue */
16
+ throttle?: number;
17
+
18
+ flavor?: "large" | "small" | "none";
19
+ focusOnMount?: boolean;
20
+ textarea?: boolean;
21
+ /** Update on key stroke, not on blur (just does onInput = onChange, as onInput already does this) */
22
+ hot?: boolean;
23
+ /** Updates arrow keys with modifier behavior to use larger numbers, instead of decimals. */
24
+ integer?: boolean;
25
+
26
+ /** Only works with number/integer */
27
+ reverseArrowKeyDirection?: boolean;
28
+
29
+ inputRef?: (x: HTMLInputElement | null) => void;
30
+ /** Don't blur on enter key */
31
+ noEnterKeyBlur?: boolean;
32
+ noFocusSelect?: boolean;
33
+ inputKey?: string;
34
+ fillWidth?: boolean;
35
+
36
+
37
+ autocompleteValues?: string[];
38
+
39
+ /** Forces the input to update when focused. Usually we hold updates, to prevent the user's
40
+ * typing to be interrupted by background updates.
41
+ * NOTE: "hot" is usually required when using this.
42
+ */
43
+ forceInputValueUpdatesWhenFocused?: boolean;
44
+
45
+ // NOTE: We trigger onChange (and onChangeValue) whenever
46
+ // e.ctrlKey && (e.code.startsWith("Key") || e.code === "Enter") || e.code === "Enter" && e.shiftKey
47
+ // This is because ctrl usually means a hotkey, and hotkeys usually want committed values.
48
+ onChangeValue?: (value: string) => void;
49
+ }
50
+ );
51
+
52
+
53
+ @observer
54
+ export class Input extends preact.Component<InputProps> {
55
+ onFocusText = "";
56
+ firstFocus = true;
57
+
58
+ elem: HTMLInputElement | null = null;
59
+ lastValue: unknown = null;
60
+ lastChecked: unknown = null;
61
+
62
+ onChangeThrottle: undefined | {
63
+ throttle: number;
64
+ run: (newValue: string) => void;
65
+ } = undefined;
66
+
67
+ render() {
68
+ let flavorOverrides: preact.JSX.CSSProperties = {};
69
+ const { flavor, textarea, hot, inputKey, fillWidth, ...nativeProps } = this.props;
70
+ let props = { ...nativeProps } as preact.RenderableProps<InputProps>;
71
+
72
+ if (props.onChangeValue) {
73
+ let throttle = this.props.throttle;
74
+ if (throttle) {
75
+ let existingThrottle = this.onChangeThrottle;
76
+ if (existingThrottle?.throttle !== throttle) {
77
+ existingThrottle = this.onChangeThrottle = {
78
+ throttle,
79
+ run: throttleFunction(throttle, (newValue) => {
80
+ this.props.onChangeValue?.(newValue);
81
+ })
82
+ };
83
+ }
84
+ props.onChangeValue = existingThrottle.run;
85
+ }
86
+ }
87
+ if (flavor === "large") {
88
+ flavorOverrides = {
89
+ fontSize: 18,
90
+ padding: "10px 15px",
91
+ };
92
+ if (props.type === "checkbox") {
93
+ flavorOverrides.width = 16;
94
+ flavorOverrides.height = 16;
95
+ }
96
+ }
97
+ if (flavor === "small") {
98
+ flavorOverrides = {
99
+ fontSize: 12,
100
+ padding: "5px 10px",
101
+ };
102
+ }
103
+
104
+ // IMPORTANT! When focused, preserve the input value, otherwise typing is annoying.
105
+ // This doesn't usually happen, but can if background tasks are updating the UI
106
+ // while the user is typing.
107
+
108
+
109
+ let attributes: preact.JSX.HTMLAttributes<HTMLInputElement> = {
110
+ ...nativeProps,
111
+ key: inputKey || "input",
112
+ ref: x => {
113
+ if (x) {
114
+ this.elem = x;
115
+ }
116
+ if (x && props.focusOnMount && this.firstFocus) {
117
+ this.firstFocus = false;
118
+ setTimeout(() => {
119
+ x.focus();
120
+ }, 0);
121
+ }
122
+ let ref = props.inputRef;
123
+ if (typeof ref === "function") {
124
+ ref(x);
125
+ }
126
+ },
127
+ class: undefined,
128
+ className: (
129
+ (props.className || props.class || " ")
130
+ + css.display("flex", "soft")
131
+ .outline("3px solid hsl(204, 100%, 50%)", "focus", "soft")
132
+ + (fillWidth && css.fillWidth)
133
+ ),
134
+ style: {
135
+ ...flavorOverrides,
136
+ ...props.style as any,
137
+ },
138
+ onFocus: e => {
139
+ if (props.type === "checkbox") return;
140
+ this.onFocusText = e.currentTarget.value;
141
+ if (!props.noFocusSelect) {
142
+ e.currentTarget.select();
143
+ }
144
+ props.onFocus?.(e);
145
+ },
146
+ onBlur: e => {
147
+ if (props.type === "checkbox") return;
148
+ props.onBlur?.(e);
149
+ if (e.currentTarget.value === this.lastValue && e.currentTarget.checked === this.lastChecked && hot) return;
150
+ this.lastValue = e.currentTarget.value;
151
+ this.lastChecked = e.currentTarget.checked;
152
+ let result = props.onInput?.(e as any);
153
+ result = props.onChange?.(e) || result;
154
+ result = props.onChangeValue?.(e.currentTarget.value) || result;
155
+ return result;
156
+ },
157
+ onChange: e => {
158
+ if (props.type !== "checkbox" && e.currentTarget.value === this.lastValue && e.currentTarget.checked === this.lastChecked) return;
159
+ this.lastValue = e.currentTarget.value;
160
+ this.lastChecked = e.currentTarget.checked;
161
+ let result: unknown = undefined;
162
+ if (!props.onChangeValue || hot) {
163
+ result = props.onChange?.(e) || result;
164
+ }
165
+ if (hot) {
166
+ result = props.onChangeValue?.(e.currentTarget.value) || result;
167
+ }
168
+ return result;
169
+ },
170
+ onKeyDown: e => {
171
+ if (e.defaultPrevented) return;
172
+ // if (textarea && e.code === "Tab") {
173
+ // e.preventDefault();
174
+ // // Inject 4 spaces into the current position
175
+ // let elem = e.currentTarget;
176
+ // let value = elem.value;
177
+ // let start = elem.selectionStart ?? elem.value.length;
178
+ // let end = elem.selectionEnd ?? elem.value.length;
179
+ // elem.value = value.slice(0, start) + " " + value.slice(end);
180
+ // elem.selectionStart = elem.selectionEnd = start + 4;
181
+ // return;
182
+ // }
183
+ props.onKeyDown?.(e);
184
+ if (e.code === "Enter" && e.ctrlKey) {
185
+ e.currentTarget.blur();
186
+ }
187
+ let callback = props.onInput;
188
+ if (!callback && hot) {
189
+ callback = props.onChange;
190
+ }
191
+
192
+ // Convert tab to 4 spaces
193
+ if (e.code === "Tab" && textarea) {
194
+ e.preventDefault();
195
+ let elem = e.currentTarget;
196
+ let value = elem.value;
197
+ let start = elem.selectionStart ?? elem.value.length;
198
+ let end = elem.selectionEnd ?? elem.value.length;
199
+ elem.value = value.slice(0, start) + " " + value.slice(end);
200
+ elem.selectionStart = elem.selectionEnd = start + 4;
201
+ callback?.(e as unknown as preact.JSX.TargetedInputEvent<HTMLInputElement>);
202
+ return;
203
+ }
204
+ if (this.elem && props.type === "number") {
205
+ let delta = 0;
206
+ let magnitude = 1;
207
+ if (e.shiftKey) {
208
+ if (props.integer) {
209
+ magnitude = 10;
210
+ } else {
211
+ magnitude = 0.1;
212
+ }
213
+ }
214
+ if (e.code === "ArrowUp") {
215
+ delta = magnitude;
216
+ } else if (e.code === "ArrowDown") {
217
+ delta = -magnitude;
218
+ }
219
+ if (props.reverseArrowKeyDirection) {
220
+ delta *= -1;
221
+ }
222
+ if (delta !== 0) {
223
+ e.preventDefault();
224
+ let newValue = Math.round(((+this.elem.value || 0) + delta) * 100) / 100;
225
+ e.currentTarget.value = newValue.toString();
226
+ callback?.(e as unknown as preact.JSX.TargetedInputEvent<HTMLInputElement>);
227
+ }
228
+ }
229
+ let { noEnterKeyBlur, onInput, onChange } = props;
230
+ // Detach from the synced function, to prevent double calls. This is important, as apparently .blur()
231
+ // synchronously triggers onChange, BUT, only if the input is changing the first time. Which means
232
+ // if this function reruns, it won't trigger the change again. Detaching it causes any triggered
233
+ // functions to become root synced functions, which will allow them to correctly run the sync loop.
234
+ void Promise.resolve().finally(() => {
235
+ if (e.defaultPrevented) return;
236
+ if (e.code === "Escape") {
237
+ let changed = e.currentTarget.value !== this.onFocusText;
238
+ e.currentTarget.value = this.onFocusText;
239
+ if (onInput) {
240
+ onInput?.(e as unknown as preact.JSX.TargetedInputEvent<HTMLInputElement>);
241
+ } else if (changed) {
242
+ if (onChange) {
243
+ onChange?.(e);
244
+ }
245
+ }
246
+ e.currentTarget.blur();
247
+ }
248
+ if (!noEnterKeyBlur && e.code === "Enter" && (!textarea || e.shiftKey || e.ctrlKey) || props.autocompleteValues && e.code === "Tab") {
249
+ e.currentTarget.blur();
250
+ } else if (e.ctrlKey && (e.code.startsWith("Key") || e.code === "Enter") || e.shiftKey && e.code === "Enter") {
251
+ onChange?.(e);
252
+ }
253
+ });
254
+ },
255
+ onInput: e => {
256
+ if (props.autocompleteValues) {
257
+ if (e.inputType === "insertText") {
258
+ let curValue = e.currentTarget.value.toLowerCase();
259
+ let match = props.autocompleteValues.find(x => x.toLowerCase().startsWith(curValue));
260
+ if (match) {
261
+ e.currentTarget.value = match || "";
262
+ // Select the part after the previous match, so when they type, they clobber the match part
263
+ let start = curValue.length;
264
+ let end = e.currentTarget.value.length;
265
+ e.currentTarget.selectionStart = start;
266
+ e.currentTarget.selectionEnd = end;
267
+ }
268
+ }
269
+ }
270
+ props.onInput?.(e);
271
+ },
272
+ };
273
+
274
+
275
+ if ("value" in props && props.type !== "checkbox") {
276
+ let elem = this.elem;
277
+ let newValue = props.value;
278
+ if (!this.props.forceInputValueUpdatesWhenFocused && elem && elem === document.activeElement) {
279
+ newValue = elem.value;
280
+ }
281
+ attributes.value = newValue;
282
+ this.lastValue = String(props.value);
283
+ }
284
+ if ("checked" in props) {
285
+ this.lastChecked = !!props.checked;
286
+ }
287
+
288
+
289
+ if (attributes.type === "number") {
290
+ // Fix stuff like 55.00000000000001
291
+ let value = attributes.value;
292
+ if (typeof value === "number") {
293
+ value = niceNumberStringify(value);
294
+ }
295
+ }
296
+
297
+ // We do number handling ourselves
298
+ if (attributes["type"] === "number") {
299
+ delete attributes["type"];
300
+ }
301
+
302
+ if (props.type === "checkbox") {
303
+
304
+ } else if (hot) {
305
+ attributes.onInput = attributes.onChange;
306
+ } else {
307
+ // We use onChange from onBlur, so don't use the onChange handler, as preact will hook this up
308
+ // with onInput, which will cause it to trigger as if the component is hot!
309
+ delete attributes.onChange;
310
+ }
311
+ if (textarea) {
312
+ return <textarea {...attributes as any} />;
313
+ } else {
314
+ return <input {...attributes} />;
315
+ }
316
+ }
317
+ }
318
+
319
+
320
+ function niceNumberStringify(valueIn: number) {
321
+ if (Math.abs(valueIn) < 0.0000000001) {
322
+ return "0";
323
+ }
324
+ let value = valueIn.toString();
325
+ // TODO: Do this MUCH better...
326
+ if (value.slice(0, -1).endsWith("00000000000")) {
327
+ value = value.slice(0, -1);
328
+ while (value.endsWith("0")) {
329
+ value = value.slice(0, -1);
330
+ }
331
+ if (value.endsWith(".")) {
332
+ value = value.slice(0, -1);
333
+ }
334
+ return value;
335
+ }
336
+ if (value.slice(0, -1).endsWith("9999999999")) {
337
+ value = value.slice(0, -1);
338
+ while (value.endsWith("9")) {
339
+ value = value.slice(0, -1);
340
+ }
341
+ if (value.endsWith(".")) {
342
+ value = value.slice(0, -1);
343
+ }
344
+ // NOTE: Interestingly enough... because we remove all trailing 9s, it means if the last number is not 9,
345
+ // so... we can do this hack to round up
346
+ value = value.slice(0, -1) + (parseInt(value.slice(-1)) + 1);
347
+ return value;
348
+ }
349
+ return value;
350
+ }
@@ -0,0 +1,288 @@
1
+ import preact from "preact";
2
+ import { Input, InputProps } from "./Input";
3
+ import { css } from "typesafecss";
4
+ import { URLParamStr } from "./URLParam";
5
+ import { lazy } from "socket-function/src/caching";
6
+ import { observer } from "./observer";
7
+ import { observable } from "mobx";
8
+
9
+
10
+ export type InputLabelProps = Omit<InputProps, "label" | "title"> & {
11
+ label?: preact.ComponentChild;
12
+ number?: boolean;
13
+ /** A number, AND, an integer. Changes behavior arrow arrow keys as well */
14
+ integer?: boolean;
15
+ checkbox?: boolean;
16
+ // Show text and a pencil, only showing the input on click
17
+ edit?: boolean;
18
+ alwaysShowPencil?: boolean;
19
+ outerClass?: string;
20
+ maxDecimals?: number;
21
+ percent?: boolean;
22
+ editClass?: string;
23
+
24
+ fontSize?: number;
25
+
26
+ tooltip?: string;
27
+
28
+ fillWidth?: boolean;
29
+
30
+ useDateUI?: boolean;
31
+ };
32
+
33
+ function roundToDecimals(value: number, decimals: number) {
34
+ return Math.round(value * 10 ** decimals) / 10 ** decimals;
35
+ }
36
+
37
+ export const startGuessDateRange = +new Date(2010, 0, 1).getTime();
38
+ export const endGuessDateRange = +new Date(2050, 0, 1).getTime();
39
+
40
+ @observer
41
+ export class InputLabel extends preact.Component<InputLabelProps> {
42
+ synced = observable({
43
+ editting: false,
44
+ editInputValue: "",
45
+ editUpdateSeqNum: 0,
46
+ });
47
+
48
+ render() {
49
+ let props = { ...this.props };
50
+
51
+ function addValueMapping(mapper: (value: string) => string) {
52
+ const baseOnChange = props.onChange;
53
+ if (baseOnChange) {
54
+ props.onChange = e => {
55
+ baseOnChange({ currentTarget: { value: mapper(e.currentTarget.value) } } as any);
56
+ };
57
+ }
58
+ const baseOnChangeValue = props.onChangeValue;
59
+ if (baseOnChangeValue) {
60
+ props.onChangeValue = e => {
61
+ baseOnChangeValue(mapper(e));
62
+ };
63
+ }
64
+ const baseOnBlur = props.onBlur;
65
+ if (baseOnBlur) {
66
+ props.onBlur = e => {
67
+ baseOnBlur({ currentTarget: { value: mapper(e.currentTarget.value) } } as any);
68
+ };
69
+ }
70
+ const baseOnInput = props.onInput;
71
+ if (baseOnInput) {
72
+ props.onInput = e => {
73
+ baseOnInput({ currentTarget: { value: mapper(e.currentTarget.value) } } as any);
74
+ };
75
+ }
76
+ }
77
+
78
+ let label = props.label || props.children;
79
+ (props as any).title = props.tooltip;
80
+ if ("value" in props) {
81
+ props.value = props.value ?? "";
82
+ }
83
+
84
+ if ((!props.type || props.type === "number") && props.useDateUI) {
85
+ let value = String(props.value);
86
+ if (
87
+ value === "date-1be2200d-9742-4e90-a6c2-e8a790277414"
88
+ || isJSNumber(value) && startGuessDateRange < +value && +value < endGuessDateRange
89
+ // NOTE: Showing the date selector is a problem, as there is not an easy way to undo this. So if the
90
+ // user typed in "2020", it would return the input into a selector and not let them keeping type.
91
+ // || startGuessDateRange < +new Date(String(value)) && +new Date(String(value)) < endGuessDateRange
92
+ ) {
93
+ props.type = "datetime-local";
94
+ props.edit = false;
95
+ props.textarea = false;
96
+ props.number = false;
97
+ props.forceInputValueUpdatesWhenFocused = true;
98
+ // NOTE: When using forceInputValueUpdatesWhenFocused we need hot, otherwise the user's updates
99
+ // won't be visible.
100
+ props.hot = true;
101
+ if (isJSNumber(value)) {
102
+ value = formatDateTimeForInput(+value);
103
+ } else {
104
+ value = "";
105
+ }
106
+ props.value = value;
107
+ addValueMapping(value => (+new Date(value).getTime() || "") + "");
108
+ }
109
+ }
110
+ if (props.fontSize !== undefined) {
111
+ props.style = { ...props.style as any, fontSize: props.fontSize };
112
+ }
113
+ if (props.integer) {
114
+ props.number = true;
115
+ }
116
+ if (props.number) {
117
+ props.type = "number";
118
+ }
119
+ if (props.checkbox) {
120
+ props.type = "checkbox";
121
+ }
122
+ if (props.percent) {
123
+ props.value = (Number(props.value) || 0) * 100;
124
+ addValueMapping(value => String(+value / 100));
125
+ props.maxDecimals = props.maxDecimals ?? 2;
126
+ props.number = props.number ?? true;
127
+ props.type = "number";
128
+ }
129
+ let maxDecimals = props.maxDecimals;
130
+ if (typeof maxDecimals === "number") {
131
+ props.value = roundToDecimals(Number(props.value), maxDecimals);
132
+ }
133
+
134
+ function formatDateTimeForInput(value: number) {
135
+ value -= new Date(value).getTimezoneOffset() * 60 * 1000;
136
+ return new Date(value).toISOString().slice(0, -1);
137
+ }
138
+
139
+ let style = { ...props.style as any };
140
+ if (props.type === "number") {
141
+ let fontSize = props.fontSize ?? 12;
142
+ style.width = 40 + String(props.value).length * (fontSize * 0.75);
143
+ }
144
+ let onClick: ((e: preact.JSX.TargetedMouseEvent<HTMLElement>) => void) | undefined;
145
+ if (props.edit) {
146
+ let baseBlur = props.onBlur;
147
+ props.onBlur = e => {
148
+ this.synced.editting = false;
149
+ baseBlur?.(e);
150
+ };
151
+ onClick = (e) => {
152
+ e.stopPropagation();
153
+ this.synced.editting = true;
154
+ };
155
+ }
156
+
157
+ let stateEditting = this.synced.editting;
158
+ let sizeBasedOnContents = props.edit;
159
+ if (props.edit && !stateEditting) {
160
+ sizeBasedOnContents = false;
161
+ }
162
+ if (sizeBasedOnContents) {
163
+ let baseChange = props.onChange;
164
+ props.onChange = e => {
165
+ this.synced.editUpdateSeqNum++;
166
+ baseChange?.(e);
167
+ };
168
+ this.synced.editUpdateSeqNum;
169
+ }
170
+
171
+ let input = <Input
172
+ {...props}
173
+ label={String(label)}
174
+ style={style}
175
+ className={
176
+ (
177
+ props.flavor === "large" && "large "
178
+ || props.flavor === "none" && " "
179
+ || "tiny "
180
+ )
181
+ + (props.class || props.className)
182
+ + (sizeBasedOnContents && css.absolute.pos(0, 0).fillBoth.resize("none") || "")
183
+ }
184
+ onInput={e => {
185
+ if (props.edit) {
186
+ this.synced.editInputValue = e.currentTarget.value;
187
+ }
188
+ props.onInput?.(e);
189
+ }}
190
+ />;
191
+
192
+ if (props.edit && !stateEditting) {
193
+ input = <span className={
194
+ css.hbox(2) + " inputPlaceholder trigger-hover "
195
+ + props.editClass
196
+ }>
197
+ <span
198
+ className={
199
+ css.whiteSpace("pre-wrap")
200
+ + (props.class || props.className)
201
+ }
202
+ >
203
+ {props.value}
204
+ </span>
205
+ <span className={css.opacity(0.1).opacity(1, "hover")}>
206
+ {pencilSVG()}
207
+ </span>
208
+ </span>;
209
+ }
210
+ return (
211
+ <label onClick={onClick} className={
212
+ css.hbox(0).relative
213
+ + " trigger-hover "
214
+ + props.outerClass
215
+ + (props.flavor === "large" && css.fontSize(18, "soft"))
216
+ + (props.fillWidth && css.fillWidth)
217
+ + css.position("relative", "soft")
218
+ }>
219
+ {/* Extra UI so the textarea sizes properly. */}
220
+ {sizeBasedOnContents &&
221
+ <span
222
+ className={
223
+ css.whiteSpace("pre-wrap")
224
+ //.opacity(0)
225
+ //.pointerEvents("none")
226
+ + (props.editClass)
227
+ + " " + (props.class || props.className)
228
+ }
229
+ >
230
+ {/* We add another character, so "a\n" results in two lines, instead of 1. */}
231
+ {this.synced.editInputValue || props.value || props.placeholder} |
232
+ </span>
233
+ }
234
+ {/* <div
235
+ className={
236
+ "show-on-hover "
237
+ + css.hsla(0, 0, 0, 0.2)
238
+ .absolute.pos(-8, -2).size("calc(100% + 16px)" as "100%", "calc(100% + 4px)" as "100%")
239
+ .zIndex(-1)
240
+ .pointerEvents("none")
241
+ }
242
+ /> */}
243
+ {label && <span className={css.fontWeight("bold").flexShrink0}>{label}</span>}
244
+ {input}
245
+ </label>
246
+ );
247
+ }
248
+ }
249
+
250
+ const pencilSVG = lazy(() => {
251
+ const src = "data:image/svg+xml;base64," + Buffer.from(`
252
+ <svg width="24" height="24" viewBox="-1 1 23 25" fill="none" xmlns="http://www.w3.org/2000/svg">
253
+ <path d="M5.98012 19.3734L8.60809 18.7164C8.62428 18.7124 8.64043 18.7084 8.65654 18.7044C8.87531 18.65 9.08562 18.5978 9.27707 18.4894C9.46852 18.381 9.62153 18.2275 9.7807 18.0679C9.79242 18.0561 9.80418 18.0444 9.81598 18.0325L17.0101 10.8385L17.0101 10.8385L17.0369 10.8117C17.3472 10.5014 17.6215 10.2272 17.8128 9.97638C18.0202 9.70457 18.1858 9.39104 18.1858 9C18.1858 8.60896 18.0202 8.29543 17.8128 8.02361C17.6215 7.77285 17.3472 7.49863 17.0369 7.18835L17.01 7.16152L16.8385 6.98995L16.8117 6.96314C16.5014 6.6528 16.2272 6.37853 15.9764 6.1872C15.7046 5.97981 15.391 5.81421 15 5.81421C14.609 5.81421 14.2954 5.97981 14.0236 6.1872C13.7729 6.37853 13.4986 6.65278 13.1884 6.96311L13.1615 6.98995L5.96745 14.184C5.95565 14.1958 5.94386 14.2076 5.93211 14.2193C5.77249 14.3785 5.61904 14.5315 5.51064 14.7229C5.40225 14.9144 5.34999 15.1247 5.29562 15.3435C5.29162 15.3596 5.28761 15.3757 5.28356 15.3919L4.62003 18.046C4.61762 18.0557 4.61518 18.0654 4.61272 18.0752C4.57411 18.2293 4.53044 18.4035 4.51593 18.5518C4.49978 18.7169 4.50127 19.0162 4.74255 19.2574C4.98383 19.4987 5.28307 19.5002 5.44819 19.4841C5.59646 19.4696 5.77072 19.4259 5.92479 19.3873C5.9346 19.3848 5.94433 19.3824 5.95396 19.38L5.95397 19.38L5.9801 19.3734L5.98012 19.3734Z" stroke="#33363F" stroke-width="1.2" fill="hsl(330, 50%, 60%)" />
254
+ <path d="M12.5 7.5L5.92819 14.0718C5.71566 14.2843 5.60939 14.3906 5.53953 14.5212C5.46966 14.6517 5.44019 14.7991 5.38124 15.0938L4.64709 18.7646C4.58057 19.0972 4.5473 19.2635 4.64191 19.3581C4.73652 19.4527 4.90283 19.4194 5.23544 19.3529L8.90621 18.6188C9.20093 18.5598 9.3483 18.5303 9.47885 18.4605C9.60939 18.3906 9.71566 18.2843 9.92819 18.0718L16.5 11.5L12.5 7.5Z" fill="hsl(45, 100%, 50%)" />
255
+ <path d="M12.5 7.5L16.5 11.5" stroke="#33363F" stroke-width="1.2" />
256
+ </svg>
257
+ `).toString("base64");
258
+ return <img draggable={false} src={src} />;
259
+ });
260
+
261
+ @observer
262
+ export class InputLabelURL extends preact.Component<InputLabelProps & {
263
+ persisted: URLParamStr;
264
+ }> {
265
+ render() {
266
+ this.props.persisted.value;
267
+ let props = { ...this.props };
268
+ if (props.type === "number" || props.number) {
269
+ return <InputLabel {...props} value={Number(props.persisted.value) || 0} onChange={e => { props.persisted.value = e.currentTarget.value; props.onChange?.(e); }} />;
270
+ } else if (props.type === "checkbox" || this.props.checkbox) {
271
+ return <InputLabel
272
+ {...props}
273
+ checked={Boolean(props.persisted.value) || false}
274
+ onFocus={e => e.currentTarget.blur()}
275
+ onChange={e => {
276
+ props.persisted.value = e.currentTarget.checked ? "1" : "";
277
+ props.onChange?.(e);
278
+ }}
279
+ />;
280
+ } else {
281
+ return <InputLabel {...props} value={String(props.persisted.value) || ""} onChange={e => { props.persisted.value = e.currentTarget.value; props.onChange?.(e); }} />;
282
+ }
283
+ }
284
+ }
285
+
286
+ function isJSNumber(value: string) {
287
+ return !isNaN(+value);
288
+ }