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
@@ -0,0 +1,158 @@
1
+ import preact from "preact";
2
+ import { observable } from "mobx";
3
+ import { observer } from "./observer";
4
+ import { sort } from "socket-function/src/misc";
5
+ import { css } from "typesafecss";
6
+ import { Input } from "./Input";
7
+ import { greenButton, yellowButton } from "./colors";
8
+
9
+ export type InputOption<T> = {
10
+ value: T;
11
+ // Defaults to String(value)
12
+ label?: preact.ComponentChild;
13
+ // Defaults to typeof label === "string" ? label : String(value)
14
+ matchText?: string;
15
+ };
16
+ export type FullInputOption<T> = {
17
+ value: T;
18
+ label: preact.ComponentChild;
19
+ matchText: string;
20
+ };
21
+
22
+ @observer
23
+ export class InputPickerURL extends preact.Component<{
24
+ label?: preact.ComponentChild;
25
+ options: (string | InputOption<string>)[];
26
+ allowNonOptions?: boolean;
27
+ value: { value: string };
28
+ }> {
29
+ render() {
30
+ let { value, options, ...remaining } = this.props;
31
+ let values = new Set(value.value.split("|").filter(x => x));
32
+ return <InputPicker
33
+ {...remaining}
34
+ picked={Array.from(values)}
35
+ addPicked={v => {
36
+ values.add(v);
37
+ value.value = Array.from(values).join("|");
38
+ }}
39
+ removePicked={v => {
40
+ values.delete(v);
41
+ value.value = Array.from(values).join("|");
42
+ }}
43
+ options={options.map(x => typeof x === "string" ? { value: x } : x)}
44
+ />;
45
+ }
46
+ }
47
+
48
+ @observer
49
+ export class InputPicker<T> extends preact.Component<{
50
+ label?: preact.ComponentChild;
51
+ picked: T[];
52
+ options: InputOption<T>[];
53
+ addPicked: (value: T) => void;
54
+ removePicked: (value: T) => void;
55
+ allowNonOptions?: boolean;
56
+ }> {
57
+ synced = observable({
58
+ pendingText: "",
59
+ focused: false,
60
+ });
61
+ render() {
62
+ // Input, and beside it the picked values
63
+ let resolvedOptions = this.props.options.map(option => {
64
+ let value = option.value;
65
+ let label = option.label ?? String(value);
66
+ let matchText = option.matchText ?? (typeof label === "string" ? label : String(value));
67
+ return { value, label, matchText };
68
+ });
69
+ let optionLookup = new Map(resolvedOptions.map((option) => [option.value, option]));
70
+ let pickedOptions = this.props.picked.map(x => optionLookup.get(x) || { value: x, label: String(x), matchText: String(x) });
71
+ let pendingMatches: FullInputOption<T>[] = [];
72
+ let pendingTextFull = this.synced.pendingText;
73
+ let pendingText = pendingTextFull.trim().toLowerCase();
74
+ if (pendingText) {
75
+ pendingMatches = resolvedOptions.filter(option => option.matchText.toLowerCase().includes(pendingText));
76
+ sort(pendingMatches, x =>
77
+ x.matchText.startsWith(pendingTextFull) && -10
78
+ || x.matchText.startsWith(pendingText) && -9
79
+ || x.matchText.toLowerCase().startsWith(pendingTextFull) && -8
80
+ || x.matchText.toLowerCase().startsWith(pendingText) && -7
81
+ || x.matchText.length
82
+ );
83
+ } else if (this.synced.focused) {
84
+ pendingMatches = resolvedOptions;
85
+ }
86
+ let extra = pendingMatches.length;
87
+ pendingMatches = pendingMatches.slice(0, 10);
88
+ extra -= pendingMatches.length;
89
+ return (
90
+ <div className={css.hbox(10).alignItems("start")}>
91
+ {this.props.label}
92
+ <Input
93
+ value={this.synced.pendingText}
94
+ hot
95
+ forceInputValueUpdatesWhenFocused
96
+ onChangeValue={(x) => this.synced.pendingText = x}
97
+ onFocus={() => this.synced.focused = true}
98
+ onBlur={() => {
99
+ this.synced.focused = false;
100
+ this.synced.pendingText = "";
101
+ }}
102
+ onKeyDown={e => {
103
+ // On tab, add first in pendingMatches
104
+ if (e.key === "Tab") {
105
+ e.preventDefault();
106
+ if (pendingMatches.length > 0) {
107
+ this.props.addPicked(pendingMatches[0].value);
108
+ this.synced.pendingText = "";
109
+ } else if (this.props.allowNonOptions) {
110
+ this.props.addPicked(this.synced.pendingText as T);
111
+ this.synced.pendingText = "";
112
+ }
113
+ } else if (e.key === "Enter" && this.props.allowNonOptions) {
114
+ // HACK: I don't even know, this is just terrible. But it is used to fix some UI where we needed a way to inject text
115
+ this.props.addPicked(this.synced.pendingText as T);
116
+ this.synced.pendingText = "";
117
+ }
118
+ }}
119
+ />
120
+ {pendingMatches.length > 0 && (
121
+ <div className={css.hbox(4).wrap}>
122
+ {pendingMatches.map((option) => (
123
+ <button
124
+ key={`add-${option.matchText}`}
125
+ className={css.hbox(5).button + greenButton}
126
+ // On mouse down, so we can add picked BEFORE we blur (otherwise
127
+ // this button disappears before it can be clicked)
128
+ onMouseDown={() => {
129
+ this.props.addPicked(option.value);
130
+ }}
131
+ >
132
+ + {option.label}
133
+ </button>
134
+ ))}
135
+ {extra > 0 && (
136
+ <button className={css.hbox(5).button} disabled>
137
+ + {extra} more...
138
+ </button>
139
+ )}
140
+ </div>
141
+ )}
142
+ <div className={css.hbox(4).wrap}>
143
+ {pickedOptions.map((option) => (
144
+ <button
145
+ key={`remove-${option.matchText}`}
146
+ className={css.hbox(5).button + yellowButton}
147
+ onMouseDown={() => {
148
+ this.props.removePicked(option.value);
149
+ }}
150
+ >
151
+ - {option.label}
152
+ </button>
153
+ ))}
154
+ </div>
155
+ </div>
156
+ );
157
+ }
158
+ }
@@ -0,0 +1,56 @@
1
+ import { isNode } from "typesafecss";
2
+ import { observable } from "mobx";
3
+
4
+ let allParams: LocalStorageParamStr[] = [];
5
+
6
+ export class LocalStorageParamStr {
7
+ private state = observable({
8
+ seqNum: 0
9
+ });
10
+ public lastSetValue = "";
11
+ constructor(public readonly storageKey: string, private defaultValue: string = "") {
12
+ allParams.push(this);
13
+ }
14
+ public forceUpdate() {
15
+ this.state.seqNum++;
16
+ }
17
+
18
+ public get() {
19
+ this.state.seqNum;
20
+ if (isNode()) return "";
21
+ return localStorage.getItem(this.storageKey) || "";
22
+ }
23
+ public set(value: string) {
24
+ let prev = this.get();
25
+ this.lastSetValue = value;
26
+ if (!isNode()) {
27
+ if (value === "") {
28
+ localStorage.removeItem(this.storageKey);
29
+ } else {
30
+ localStorage.setItem(this.storageKey, value);
31
+ }
32
+ }
33
+ let after = this.get();
34
+ if (prev !== after) {
35
+ this.state.seqNum++;
36
+ }
37
+ }
38
+
39
+ public get value() {
40
+ return this.get() || this.defaultValue;
41
+ }
42
+ public set value(value: string) {
43
+ this.set(value);
44
+ }
45
+ }
46
+
47
+ if (!isNode()) {
48
+ // Watch for storage events from other tabs/windows
49
+ window.addEventListener("storage", (e) => {
50
+ for (let param of allParams) {
51
+ if (e.key === param.storageKey) {
52
+ param.forceUpdate();
53
+ }
54
+ }
55
+ });
56
+ }
@@ -0,0 +1,405 @@
1
+ import { SocketFunction } from "socket-function/SocketFunction";
2
+ import { SocketRegistered } from "socket-function/SocketFunctionTypes";
3
+ import { onHotReload } from "socket-function/hot/HotReloadController";
4
+ import { cache } from "socket-function/src/caching";
5
+ import { nextId } from "socket-function/src/misc";
6
+ import { MaybePromise } from "socket-function/src/types";
7
+ import { observable } from "mobx";
8
+ import { formatTime } from "socket-function/src/formatting/format";
9
+ import { isNode } from "typesafecss";
10
+ import { delay } from "socket-function/src/batching";
11
+
12
+ module.hotreload = false;
13
+
14
+ let syncedData: {
15
+ [controllerId: string]: {
16
+ [nodeId: string]: {
17
+ [fnc: string]: {
18
+ [argsHash: string]: {
19
+ promise: Promise<unknown> | undefined;
20
+ result?: { result: unknown } | { error: Error };
21
+ invalidated?: boolean;
22
+ setCacheSeqNumber: number;
23
+ } | undefined;
24
+ } | undefined;
25
+ } | undefined;
26
+ } | undefined;
27
+ } = {};
28
+ let syncedDataSeqNum = observable({
29
+ value: 0
30
+ });
31
+
32
+ function commit<T>(code: () => T) {
33
+ let result = code();
34
+ syncedDataSeqNum.value++;
35
+ return result;
36
+ }
37
+ function read<T>(code: () => T) {
38
+ syncedDataSeqNum.value;
39
+ return code();
40
+ }
41
+
42
+ type RemapFunction<T> = T extends (...args: infer Args) => Promise<infer Return>
43
+ ? {
44
+ (...args: Args): Return | undefined;
45
+ promise(...args: Args): Promise<Return>;
46
+ refresh(...args: Args): void;
47
+ refreshAll(): void;
48
+ reset(...args: Args): void;
49
+ resetAll(): void;
50
+ isLoading(...args: Args): boolean;
51
+ setCache(cache: {
52
+ args: Args;
53
+ result: Return;
54
+ }): void;
55
+ }
56
+ : T;
57
+
58
+ // key =>
59
+ const writeWatchers = new Map<string, {
60
+ controllerId: string;
61
+ fncName: string;
62
+ }[]>();
63
+
64
+ export function getSyncedController<T extends SocketRegistered>(
65
+ controller: T,
66
+ config?: {
67
+ /** When a controller call for a write finishes, we refresh all readers.
68
+ * - Invalidation is global, across all controllers.
69
+ */
70
+ reads?: { [key in keyof T["nodes"][""]]?: string[]; };
71
+ writes?: { [key in keyof T["nodes"][""]]?: string[]; };
72
+ }
73
+ ): {
74
+ (nodeId: string): {
75
+ [fnc in keyof T["nodes"][""]]: RemapFunction<T["nodes"][""][fnc]>;
76
+ } & {
77
+ resetAll(): void;
78
+ refreshAll(): void;
79
+ anyPending(): boolean;
80
+ };
81
+ resetAll(): void;
82
+ refreshAll(): void;
83
+ anyPending(): boolean;
84
+ rerenderAll(): void;
85
+ } {
86
+ if (isNode()) {
87
+ let result = cache((nodeId: string) => {
88
+ let proxy = new Proxy({}, {
89
+ get: (target, fncNameUntyped) => {
90
+ if (typeof fncNameUntyped !== "string") return undefined;
91
+ if (fncNameUntyped === "resetAll" || fncNameUntyped === "refreshAll" || fncNameUntyped === "isAnyLoading") {
92
+ return notAllowedOnServer;
93
+ }
94
+ let fncName = fncNameUntyped;
95
+ function call(...args: any[]) {
96
+ notAllowedOnServer();
97
+ }
98
+ call.promise = (...args: any[]) => {
99
+ return controller.nodes[nodeId][fncName](...args);
100
+ };
101
+ call.reset = (...args: any[]) => {
102
+ notAllowedOnServer();
103
+ };
104
+ call.resetAll = () => {
105
+ notAllowedOnServer();
106
+ };
107
+ call.refresh = (...args: any[]) => {
108
+ notAllowedOnServer();
109
+ };
110
+ call.refreshAll = () => {
111
+ notAllowedOnServer();
112
+ };
113
+ call.isAnyLoading = () => {
114
+ notAllowedOnServer();
115
+ };
116
+ call.setCache = (config: { args: any[], result: any }) => {
117
+ notAllowedOnServer();
118
+ };
119
+ return call;
120
+ }
121
+ }) as any;
122
+ return proxy;
123
+ }) as any;
124
+ function notAllowedOnServer() {
125
+ throw new Error(`Syncing with getSyncedController is not allowed on the server. You can call promise, but not the synced functions.`);
126
+ }
127
+ result.resetAll = () => {
128
+ notAllowedOnServer();
129
+ };
130
+ result.refreshAll = () => {
131
+ notAllowedOnServer();
132
+ };
133
+ result.isAnyLoading = () => {
134
+ notAllowedOnServer();
135
+ };
136
+ return result;
137
+ }
138
+ let controllerId = nextId();
139
+
140
+ for (let [fncName, keys] of Object.entries(config?.reads ?? {})) {
141
+ for (let key of keys || []) {
142
+ let watcherList = writeWatchers.get(key);
143
+ if (!watcherList) {
144
+ watcherList = [];
145
+ writeWatchers.set(key, watcherList);
146
+ }
147
+ watcherList.push({ controllerId, fncName });
148
+ }
149
+ }
150
+
151
+ let result = cache((nodeId: string) => {
152
+ SocketFunction.onNextDisconnect(nodeId, () => {
153
+ commit(() => {
154
+ delete syncedData[controllerId]?.[nodeId];
155
+ });
156
+ });
157
+ return new Proxy({}, {
158
+ get: (target, fncNameUntyped) => {
159
+ if (typeof fncNameUntyped !== "string") return undefined;
160
+ if (fncNameUntyped === "resetAll") {
161
+ return () => {
162
+ return commit(() => {
163
+ delete syncedData[controllerId]?.[nodeId];
164
+ });
165
+ };
166
+ }
167
+ if (fncNameUntyped === "refreshAll") {
168
+ return () => {
169
+ return commit(() => {
170
+ let nodeObj = syncedData[controllerId]?.[nodeId];
171
+ if (!nodeObj) return;
172
+ for (let fncObj of Object.values(nodeObj)) {
173
+ for (let argsHash in fncObj) {
174
+ let obj = fncObj[argsHash];
175
+ if (!obj) continue;
176
+ obj.invalidated = true;
177
+ }
178
+ }
179
+ });
180
+ };
181
+ }
182
+ if (fncNameUntyped === "anyPending") {
183
+ return () => {
184
+ return read(() => {
185
+ let nodeObj = syncedData[controllerId]?.[nodeId];
186
+ if (!nodeObj) return false;
187
+ for (let fncObj of Object.values(nodeObj)) {
188
+ for (let argsHash in fncObj) {
189
+ let obj = fncObj[argsHash];
190
+ if (!obj) continue;
191
+ if (obj.promise) return true;
192
+ }
193
+ }
194
+ return false;
195
+ });
196
+ };
197
+ }
198
+ let fncName = fncNameUntyped;
199
+ function getObj(...args: any[]) {
200
+ let argsHash = JSON.stringify(args);
201
+ let controllerObj = syncedData[controllerId];
202
+ if (!controllerObj) {
203
+ controllerObj = syncedData[controllerId] = {};
204
+ }
205
+ let nodeObj = controllerObj[nodeId];
206
+ if (!nodeObj) {
207
+ nodeObj = controllerObj[nodeId] = {};
208
+ }
209
+ let fncObj = nodeObj[fncName];
210
+ if (!fncObj) {
211
+ fncObj = nodeObj[fncName] = {};
212
+ }
213
+ let obj = fncObj[argsHash];
214
+ if (!obj) {
215
+ obj = fncObj[argsHash] = {
216
+ promise: undefined,
217
+ result: undefined,
218
+ setCacheSeqNumber: 0,
219
+ };
220
+ }
221
+ return obj;
222
+ }
223
+ function call(...args: any[]) {
224
+ return read(() => {
225
+ let obj = getObj(...args);
226
+
227
+ if (!obj.promise && (!obj.result || obj.invalidated)) {
228
+ obj.invalidated = false;
229
+ let time = Date.now();
230
+ let finished = false;
231
+ function logFinished() {
232
+ finished = true;
233
+ let duration = Date.now() - time;
234
+ if (duration > 500) {
235
+ console.warn(`Slow call ${fncName} took ${formatTime(duration)}`);
236
+ }
237
+ }
238
+ let promise = controller.nodes[nodeId][fncName](...args) as Promise<unknown>;
239
+ obj.promise = promise;
240
+ function invalidateReaders() {
241
+ let root = syncedData;
242
+ for (let writesTo of config?.writes?.[fncName] || []) {
243
+ for (let watcher of writeWatchers.get(writesTo) || []) {
244
+ for (let nodeObj of Object.values(root[watcher.controllerId] ?? {})) {
245
+ for (let fncObj of Object.values(nodeObj || {})) {
246
+ for (let obj of Object.values(fncObj || {})) {
247
+ if (!obj) continue;
248
+ obj.invalidated = true;
249
+ }
250
+ }
251
+ }
252
+ }
253
+ }
254
+ }
255
+ let setCacheSeqNumber = obj.setCacheSeqNumber;
256
+ // Run a loop warning every 10 seconds that the call is still pending until it's finished.
257
+ void (async () => {
258
+ while (true) {
259
+ await delay(10000);
260
+ if (finished) break;
261
+ console.warn(`Slow call, running for ${formatTime(Date.now() - time)}: ${fncName}`);
262
+ }
263
+ })();
264
+ promise.then(
265
+ result => {
266
+ invalidateReaders();
267
+ logFinished();
268
+ commit(() => {
269
+ obj.promise = undefined;
270
+ if (obj.setCacheSeqNumber === setCacheSeqNumber) {
271
+ obj.result = { result };
272
+ }
273
+ });
274
+ },
275
+ error => {
276
+ invalidateReaders();
277
+ logFinished();
278
+ commit(() => {
279
+ obj.promise = undefined;
280
+ if (obj.setCacheSeqNumber === setCacheSeqNumber) {
281
+ obj.result = { error };
282
+ }
283
+ });
284
+ }
285
+ );
286
+ }
287
+
288
+ let result = obj.result;
289
+ if (result) {
290
+ if ("error" in result) {
291
+ throw result.error;
292
+ } else {
293
+ return result.result;
294
+ }
295
+ }
296
+ return undefined;
297
+ });
298
+ }
299
+ call.promise = (...args: any[]) => {
300
+ let obj = getObj(...args);
301
+ // Reset promise, to force it to not use the cache, as promise functions should never be cached. This might result in the results being set out of order, but... generally functions called with promise and accessed inside a watcher, so this should be fine.
302
+ obj.promise = undefined;
303
+ obj.invalidated = true;
304
+ call(...args);
305
+ if (!obj.promise) {
306
+ debugger;
307
+ throw new Error("Promise is undefined after calling function?");
308
+ }
309
+ // Reset typeguards, as typescript doesn't think call will change obj.promise
310
+ obj = obj as any;
311
+ let promise = obj.promise;
312
+ void promise?.finally(() => {
313
+ // Don't cache it, we never want to cache pure promise calls.
314
+ if (promise === obj.promise) {
315
+ obj.promise = undefined;
316
+ }
317
+ });
318
+ return obj.promise;
319
+ };
320
+ call.reset = (...args: any[]) => {
321
+ return commit(() => {
322
+ let argsHash = JSON.stringify(args);
323
+ let obj = syncedData[controllerId]?.[nodeId]?.[fncName]?.[argsHash];
324
+ if (!obj) return;
325
+ delete obj.promise;
326
+ delete obj.result;
327
+ });
328
+ };
329
+ call.resetAll = () => {
330
+ return commit(() => {
331
+ delete syncedData[controllerId]?.[nodeId]?.[fncName];
332
+ });
333
+ };
334
+ call.refresh = (...args: any[]) => {
335
+ return commit(() => {
336
+ let argsHash = JSON.stringify(args);
337
+ let obj = syncedData[controllerId]?.[nodeId]?.[fncName]?.[argsHash];
338
+ if (!obj) return;
339
+ obj.invalidated = true;
340
+ });
341
+ };
342
+ call.refreshAll = () => {
343
+ return commit(() => {
344
+ delete syncedData[controllerId]?.[nodeId]?.[fncName];
345
+ });
346
+ };
347
+ call.isLoading = (...args: any[]) => {
348
+ return read(() => {
349
+ let argsHash = JSON.stringify(args);
350
+ let obj = syncedData[controllerId]?.[nodeId]?.[fncName]?.[argsHash];
351
+ return !!obj?.promise;
352
+ });
353
+ };
354
+ call.setCache = (cache: {
355
+ args: any[];
356
+ result: unknown;
357
+ }) => {
358
+ return commit(() => {
359
+ let obj = getObj(...cache.args);
360
+ obj.result = { result: cache.result };
361
+ obj.promise = undefined;
362
+ obj.setCacheSeqNumber++;
363
+ });
364
+ };
365
+ return call;
366
+ },
367
+ });
368
+ }) as any;
369
+ result.resetAll = () => {
370
+ return commit(() => {
371
+ delete syncedData[controllerId];
372
+ });
373
+ };
374
+ result.refreshAll = () => {
375
+ return commit(() => {
376
+ for (let node of Object.values(syncedData[controllerId] ?? {})) {
377
+ for (let fncObj of Object.values(node || {})) {
378
+ for (let obj of Object.values(fncObj || {})) {
379
+ if (!obj) continue;
380
+ obj.invalidated = true;
381
+ }
382
+ }
383
+ }
384
+ });
385
+ };
386
+ result.anyPending = () => {
387
+ return read(() => {
388
+ if (!syncedData[controllerId]) return false;
389
+ for (let node of Object.values(syncedData[controllerId] ?? {})) {
390
+ for (let fncObj of Object.values(node || {})) {
391
+ for (let obj of Object.values(fncObj || {})) {
392
+ if (!obj) continue;
393
+ if (obj.promise) return true;
394
+ }
395
+ }
396
+ }
397
+ return false;
398
+ });
399
+ };
400
+ result.rerenderAll = () => {
401
+ return commit(() => {
402
+ });
403
+ };
404
+ return result;
405
+ }
@@ -0,0 +1,37 @@
1
+ import { observer } from "./observer";
2
+ import * as preact from "preact";
3
+ import { css } from "typesafecss";
4
+
5
+ @observer
6
+ export class SyncedLoadingIndicator extends preact.Component<{
7
+ controller: { anyPending: () => boolean };
8
+ }> {
9
+ render() {
10
+ let { controller } = this.props;
11
+ let pending = controller.anyPending();
12
+ if (!pending) return null;
13
+
14
+ return <div className={css.hbox(8).pad2(12).hsl(220, 50, 20).bord2(220, 50, 40, 1).borderRadius(6)}>
15
+ <style>
16
+ {`
17
+ @keyframes syncSpin {
18
+ 0% { transform: rotate(0deg); }
19
+ 100% { transform: rotate(360deg); }
20
+ }
21
+ .sync-spinner {
22
+ animation: syncSpin 1s linear infinite;
23
+ }
24
+ @keyframes syncPulse {
25
+ 0%, 100% { opacity: 0.6; }
26
+ 50% { opacity: 1; }
27
+ }
28
+ .sync-pulse {
29
+ animation: syncPulse 1.5s ease-in-out infinite;
30
+ }
31
+ `}
32
+ </style>
33
+ <div className={css.size(16, 16).bord2(220, 60, 70, 2).borderRadius(50).borderTopColor("transparent") + " sync-spinner"}></div>
34
+ <div className={css.colorhsl(220, 60, 80).fontSize(14) + " sync-pulse"}>Syncing...</div>
35
+ </div>;
36
+ }
37
+ }