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.
- package/.cursorrules +161 -0
- package/.eslintrc.js +38 -0
- package/.vscode/settings.json +39 -0
- package/bundler/buffer.js +2370 -0
- package/bundler/bundleEntry.ts +32 -0
- package/bundler/bundleEntryCaller.ts +8 -0
- package/bundler/bundleRequire.ts +244 -0
- package/bundler/bundleWrapper.ts +115 -0
- package/bundler/bundler.ts +72 -0
- package/bundler/flattenSourceMaps.ts +0 -0
- package/bundler/sourceMaps.ts +261 -0
- package/misc/environment.ts +11 -0
- package/misc/types.ts +3 -0
- package/misc/zip.ts +37 -0
- package/package.json +24 -0
- package/spec.txt +33 -0
- package/storage/CachedStorage.ts +32 -0
- package/storage/DelayedStorage.ts +30 -0
- package/storage/DiskCollection.ts +272 -0
- package/storage/FileFolderAPI.tsx +427 -0
- package/storage/IStorage.ts +40 -0
- package/storage/IndexedDBFileFolderAPI.ts +170 -0
- package/storage/JSONStorage.ts +35 -0
- package/storage/PendingManager.tsx +63 -0
- package/storage/PendingStorage.ts +47 -0
- package/storage/PrivateFileSystemStorage.ts +192 -0
- package/storage/StorageObservable.ts +122 -0
- package/storage/TransactionStorage.ts +485 -0
- package/storage/fileSystemPointer.ts +81 -0
- package/storage/storage.d.ts +41 -0
- package/tsconfig.json +31 -0
- package/web/DropdownCustom.tsx +150 -0
- package/web/FullscreenModal.tsx +75 -0
- package/web/GenericFormat.tsx +186 -0
- package/web/Input.tsx +350 -0
- package/web/InputLabel.tsx +288 -0
- package/web/InputPicker.tsx +158 -0
- package/web/LocalStorageParam.ts +56 -0
- package/web/SyncedController.ts +405 -0
- package/web/SyncedLoadingIndicator.tsx +37 -0
- package/web/Table.tsx +188 -0
- package/web/URLParam.ts +84 -0
- package/web/asyncObservable.ts +40 -0
- package/web/colors.tsx +14 -0
- package/web/mobxTyped.ts +29 -0
- package/web/modal.tsx +18 -0
- package/web/observer.tsx +35 -0
package/web/Table.tsx
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import preact from "preact";
|
|
2
|
+
import { css } from "typesafecss";
|
|
3
|
+
import { formatValue, JSXFormatter, toSpaceCase } from "./GenericFormat";
|
|
4
|
+
import { observer } from "./observer";
|
|
5
|
+
import { canHaveChildren } from "socket-function/src/types";
|
|
6
|
+
import { showFullscreenModal } from "./FullscreenModal";
|
|
7
|
+
|
|
8
|
+
// Null means the column is removed
|
|
9
|
+
export type ColumnType<T = unknown, Row extends RowType = RowType> = undefined | null | {
|
|
10
|
+
center?: boolean;
|
|
11
|
+
// Defaults to column name
|
|
12
|
+
title?: preact.ComponentChild;
|
|
13
|
+
formatter?: JSXFormatter<T, Row>;
|
|
14
|
+
};
|
|
15
|
+
export type RowType = {
|
|
16
|
+
[columnName: string]: unknown;
|
|
17
|
+
};
|
|
18
|
+
export type ColumnsType = { [columnName: string]: ColumnType };
|
|
19
|
+
export type TableType<RowT extends RowType = RowType> = {
|
|
20
|
+
columns: { [columnName in keyof RowT]?: ColumnType<RowT[columnName], RowT> };
|
|
21
|
+
rows: RowT[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
@observer
|
|
25
|
+
export class Table<RowT extends RowType> extends preact.Component<TableType<RowT> & {
|
|
26
|
+
class?: string;
|
|
27
|
+
cellClass?: string;
|
|
28
|
+
initialLimit?: number;
|
|
29
|
+
|
|
30
|
+
// Line and character limits before we cut off the inner content
|
|
31
|
+
lineLimit?: number;
|
|
32
|
+
characterLimit?: number;
|
|
33
|
+
|
|
34
|
+
excludeEmptyColumns?: boolean;
|
|
35
|
+
}> {
|
|
36
|
+
state = {
|
|
37
|
+
limit: this.props.initialLimit || 100,
|
|
38
|
+
};
|
|
39
|
+
render() {
|
|
40
|
+
let { columns, rows, excludeEmptyColumns } = this.props;
|
|
41
|
+
|
|
42
|
+
let cellClass = " " + String(this.props.cellClass || "") + " ";
|
|
43
|
+
let allRows = rows;
|
|
44
|
+
rows = rows.slice(0, this.state.limit);
|
|
45
|
+
|
|
46
|
+
const lineLimit = this.props.lineLimit ?? 3;
|
|
47
|
+
const characterLimit = this.props.characterLimit ?? 300;
|
|
48
|
+
|
|
49
|
+
if (excludeEmptyColumns) {
|
|
50
|
+
columns = { ...columns };
|
|
51
|
+
for (let column of Object.keys(columns)) {
|
|
52
|
+
if (!rows.some(row => row[column] !== undefined && row[column] !== null)) {
|
|
53
|
+
delete columns[column];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<table className={css.borderCollapse("collapse") + this.props.class}>
|
|
60
|
+
<tr className={css.position("sticky").top(0).hsla(0, 0, 50, 0.95)}>
|
|
61
|
+
<th className={css.whiteSpace("nowrap")}>⧉ {allRows.length}</th>
|
|
62
|
+
{Object.entries(columns).filter(x => x[1] !== null).map(([columnName, column]: [string, ColumnType]) =>
|
|
63
|
+
<th className={css.pad2(8, 4) + cellClass}>{column?.title || toSpaceCase(columnName)}</th>
|
|
64
|
+
)}
|
|
65
|
+
</tr>
|
|
66
|
+
{rows.map((row, index) => (
|
|
67
|
+
<tr
|
|
68
|
+
className={(index % 2 === 1 && css.hsla(0, 0, 100, 0.25) || "")}
|
|
69
|
+
>
|
|
70
|
+
<td className={css.center}>{index + 1}</td>
|
|
71
|
+
{Object.entries(columns).filter(x => x[1] !== null).map(([columnName, column]: [string, ColumnType]) => {
|
|
72
|
+
let value = row[columnName];
|
|
73
|
+
let formatter = column?.formatter || "guess";
|
|
74
|
+
let result = formatValue(value, formatter, { row, columnName });
|
|
75
|
+
let renderedObj = renderTrimmed({
|
|
76
|
+
content: result,
|
|
77
|
+
lineLimit,
|
|
78
|
+
characterLimit,
|
|
79
|
+
});
|
|
80
|
+
let attributes = { ...renderedObj.outerAttributes };
|
|
81
|
+
attributes.class = attributes.class || "";
|
|
82
|
+
attributes.class += " " + css.whiteSpace("pre-wrap").pad2(8, 4);
|
|
83
|
+
if (column?.center) attributes.class += " " + css.verticalAlign("middle").textAlign("center");
|
|
84
|
+
attributes.class += cellClass;
|
|
85
|
+
// If the inner content looks like a VNode, take it's attributes and unwrap it,
|
|
86
|
+
// so it can fill the entire cell.
|
|
87
|
+
let innerContent = renderedObj.innerContent;
|
|
88
|
+
if (
|
|
89
|
+
canHaveChildren(innerContent) && "props" in innerContent
|
|
90
|
+
&& canHaveChildren(innerContent.props)
|
|
91
|
+
&& "children" in innerContent.props
|
|
92
|
+
&& (
|
|
93
|
+
Array.isArray(innerContent.props.children) && innerContent.props.children.length === 1
|
|
94
|
+
|| !Array.isArray(innerContent.props.children)
|
|
95
|
+
)
|
|
96
|
+
// AND, it is a div or span (a tags shouldn't be unwrapped)
|
|
97
|
+
&& (innerContent.type === "div")
|
|
98
|
+
) {
|
|
99
|
+
attributes.class += " " + innerContent.props.class;
|
|
100
|
+
let baseOnClick = attributes.onClick;
|
|
101
|
+
let props = innerContent.props;
|
|
102
|
+
attributes.onClick = (e) => {
|
|
103
|
+
if (baseOnClick) baseOnClick(e);
|
|
104
|
+
(props as any).onClick?.(e);
|
|
105
|
+
};
|
|
106
|
+
for (let key in props) {
|
|
107
|
+
if (key === "class") continue;
|
|
108
|
+
if (key === "onClick") continue;
|
|
109
|
+
(attributes as any)[key] = props[key];
|
|
110
|
+
}
|
|
111
|
+
innerContent = props.children as any;
|
|
112
|
+
}
|
|
113
|
+
return <td {...attributes}>
|
|
114
|
+
{innerContent}
|
|
115
|
+
</td>;
|
|
116
|
+
})}
|
|
117
|
+
</tr>
|
|
118
|
+
))}
|
|
119
|
+
{allRows.length > rows.length && <tr>
|
|
120
|
+
<td
|
|
121
|
+
colSpan={1 + Object.keys(columns).length}
|
|
122
|
+
className={css.pad2(8).textAlign("center")}
|
|
123
|
+
>
|
|
124
|
+
<button onClick={() => this.state.limit += 100}>
|
|
125
|
+
{/* TODO: Load more as soon as they get close to the end.
|
|
126
|
+
- It doesn't really matter, as there is little reason for them to scroll far
|
|
127
|
+
(they should just filter/search instead).
|
|
128
|
+
*/}
|
|
129
|
+
Load more
|
|
130
|
+
</button>
|
|
131
|
+
</td>
|
|
132
|
+
</tr>}
|
|
133
|
+
</table>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function renderTrimmed(config: {
|
|
139
|
+
content: preact.ComponentChild;
|
|
140
|
+
lineLimit: number;
|
|
141
|
+
characterLimit: number;
|
|
142
|
+
}): {
|
|
143
|
+
outerAttributes: preact.JSX.HTMLAttributes<HTMLTableCellElement>;
|
|
144
|
+
innerContent: preact.ComponentChild;
|
|
145
|
+
} {
|
|
146
|
+
let { content, lineLimit, characterLimit } = config;
|
|
147
|
+
if (typeof content !== "string" && typeof content !== "number") {
|
|
148
|
+
return {
|
|
149
|
+
outerAttributes: {},
|
|
150
|
+
innerContent: content as any,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
let trimmed = false;
|
|
154
|
+
let contentStr = String(content);
|
|
155
|
+
if (contentStr.length > characterLimit) {
|
|
156
|
+
contentStr = contentStr.slice(0, characterLimit - 3) + "...";
|
|
157
|
+
trimmed = true;
|
|
158
|
+
}
|
|
159
|
+
let lines = contentStr.split("\n");
|
|
160
|
+
if (lines.length > lineLimit) {
|
|
161
|
+
lines = lines.slice(0, lineLimit);
|
|
162
|
+
lines[lines.length - 1] += "...";
|
|
163
|
+
contentStr = lines.join("\n");
|
|
164
|
+
trimmed = true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!trimmed) {
|
|
168
|
+
return {
|
|
169
|
+
outerAttributes: {},
|
|
170
|
+
innerContent: contentStr,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
outerAttributes: {
|
|
177
|
+
class: css.opacity(0.5, "hover").button,
|
|
178
|
+
onClick: () => {
|
|
179
|
+
showFullscreenModal(
|
|
180
|
+
<div className={css.whiteSpace("pre-wrap")}>
|
|
181
|
+
{content}
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
innerContent: contentStr
|
|
187
|
+
};
|
|
188
|
+
}
|
package/web/URLParam.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { isNode } from "typesafecss";
|
|
2
|
+
import { observable } from "mobx";
|
|
3
|
+
|
|
4
|
+
let allParams: URLParamStr[] = [];
|
|
5
|
+
|
|
6
|
+
let updated: URLParamStr[] = [];
|
|
7
|
+
|
|
8
|
+
export class URLParamStr {
|
|
9
|
+
private state = observable({
|
|
10
|
+
seqNum: 0
|
|
11
|
+
});
|
|
12
|
+
public lastSetValue = "";
|
|
13
|
+
constructor(public readonly urlKey: string) {
|
|
14
|
+
allParams.push(this);
|
|
15
|
+
}
|
|
16
|
+
public forceUpdate() {
|
|
17
|
+
this.state.seqNum++;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public get() {
|
|
21
|
+
this.state.seqNum;
|
|
22
|
+
return new URLSearchParams(window.location.search).get(this.urlKey) || "";
|
|
23
|
+
}
|
|
24
|
+
public set(value: string) {
|
|
25
|
+
if (value === this.get()) return;
|
|
26
|
+
this.lastSetValue = value;
|
|
27
|
+
batchUrlUpdate(() => {
|
|
28
|
+
updated.push(this);
|
|
29
|
+
});
|
|
30
|
+
this.state.seqNum++;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public get value() {
|
|
34
|
+
return this.get();
|
|
35
|
+
}
|
|
36
|
+
public set value(value: string) {
|
|
37
|
+
this.set(value);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let inBatchUpdate = false;
|
|
42
|
+
export function batchUrlUpdate<T>(code: () => T): T {
|
|
43
|
+
if (inBatchUpdate) return code();
|
|
44
|
+
inBatchUpdate = true;
|
|
45
|
+
try {
|
|
46
|
+
return code();
|
|
47
|
+
} finally {
|
|
48
|
+
inBatchUpdate = false;
|
|
49
|
+
|
|
50
|
+
let prevUpdated = updated;
|
|
51
|
+
updated = [];
|
|
52
|
+
let searchParams = new URLSearchParams(window.location.search);
|
|
53
|
+
for (let obj of prevUpdated) {
|
|
54
|
+
searchParams.set(obj.urlKey, obj.lastSetValue);
|
|
55
|
+
}
|
|
56
|
+
let newURL = "?" + searchParams.toString();
|
|
57
|
+
if (window.location.hash) {
|
|
58
|
+
newURL += window.location.hash;
|
|
59
|
+
}
|
|
60
|
+
window.history.pushState({}, "", newURL);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function createLink(params: [URLParamStr, string][]) {
|
|
65
|
+
let searchParams = new URLSearchParams(window.location.search);
|
|
66
|
+
for (let [param, value] of params) {
|
|
67
|
+
searchParams.set(param.urlKey, value);
|
|
68
|
+
}
|
|
69
|
+
let newURL = "?" + searchParams.toString();
|
|
70
|
+
if (window.location.hash) {
|
|
71
|
+
newURL += window.location.hash;
|
|
72
|
+
}
|
|
73
|
+
return newURL;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!isNode()) {
|
|
77
|
+
// Watch for url push states
|
|
78
|
+
window.addEventListener("popstate", () => {
|
|
79
|
+
// Force all to update, in case their param changed
|
|
80
|
+
for (let param of allParams) {
|
|
81
|
+
param.forceUpdate();
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { observable } from "mobx";
|
|
2
|
+
|
|
3
|
+
export function asyncCache<Args, T>(getValue: (args: Args) => Promise<T>): {
|
|
4
|
+
(args: Args): T | undefined;
|
|
5
|
+
} {
|
|
6
|
+
let cache = new Map<string, {
|
|
7
|
+
result: {
|
|
8
|
+
value?: T;
|
|
9
|
+
error?: Error;
|
|
10
|
+
};
|
|
11
|
+
}>();
|
|
12
|
+
return (args: Args) => {
|
|
13
|
+
let key = JSON.stringify(args);
|
|
14
|
+
let result = cache.get(key);
|
|
15
|
+
if (result) {
|
|
16
|
+
let r = result.result;
|
|
17
|
+
if (r.error) throw r.error;
|
|
18
|
+
return r.value;
|
|
19
|
+
}
|
|
20
|
+
result = {
|
|
21
|
+
result: observable({
|
|
22
|
+
value: undefined,
|
|
23
|
+
error: undefined,
|
|
24
|
+
}),
|
|
25
|
+
};
|
|
26
|
+
cache.set(key, result);
|
|
27
|
+
|
|
28
|
+
void (async () => {
|
|
29
|
+
try {
|
|
30
|
+
let value = await getValue(args);
|
|
31
|
+
result.result.value = value;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
result.result.error = error as Error;
|
|
34
|
+
}
|
|
35
|
+
})();
|
|
36
|
+
// Access the observable so we're watching it
|
|
37
|
+
result.result.error;
|
|
38
|
+
return result.result.value;
|
|
39
|
+
};
|
|
40
|
+
}
|
package/web/colors.tsx
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { css } from "typesafecss";
|
|
2
|
+
|
|
3
|
+
export const redButton = css.hsl(0, 75, 50).bord(1, "hsl(0, 75%, 75%)").background("hsl(0, 75%, 75%)", "hover");
|
|
4
|
+
export const yellowButton = css.hsl(50, 90, 50).bord(1, "hsl(40, 75%, 75%)").color("hsl(0, 0%, 16%)!important").background("hsl(40, 75%, 75%)", "hover");
|
|
5
|
+
export const greenButton = css.hsl(110, 65, 45).bord(1, { h: 110, s: 65, l: 75 }).background("hsl(110, 65%, 90%)", "hover");
|
|
6
|
+
|
|
7
|
+
export const errorMessage = css.hsl(0, 75, 50).color("white")
|
|
8
|
+
.padding("4px 6px", "soft")
|
|
9
|
+
.whiteSpace("pre-wrap").display("inline-block", "soft")
|
|
10
|
+
;
|
|
11
|
+
export const warnMessage = css.hsl(50, 75, 50).color("hsl(0, 0%, 7%)", "important", "soft")
|
|
12
|
+
.padding("4px 6px", "soft")
|
|
13
|
+
.whiteSpace("pre-wrap").display("inline-block", "soft")
|
|
14
|
+
;
|
package/web/mobxTyped.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as mobx from "mobx";
|
|
2
|
+
import { batchFunction } from "socket-function/src/batching";
|
|
3
|
+
export function configureMobxNextFrameScheduler() {
|
|
4
|
+
// NOTE: This makes a big difference if we do await calls in a loop which mutates observable state. BUT... we should probably just do those await calls before the loop?
|
|
5
|
+
let batchReactionScheduler = batchFunction({
|
|
6
|
+
delay: 16,
|
|
7
|
+
name: "reactionScheduler",
|
|
8
|
+
}, (callbacks: (() => void)[]) => {
|
|
9
|
+
//console.log(`Triggering ${callbacks.length} reactions`);
|
|
10
|
+
for (let callback of callbacks) {
|
|
11
|
+
callback();
|
|
12
|
+
}
|
|
13
|
+
lastRenderTime = Date.now();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
let lastRenderTime = 0;
|
|
17
|
+
mobx.configure({
|
|
18
|
+
enforceActions: "never",
|
|
19
|
+
reactionScheduler(callback) {
|
|
20
|
+
let now = performance.now();
|
|
21
|
+
if (now - lastRenderTime < 16) {
|
|
22
|
+
void batchReactionScheduler(callback);
|
|
23
|
+
} else {
|
|
24
|
+
callback();
|
|
25
|
+
lastRenderTime = now;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
package/web/modal.tsx
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import preact from "preact";
|
|
2
|
+
|
|
3
|
+
export function showModal(config: {
|
|
4
|
+
contents: preact.ComponentChildren;
|
|
5
|
+
}): {
|
|
6
|
+
close: () => void;
|
|
7
|
+
} {
|
|
8
|
+
let root = document.createElement("div");
|
|
9
|
+
document.body.appendChild(root);
|
|
10
|
+
preact.render(config.contents, root);
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
close() {
|
|
14
|
+
preact.render(undefined, root);
|
|
15
|
+
document.body.removeChild(root);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
package/web/observer.tsx
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as preact from "preact";
|
|
2
|
+
import { observable, Reaction } from "mobx";
|
|
3
|
+
|
|
4
|
+
let globalConstructOrder = 1;
|
|
5
|
+
export function observer<
|
|
6
|
+
T extends {
|
|
7
|
+
new(...args: any[]): {
|
|
8
|
+
render(): preact.ComponentChild;
|
|
9
|
+
forceUpdate(callback?: () => void): void;
|
|
10
|
+
componentWillUnmount?(): void;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
>(
|
|
14
|
+
Constructor: T
|
|
15
|
+
) {
|
|
16
|
+
let name = Constructor.name;
|
|
17
|
+
return class extends Constructor {
|
|
18
|
+
// @ts-ignore
|
|
19
|
+
static get name() { return Constructor.name; }
|
|
20
|
+
reaction = new Reaction(`render.${name}.${globalConstructOrder++}`, () => {
|
|
21
|
+
super.forceUpdate();
|
|
22
|
+
});
|
|
23
|
+
componentWillUnmount() {
|
|
24
|
+
this.reaction.dispose();
|
|
25
|
+
super.componentWillUnmount?.();
|
|
26
|
+
}
|
|
27
|
+
render(...args: any[]) {
|
|
28
|
+
let output: preact.ComponentChild;
|
|
29
|
+
this.reaction.track(() => {
|
|
30
|
+
output = (super.render as any)(...args);
|
|
31
|
+
});
|
|
32
|
+
return output;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|