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/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
+ }
@@ -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
+ ;
@@ -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
+ }
@@ -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
+ }