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,150 @@
1
+ import preact from "preact";
2
+ import { observable } from "mobx";
3
+ import { observer } from "./observer";
4
+ import { css } from "typesafecss";
5
+ import { LengthOrPercentage, LengthOrPercentageOrAuto } from "typesafecss/cssTypes";
6
+
7
+
8
+ @observer
9
+ export class DropdownCustom<T> extends preact.Component<{
10
+ class?: string;
11
+ optionClass?: string;
12
+ title?: string;
13
+ value: T;
14
+ onChange: (value: T, index: number) => void;
15
+ maxWidth?: LengthOrPercentage;
16
+ options: { value: T; label: (isOpen: boolean) => preact.ComponentChild; }[];
17
+ }> {
18
+ synced = observable({
19
+ isOpen: false,
20
+ tempIndexSelected: null as null | number,
21
+ });
22
+ onUnmount: (() => void)[] = [];
23
+ componentDidMount(): void {
24
+ const handler = (e: MouseEvent) => {
25
+ if (!this.synced.isOpen) return;
26
+ let el = e.target as HTMLElement | null;
27
+ while (el) {
28
+ if (el === this.base) return;
29
+ el = el.parentElement;
30
+ }
31
+ this.synced.isOpen = false;
32
+ };
33
+ window.addEventListener("click", handler);
34
+ this.onUnmount.push(() => window.removeEventListener("click", handler));
35
+ }
36
+ componentDidUnmount(): void {
37
+ for (let f of this.onUnmount) f();
38
+ }
39
+ render() {
40
+ const { options, value, title, onChange } = this.props;
41
+ let selectedIndex = options.findIndex(o => o.value === value);
42
+ if (selectedIndex === -1) selectedIndex = 0;
43
+ let selectedItem = options[selectedIndex] || { value: undefined, label: () => { } };
44
+
45
+ let renderOptions = () => {
46
+ let selIndex = this.synced.tempIndexSelected ?? selectedIndex;
47
+ return (
48
+ <div className={css.absolute.width(this.props.maxWidth || "50vw")}>
49
+ <button
50
+ className={css.opacity(0).visibility("hidden").absolute}
51
+ data-hotkey={"Enter"}
52
+ onClick={() => {
53
+ if (this.synced.tempIndexSelected !== null) {
54
+ this.props.onChange(options[this.synced.tempIndexSelected].value, this.synced.tempIndexSelected);
55
+ }
56
+ this.synced.isOpen = false;
57
+ this.synced.tempIndexSelected = null;
58
+ }}
59
+ />
60
+ <button
61
+ className={css.opacity(0).visibility("hidden").absolute}
62
+ data-hotkey={"ArrowUp"}
63
+ onClick={(e) => {
64
+ e.stopPropagation();
65
+ this.synced.tempIndexSelected = (this.synced.tempIndexSelected ?? selectedIndex) - 1;
66
+ if (this.synced.tempIndexSelected < 0) this.synced.tempIndexSelected = options.length - 1;
67
+ }}
68
+ />
69
+ <button
70
+ className={css.opacity(0).visibility("hidden").absolute}
71
+ data-hotkey={"ArrowDown"}
72
+ onClick={(e) => {
73
+ e.stopPropagation();
74
+ this.synced.tempIndexSelected = (this.synced.tempIndexSelected ?? selectedIndex) + 1;
75
+ if (this.synced.tempIndexSelected >= options.length) this.synced.tempIndexSelected = 0;
76
+ }}
77
+ />
78
+ <div
79
+ className={
80
+ css.pad(2).margin(2)
81
+ .absolute.pos(0, 0).zIndex(1)
82
+ .hsl(0, 0, 25)
83
+ .vbox(2).alignItems("stretch")
84
+ .overflow("auto")
85
+ .maxHeight("50vh")
86
+ .border("1px solid hsl(0, 0%, 10%)")
87
+ + this.props.optionClass
88
+ }
89
+ >
90
+ {this.props.options.map(({ value, label }, index, list) =>
91
+ <>
92
+ {index !== 0 &&
93
+ <div className={css.height(1).hsl(0, 0, 20)} />
94
+ }
95
+ <div
96
+ onClick={() => {
97
+ this.props.onChange(value, index);
98
+ this.synced.isOpen = false;
99
+ this.synced.tempIndexSelected = null;
100
+ }}
101
+ className={
102
+ " "
103
+ + (
104
+ index === selIndex && css.hsl(0, 0, 40)
105
+ || index % 2 === 0 && css.hsl(0, 0, 25)
106
+ || index % 2 === 1 && css.hsl(0, 0, 20)
107
+ || ""
108
+ )
109
+ + css.button.pad(2, 6)
110
+ + css.background("hsl(0, 0%, 60%)", "hover")
111
+ + this.props.optionClass
112
+ }
113
+ >
114
+ {label(true)}
115
+ </div>
116
+ </>
117
+ )}
118
+ </div>
119
+ </div >
120
+ );
121
+ };
122
+
123
+ return (
124
+ <div
125
+ className={(this.synced.isOpen && css.zIndex(1)) + (this.props.class || "")}
126
+ >
127
+ {this.props.title && (
128
+ <div
129
+ style={{
130
+ fontWeight: "bold",
131
+ }}
132
+ onClick={() => this.synced.isOpen = !this.synced.isOpen}
133
+ >
134
+ {this.props.title}
135
+ </div>
136
+ )}
137
+ <div className={css.relative.vbox0.maxWidth(this.props.maxWidth)}>
138
+ <div
139
+ className={css.hbox(10).hsl(0, 0, 25).pad(4, 10).button + this.props.optionClass}
140
+ onClick={() => this.synced.isOpen = !this.synced.isOpen}
141
+ >
142
+ {selectedItem?.label(false)}
143
+ <button>Expand</button>
144
+ </div>
145
+ {this.synced.isOpen && renderOptions()}
146
+ </div>
147
+ </div>
148
+ );
149
+ }
150
+ }
@@ -0,0 +1,75 @@
1
+ import preact from "preact";
2
+ import { showModal } from "./modal";
3
+ import { observable } from "mobx";
4
+ import { observer } from "./observer";
5
+
6
+ export function showFullscreenModal(contents: preact.ComponentChildren) {
7
+ let { close } = showModal({
8
+ contents: <FullscreenModal onCancel={() => close()}>
9
+ {contents}
10
+ </FullscreenModal>
11
+ });
12
+ }
13
+
14
+ @observer
15
+ export class FullscreenModal extends preact.Component<{
16
+ parentState?: { open: boolean };
17
+ onCancel?: () => void;
18
+ style?: preact.JSX.CSSProperties;
19
+ outerStyle?: preact.JSX.CSSProperties;
20
+ }> {
21
+ render() {
22
+ let { parentState } = this.props;
23
+ return (
24
+ <div>
25
+ <div style={{ display: "none" }}>
26
+ <button data-hotkey={"Escape"} onClick={() => {
27
+ if (parentState) parentState.open = false;
28
+ this.props.onCancel?.();
29
+ }}>Close</button>
30
+ </div>
31
+ <div
32
+ style={{
33
+ position: "fixed",
34
+ top: 0,
35
+ left: 0,
36
+ width: "100vw",
37
+ height: "100vh",
38
+ background: "hsla(0, 0%, 30%, 0.5)",
39
+ padding: 100,
40
+ display: "flex",
41
+ alignItems: "center",
42
+ justifyContent: "center",
43
+ overflow: "auto",
44
+ cursor: "pointer",
45
+ ...this.props.outerStyle,
46
+ }}
47
+ onClick={e => {
48
+ if (e.currentTarget === e.target) {
49
+ if (parentState) parentState.open = false;
50
+ this.props.onCancel?.();
51
+ }
52
+ }}
53
+ >
54
+ <div
55
+ style={{
56
+ background: "hsl(0, 0%, 100%)",
57
+ padding: 20,
58
+ color: "hsl(0, 0%, 7%)",
59
+ cursor: "default",
60
+ width: "100%",
61
+ display: "flex",
62
+ flexDirection: "column",
63
+ gap: 10,
64
+ maxHeight: "calc(100% - 200px)",
65
+ overflow: "auto",
66
+ ...this.props.style
67
+ }}
68
+ >
69
+ {this.props.children}
70
+ </div>
71
+ </div>
72
+ </div>
73
+ );
74
+ }
75
+ }
@@ -0,0 +1,186 @@
1
+ import preact, { ContextType } from "preact";
2
+ import { formatNumber, formatTime } from "socket-function/src/formatting/format";
3
+ import { canHaveChildren } from "socket-function/src/types";
4
+ import { css } from "typesafecss";
5
+
6
+ export const errorMessage = css.hsl(0, 75, 50).color("white", "important", "soft")
7
+ .padding("4px 6px", "soft")
8
+ .whiteSpace("pre-wrap").display("inline-block", "soft")
9
+ ;
10
+ export const warnMessage = css.hsl(50, 75, 50).color("hsl(0, 0%, 7%)", "important", "soft")
11
+ .padding("4px 6px", "soft")
12
+ .whiteSpace("pre-wrap").display("inline-block", "soft")
13
+ ;
14
+
15
+ export type RowType = { [columnName: string]: unknown };
16
+ export type FormatContext<RowT extends RowType = RowType> = {
17
+ row?: RowT;
18
+ columnName?: RowT extends undefined ? string : keyof RowT;
19
+ };
20
+ export type JSXFormatter<T = unknown, RowT extends RowType = RowType> = (
21
+ StringFormatters
22
+ | `varray:${StringFormatters}`
23
+ | `link:${string}`
24
+ | ((value: T, context?: FormatContext<RowT>) => preact.ComponentChild)
25
+ );
26
+
27
+ type StringFormatters = (
28
+ "guess"
29
+ | "string" | "number"
30
+ | "timeSpan" | "date"
31
+ | "error" | "link"
32
+ | "toSpaceCase"
33
+ );
34
+
35
+ function d(value: unknown, formattedValue: preact.ComponentChild) {
36
+ if (value === undefined || value === null) {
37
+ return "";
38
+ }
39
+ return formattedValue;
40
+ }
41
+
42
+ export function toSpaceCase(text: string) {
43
+ return text
44
+ // "camelCase" => "camel Case"
45
+ // "URL" => "URL"
46
+ .replace(/([a-z][A-Z])/g, str => str[0] + " " + str[1])
47
+ // "snake_case" => "snake case"
48
+ .replace(/_([A-Za-z])/g, " $1")
49
+ // "firstletter" => "Firstletter"
50
+ .replace(/^./, str => str.toUpperCase())
51
+ // "first letter" => "first Letter"
52
+ .replace(/ ./, str => str.toUpperCase())
53
+ // Convert multiple spaces to a single space
54
+ .replace(/\s+/g, " ");
55
+ ;
56
+ }
57
+
58
+ /** YYYY/MM/DD HH:MM:SS PM/AM */
59
+ function formatDateTime(time: number) {
60
+ function p(s: number) {
61
+ return s.toString().padStart(2, "0");
62
+ }
63
+ let date = new Date(time);
64
+ let hours = date.getHours();
65
+ let minutes = date.getMinutes();
66
+ let seconds = date.getSeconds();
67
+ let ampm = hours >= 12 ? "PM" : "AM";
68
+ hours = hours % 12;
69
+ hours = hours ? hours : 12; // the hour '0' should be '12'
70
+ let strTime = p(hours) + ":" + p(minutes) + ":" + p(seconds) + " " + ampm;
71
+ return date.getFullYear() + "/" + p(date.getMonth() + 1) + "/" + p(date.getDate()) + " " + strTime;
72
+ }
73
+
74
+ const startGuessDateRange = +new Date(2010, 0, 1).getTime();
75
+ const endGuessDateRange = +new Date(2040, 0, 1).getTime();
76
+ let formatters: { [formatter in StringFormatters]: (value: unknown) => preact.ComponentChild } = {
77
+ string: (value) => d(value, String(value || "")),
78
+ number: (value) => d(value, formatNumber(Number(value))),
79
+ timeSpan: (value) => d(value, formatTime(Number(value))),
80
+ date: (value) => d(value, formatDateTime(Number(value))),
81
+ error: (value) => d(value, <span className={errorMessage}>{String(value)}</span>),
82
+ toSpaceCase: (value) => d(value, toSpaceCase(String(value))),
83
+ link: (value) => {
84
+ if (value === undefined || value === null) {
85
+ return "";
86
+ }
87
+ let href = String(value);
88
+ let str = String(value);
89
+ // https://google.com<google> => href = https://google.com, str = google
90
+ let match = str.match(/<([^>]+)>/);
91
+ if (match) {
92
+ href = str.slice(0, match.index);
93
+ str = match[1];
94
+ }
95
+ return <a target="_blank" href={href}>{str}</a>;
96
+ },
97
+ guess: (value) => {
98
+ if (typeof value === "number") {
99
+ // NOTE: These special values don't represent real values, and if they are shown
100
+ // to the user, it is a mistake anyways. So instead of showing a large number
101
+ // that is not meaningful to the user, we show a string, so they the issue
102
+ // is not the system having a large number, but the system not changing
103
+ // the default value.
104
+ if (value === Number.MAX_SAFE_INTEGER) {
105
+ return "Number.MAX_SAFE_INTEGER";
106
+ }
107
+ if (value === Number.MIN_SAFE_INTEGER) {
108
+ return "Number.MIN_SAFE_INTEGER";
109
+ }
110
+ if (value === Number.MAX_VALUE) {
111
+ return "Number.MAX_VALUE";
112
+ }
113
+ if (value === Number.MIN_VALUE) {
114
+ return "Number.MIN_VALUE";
115
+ }
116
+ // Infinity should be somewhat understood by the user, if they are even a little
117
+ // bit literate. Of course, the value is likely a bug, but at least the consequences
118
+ // may be inferrable (the threshold is +Infinity, so it will never be reached, etc).
119
+ if (value === Number.POSITIVE_INFINITY) {
120
+ return "+Infinity";
121
+ }
122
+ if (value === Number.NEGATIVE_INFINITY) {
123
+ return "-Infinity";
124
+ }
125
+ // These are somewhat a mistake, and should almost always be displayed as ""
126
+ if (Number.isNaN(value)) {
127
+ return "";
128
+ }
129
+
130
+ if (startGuessDateRange < value && value < endGuessDateRange) {
131
+ return formatters.date(value);
132
+ }
133
+ return formatters.number(value);
134
+ }
135
+ if (typeof value === "string" && value.startsWith("Error:")) {
136
+ return formatters.error(value.slice("Error:".length));
137
+ }
138
+ if (typeof value === "string" && value.startsWith("https://")) {
139
+ return formatters.link(value);
140
+ }
141
+ // Don't render nested objects, etc, otherwise passing large arrays
142
+ // could just in us blowing the output up, when their intention
143
+ // is to just show "Array()"
144
+ if (Array.isArray(value) && !value.some(x => canHaveChildren(x))) {
145
+ return formatValue(value, "varray:guess");
146
+ }
147
+ return formatters.string(value);
148
+ },
149
+ };
150
+ function formatVArray(value: unknown[], formatter: StringFormatters) {
151
+ if (!Array.isArray(value)) {
152
+ return <span className={errorMessage}>Expected array, got {typeof value}</span>;
153
+ }
154
+ let values = value.map(v => {
155
+ let formatted = formatValue(v, formatter);
156
+ if (!canHaveChildren(formatted)) {
157
+ return <span>{formatted}</span>;
158
+ }
159
+ return formatted;
160
+ });
161
+ return (
162
+ <div className={css.vbox(4)}>
163
+ {values}
164
+ </div>
165
+ );
166
+ }
167
+
168
+ export function formatValue(value: unknown, formatter: JSXFormatter = "guess", context?: FormatContext) {
169
+ if (typeof formatter === "function") {
170
+ return formatter(value, context);
171
+ }
172
+ let formatterT = formatter as StringFormatters;
173
+ if (formatterT.startsWith("varray:")) {
174
+ formatterT = formatterT.slice("varray:".length) as StringFormatters;
175
+ return formatVArray(value as unknown[], formatterT);
176
+ }
177
+ if (formatterT.startsWith("link:")) {
178
+ let href = formatterT.slice("link:".length);
179
+ href = href.replaceAll("$VALUE$", String(value));
180
+ return formatters.link(href);
181
+ }
182
+ if (!formatters[formatterT]) {
183
+ throw new Error(`Unknown formatter: ${formatter}`);
184
+ }
185
+ return formatters[formatterT](value);
186
+ }