querysub 0.2.0
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/.dependency-cruiser.js +304 -0
- package/.eslintrc.js +51 -0
- package/.github/copilot-instructions.md +1 -0
- package/.vscode/settings.json +25 -0
- package/bin/deploy.js +4 -0
- package/bin/function.js +4 -0
- package/bin/server.js +4 -0
- package/costsBenefits.txt +112 -0
- package/deploy.ts +3 -0
- package/inject.ts +1 -0
- package/package.json +60 -0
- package/prompts.txt +54 -0
- package/spec.txt +820 -0
- package/src/-a-archives/archiveCache.ts +913 -0
- package/src/-a-archives/archives.ts +148 -0
- package/src/-a-archives/archivesBackBlaze.ts +792 -0
- package/src/-a-archives/archivesDisk.ts +418 -0
- package/src/-a-archives/copyLocalToBackblaze.ts +24 -0
- package/src/-a-auth/certs.ts +517 -0
- package/src/-a-auth/der.ts +122 -0
- package/src/-a-auth/ed25519.ts +1015 -0
- package/src/-a-auth/node-forge-ed25519.d.ts +17 -0
- package/src/-b-authorities/dnsAuthority.ts +203 -0
- package/src/-b-authorities/emailAuthority.ts +57 -0
- package/src/-c-identity/IdentityController.ts +200 -0
- package/src/-d-trust/NetworkTrust2.ts +150 -0
- package/src/-e-certs/EdgeCertController.ts +288 -0
- package/src/-e-certs/certAuthority.ts +192 -0
- package/src/-f-node-discovery/NodeDiscovery.ts +543 -0
- package/src/-g-core-values/NodeCapabilities.ts +134 -0
- package/src/-g-core-values/oneTimeForward.ts +91 -0
- package/src/-h-path-value-serialize/PathValueSerializer.ts +769 -0
- package/src/-h-path-value-serialize/stringSerializer.ts +176 -0
- package/src/0-path-value-core/LoggingClient.tsx +24 -0
- package/src/0-path-value-core/NodePathAuthorities.ts +978 -0
- package/src/0-path-value-core/PathController.ts +1 -0
- package/src/0-path-value-core/PathValueCommitter.ts +565 -0
- package/src/0-path-value-core/PathValueController.ts +231 -0
- package/src/0-path-value-core/archiveLocks/ArchiveLocks.ts +154 -0
- package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +820 -0
- package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +180 -0
- package/src/0-path-value-core/debugLogs.ts +90 -0
- package/src/0-path-value-core/pathValueArchives.ts +483 -0
- package/src/0-path-value-core/pathValueCore.ts +2217 -0
- package/src/1-path-client/RemoteWatcher.ts +558 -0
- package/src/1-path-client/pathValueClientWatcher.ts +702 -0
- package/src/2-proxy/PathValueProxyWatcher.ts +1857 -0
- package/src/2-proxy/archiveMoveHarness.ts +376 -0
- package/src/2-proxy/garbageCollection.ts +753 -0
- package/src/2-proxy/pathDatabaseProxyBase.ts +37 -0
- package/src/2-proxy/pathValueProxy.ts +139 -0
- package/src/2-proxy/schema2.ts +518 -0
- package/src/3-path-functions/PathFunctionHelpers.ts +129 -0
- package/src/3-path-functions/PathFunctionRunner.ts +619 -0
- package/src/3-path-functions/PathFunctionRunnerMain.ts +67 -0
- package/src/3-path-functions/deployBlock.ts +10 -0
- package/src/3-path-functions/deployCheck.ts +7 -0
- package/src/3-path-functions/deployMain.ts +160 -0
- package/src/3-path-functions/pathFunctionLoader.ts +282 -0
- package/src/3-path-functions/syncSchema.ts +475 -0
- package/src/3-path-functions/tests/functionsTest.ts +135 -0
- package/src/3-path-functions/tests/rejectTest.ts +77 -0
- package/src/4-dom/css.tsx +29 -0
- package/src/4-dom/cssTypes.d.ts +212 -0
- package/src/4-dom/qreact.tsx +2322 -0
- package/src/4-dom/qreactTest.tsx +417 -0
- package/src/4-querysub/Querysub.ts +877 -0
- package/src/4-querysub/QuerysubController.ts +620 -0
- package/src/4-querysub/copyEvent.ts +0 -0
- package/src/4-querysub/permissions.ts +289 -0
- package/src/4-querysub/permissionsShared.ts +1 -0
- package/src/4-querysub/querysubPrediction.ts +525 -0
- package/src/5-diagnostics/FullscreenModal.tsx +67 -0
- package/src/5-diagnostics/GenericFormat.tsx +165 -0
- package/src/5-diagnostics/Modal.tsx +79 -0
- package/src/5-diagnostics/Table.tsx +183 -0
- package/src/5-diagnostics/TimeGrouper.tsx +114 -0
- package/src/5-diagnostics/diskValueAudit.ts +216 -0
- package/src/5-diagnostics/memoryValueAudit.ts +442 -0
- package/src/5-diagnostics/nodeMetadata.ts +135 -0
- package/src/5-diagnostics/qreactDebug.tsx +309 -0
- package/src/5-diagnostics/shared.ts +26 -0
- package/src/5-diagnostics/synchronousLagTracking.ts +47 -0
- package/src/TestController.ts +35 -0
- package/src/allowclient.flag +0 -0
- package/src/bits.ts +86 -0
- package/src/buffers.ts +69 -0
- package/src/config.ts +53 -0
- package/src/config2.ts +48 -0
- package/src/diagnostics/ActionsHistory.ts +56 -0
- package/src/diagnostics/NodeViewer.tsx +503 -0
- package/src/diagnostics/SizeLimiter.ts +62 -0
- package/src/diagnostics/TimeDebug.tsx +18 -0
- package/src/diagnostics/benchmark.ts +139 -0
- package/src/diagnostics/errorLogs/ErrorLogController.ts +515 -0
- package/src/diagnostics/errorLogs/ErrorLogCore.ts +274 -0
- package/src/diagnostics/errorLogs/LogClassifiers.tsx +302 -0
- package/src/diagnostics/errorLogs/LogFilterUI.tsx +84 -0
- package/src/diagnostics/errorLogs/LogNotify.tsx +101 -0
- package/src/diagnostics/errorLogs/LogTimeSelector.tsx +724 -0
- package/src/diagnostics/errorLogs/LogViewer.tsx +757 -0
- package/src/diagnostics/errorLogs/hookErrors.ts +60 -0
- package/src/diagnostics/errorLogs/logFiltering.tsx +149 -0
- package/src/diagnostics/heapTag.ts +13 -0
- package/src/diagnostics/listenOnDebugger.ts +77 -0
- package/src/diagnostics/logs/DiskLoggerPage.tsx +572 -0
- package/src/diagnostics/logs/ObjectDisplay.tsx +165 -0
- package/src/diagnostics/logs/ansiFormat.ts +108 -0
- package/src/diagnostics/logs/diskLogGlobalContext.ts +38 -0
- package/src/diagnostics/logs/diskLogger.ts +305 -0
- package/src/diagnostics/logs/diskShimConsoleLogs.ts +32 -0
- package/src/diagnostics/logs/injectFileLocationToConsole.ts +50 -0
- package/src/diagnostics/logs/logGitHashes.ts +30 -0
- package/src/diagnostics/managementPages.tsx +289 -0
- package/src/diagnostics/periodic.ts +89 -0
- package/src/diagnostics/runSaturationTest.ts +416 -0
- package/src/diagnostics/satSchema.ts +64 -0
- package/src/diagnostics/trackResources.ts +82 -0
- package/src/diagnostics/watchdog.ts +55 -0
- package/src/errors.ts +132 -0
- package/src/forceProduction.ts +3 -0
- package/src/fs.ts +72 -0
- package/src/heapDumps.ts +666 -0
- package/src/https.ts +2 -0
- package/src/inject.ts +1 -0
- package/src/library-components/ATag.tsx +84 -0
- package/src/library-components/Button.tsx +344 -0
- package/src/library-components/ButtonSelector.tsx +64 -0
- package/src/library-components/DropdownCustom.tsx +151 -0
- package/src/library-components/DropdownSelector.tsx +32 -0
- package/src/library-components/Input.tsx +334 -0
- package/src/library-components/InputLabel.tsx +198 -0
- package/src/library-components/InputPicker.tsx +125 -0
- package/src/library-components/LazyComponent.tsx +62 -0
- package/src/library-components/MeasureHeightCSS.tsx +48 -0
- package/src/library-components/MeasuredDiv.tsx +47 -0
- package/src/library-components/ShowMore.tsx +51 -0
- package/src/library-components/SyncedController.ts +171 -0
- package/src/library-components/TimeRangeSelector.tsx +407 -0
- package/src/library-components/URLParam.ts +263 -0
- package/src/library-components/colors.tsx +14 -0
- package/src/library-components/drag.ts +114 -0
- package/src/library-components/icons.tsx +692 -0
- package/src/library-components/niceStringify.ts +50 -0
- package/src/library-components/renderToString.ts +52 -0
- package/src/misc/PromiseRace.ts +101 -0
- package/src/misc/color.ts +30 -0
- package/src/misc/getParentProcessId.cs +53 -0
- package/src/misc/getParentProcessId.ts +53 -0
- package/src/misc/hash.ts +83 -0
- package/src/misc/ipPong.js +13 -0
- package/src/misc/networking.ts +2 -0
- package/src/misc/random.ts +45 -0
- package/src/misc.ts +19 -0
- package/src/noserverhotreload.flag +0 -0
- package/src/path.ts +226 -0
- package/src/persistentLocalStore.ts +37 -0
- package/src/promise.ts +15 -0
- package/src/server.ts +73 -0
- package/src/src.d.ts +1 -0
- package/src/test/heapProcess.ts +36 -0
- package/src/test/mongoSatTest.tsx +55 -0
- package/src/test/satTest.ts +193 -0
- package/src/test/test.tsx +552 -0
- package/src/zip.ts +92 -0
- package/src/zipThreaded.ts +106 -0
- package/src/zipThreadedWorker.js +19 -0
- package/tsconfig.json +27 -0
- package/yarnSpec.txt +56 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import preact from "preact";
|
|
2
|
+
import { css } from "typesafecss";
|
|
3
|
+
import { URLParam, parseSearchString, encodeSearchString } from "./URLParam";
|
|
4
|
+
import { qreact } from "../4-dom/qreact";
|
|
5
|
+
|
|
6
|
+
export type URLOverride<T = unknown> = {
|
|
7
|
+
param: URLParam<T>;
|
|
8
|
+
value: T;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type ATagProps = (
|
|
12
|
+
Omit<preact.JSX.HTMLAttributes<HTMLAnchorElement>, "href">
|
|
13
|
+
& {
|
|
14
|
+
values?: URLOverride[];
|
|
15
|
+
// TODO:
|
|
16
|
+
/** For example: ["ArrowLeft", "ctrl+z", "shift+toggle"] */
|
|
17
|
+
hotkeys?: string[];
|
|
18
|
+
|
|
19
|
+
/** Instead of turning into a div when there are no values[], stays as a link. */
|
|
20
|
+
alwaysLink?: boolean;
|
|
21
|
+
|
|
22
|
+
/** Only act as a link, not using single page application value setting. This means
|
|
23
|
+
* it results in a page load on click.
|
|
24
|
+
*/
|
|
25
|
+
rawLink?: boolean;
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
export class ATag extends qreact.Component<ATagProps> {
|
|
30
|
+
render() {
|
|
31
|
+
const { values, children, alwaysLink, ...props } = this.props;
|
|
32
|
+
if (values?.length === 0 && !alwaysLink) {
|
|
33
|
+
return <div {...props as any}>{children}</div>;
|
|
34
|
+
}
|
|
35
|
+
let isCurrent = values?.every(value => value.param.value === value.value);
|
|
36
|
+
return (
|
|
37
|
+
<a
|
|
38
|
+
tabIndex={0}
|
|
39
|
+
{...props}
|
|
40
|
+
className={
|
|
41
|
+
(isCurrent ? css.color("hsl(110, 75%, 80%)") : css.color("hsl(210, 100%, 80%)"))
|
|
42
|
+
+ css.textDecoration("none")
|
|
43
|
+
.textDecoration("underline", "hover").outline("3px solid hsl(204, 100%, 50%)", "focus")
|
|
44
|
+
+ (props.className ?? props.class)
|
|
45
|
+
|
|
46
|
+
}
|
|
47
|
+
onClick={e => {
|
|
48
|
+
if (this.props.rawLink) return;
|
|
49
|
+
if (e.button !== 0) return;
|
|
50
|
+
if (this.props.target !== "_blank") {
|
|
51
|
+
e.preventDefault();
|
|
52
|
+
for (let value of values || []) {
|
|
53
|
+
value.param.value = value.value;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
props.onClick?.(e);
|
|
57
|
+
}}
|
|
58
|
+
onMouseOver={e => {
|
|
59
|
+
// NOTE: Calculate the href dynamically, to prevent pages with a lot of links that
|
|
60
|
+
// reference frequently changing values from resulting in a huge number of DOM changes
|
|
61
|
+
// every time a value changes.
|
|
62
|
+
let urlObj = new URL(document.location.href);
|
|
63
|
+
let params = parseSearchString(urlObj.search);
|
|
64
|
+
for (let value of values || []) {
|
|
65
|
+
params[value.param.urlKey] = value.value;
|
|
66
|
+
}
|
|
67
|
+
urlObj.search = encodeSearchString(params);
|
|
68
|
+
e.currentTarget.href = urlObj.toString();
|
|
69
|
+
props.onMouseOver?.(e);
|
|
70
|
+
}}
|
|
71
|
+
onKeyDown={e => {
|
|
72
|
+
if (e.key === "Enter") {
|
|
73
|
+
e.currentTarget.click();
|
|
74
|
+
}
|
|
75
|
+
props.onKeyDown?.(e);
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
{children}
|
|
79
|
+
</a>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export const Anchor = ATag;
|
|
84
|
+
export const Link = ATag;
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import preact from "preact";
|
|
2
|
+
import { isNode, list, nextId } from "socket-function/src/misc";
|
|
3
|
+
import { css } from "typesafecss";
|
|
4
|
+
import { Querysub } from "../4-querysub/QuerysubController";
|
|
5
|
+
import { qreact } from "../4-dom/qreact";
|
|
6
|
+
|
|
7
|
+
export type ButtonProps = (
|
|
8
|
+
preact.JSX.HTMLAttributes<HTMLButtonElement>
|
|
9
|
+
& {
|
|
10
|
+
flavor?: "large" | "small" | "tiny" | "noui";
|
|
11
|
+
square?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
For example: ["ArrowLeft", "ctrl+z", "shift+toggle", "global+shift+enter"]
|
|
14
|
+
"toggle" will trigger the button twice, once on press and once on release.
|
|
15
|
+
Order matters, and the correct order is "global+ctrl+shift+alt+X"
|
|
16
|
+
*/
|
|
17
|
+
hotkeys?: string[];
|
|
18
|
+
/**
|
|
19
|
+
* Allows overriding hotkeys with ones of higher priority.
|
|
20
|
+
* All hotkeys with the max priority are pressed at once, other keys are NOT pressed.
|
|
21
|
+
* - Useful for things like popups, which shouldn't press hotkeys in the main window
|
|
22
|
+
* */
|
|
23
|
+
hotkeyPriority?: number;
|
|
24
|
+
showHotkeys?: boolean | "vertical" | "reverse";
|
|
25
|
+
hideHotkeys?: boolean;
|
|
26
|
+
/** Immediately repeats, every animation frame */
|
|
27
|
+
immediateRepeat?: boolean;
|
|
28
|
+
|
|
29
|
+
/** By default the last hotkey handler (which is probably the latest) is called. If this is
|
|
30
|
+
* true we also call this hotkey handler, no matter what.
|
|
31
|
+
*/
|
|
32
|
+
callEvenIfRedundant?: boolean;
|
|
33
|
+
|
|
34
|
+
/** Uses userSelect: "none", instead of preventDefault. Using userSelect: "none"
|
|
35
|
+
* has numerous drawbacks, such as not preventing them from selecting other elements
|
|
36
|
+
* if they click on the button and drag. It is better to use preventDefault, that way
|
|
37
|
+
* we get the same effect (they don't accidentally select text when clicking), but
|
|
38
|
+
* with other elements, and they can still select the button text if they want to.
|
|
39
|
+
* - This is required if you need mousedown to trigger the default browser behavior.
|
|
40
|
+
*/
|
|
41
|
+
useLegacySelection?: boolean;
|
|
42
|
+
|
|
43
|
+
/** Causes the output component to be a type other than "button" */
|
|
44
|
+
typeOverride?: string;
|
|
45
|
+
|
|
46
|
+
/** Default right */
|
|
47
|
+
hotkeyPosition?: "left" | "right" | "above";
|
|
48
|
+
|
|
49
|
+
hue?: number;
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
/** Allows setting the default of some fields (just hotkeyPriority for now), of all Buttons under this. */
|
|
54
|
+
export class ButtonHotkeyRegion extends qreact.Component<{ hotkeyPriority?: number }> {
|
|
55
|
+
render() {
|
|
56
|
+
return this.props.children;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
type Watcher = {
|
|
61
|
+
props: {
|
|
62
|
+
hotkeys?: string[];
|
|
63
|
+
hotkeyPriority?: number;
|
|
64
|
+
immediateRepeat?: boolean;
|
|
65
|
+
callEvenIfRedundant?: boolean;
|
|
66
|
+
};
|
|
67
|
+
element: HTMLElement | null;
|
|
68
|
+
};
|
|
69
|
+
let registeredWatches = new Set<Watcher>();
|
|
70
|
+
|
|
71
|
+
if (!isNode()) {
|
|
72
|
+
let insideAnims = new Set<Watcher>();
|
|
73
|
+
let keyUpListener = new Set<() => void>();
|
|
74
|
+
document.addEventListener("keyup", e => {
|
|
75
|
+
for (let listener of keyUpListener) {
|
|
76
|
+
listener();
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
document.addEventListener("keydown", e => {
|
|
80
|
+
let isAmbientEvent = (
|
|
81
|
+
e.target === document.body
|
|
82
|
+
// Some elements are commonly selected, but shouldn't handling key events
|
|
83
|
+
|| (e.target as HTMLElement).tagName === "BUTTON"
|
|
84
|
+
|| (e.target as HTMLElement).tagName === "A"
|
|
85
|
+
|| (e.target as HTMLElement).tagName === "INPUT" && (e.target as HTMLInputElement).type === "checkbox"
|
|
86
|
+
//|| (e.target as HTMLElement).tagName === "INPUT" && (e.target as HTMLInputElement).type === "number" && Number.isNaN(+e.key) && !e.key.includes("Arrow")
|
|
87
|
+
//|| ["INPUT", "TEXTAREA"].includes((e.target as HTMLElement).tagName)
|
|
88
|
+
);
|
|
89
|
+
if (e.key === "Escape") {
|
|
90
|
+
(document.activeElement as any)?.blur();
|
|
91
|
+
}
|
|
92
|
+
let keyFull = e.code;
|
|
93
|
+
if (keyFull.startsWith("Key")) {
|
|
94
|
+
keyFull = keyFull.slice("Key".length);
|
|
95
|
+
}
|
|
96
|
+
if (e.metaKey) keyFull = "meta+" + keyFull;
|
|
97
|
+
if (e.altKey) keyFull = "alt+" + keyFull;
|
|
98
|
+
if (e.shiftKey) keyFull = "shift+" + keyFull;
|
|
99
|
+
if (e.ctrlKey || e.metaKey) keyFull = "ctrl+" + keyFull;
|
|
100
|
+
if (!isAmbientEvent) {
|
|
101
|
+
trigger("global+" + keyFull);
|
|
102
|
+
} else {
|
|
103
|
+
trigger("global+" + keyFull);
|
|
104
|
+
trigger(keyFull);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function trigger(keyFull: string) {
|
|
108
|
+
keyFull = keyFull.toLowerCase();
|
|
109
|
+
let listeners = Array.from(registeredWatches).map(x => {
|
|
110
|
+
let props = Querysub.localRead(() => ({ ...x.props }));
|
|
111
|
+
let match = props.hotkeys?.find(x => {
|
|
112
|
+
let hotkey = x.toLowerCase();
|
|
113
|
+
if (hotkey.endsWith("+toggle")) {
|
|
114
|
+
hotkey = hotkey.slice(0, -"+toggle".length);
|
|
115
|
+
}
|
|
116
|
+
return hotkey === keyFull;
|
|
117
|
+
}) || "";
|
|
118
|
+
let hotkeyPriority = match && (
|
|
119
|
+
props.hotkeyPriority
|
|
120
|
+
?? Querysub.localRead(() => qreact.getAncestorProps(ButtonHotkeyRegion, x)?.hotkeyPriority)
|
|
121
|
+
?? Number.MIN_SAFE_INTEGER
|
|
122
|
+
) || Number.MIN_SAFE_INTEGER;
|
|
123
|
+
return { match, listener: x, props, hotkeyPriority };
|
|
124
|
+
}).filter(x => x.match);
|
|
125
|
+
if (!listeners.length) return;
|
|
126
|
+
console.log(`Triggering hotkey ${JSON.stringify(keyFull)} for ${listeners.length} listeners`);
|
|
127
|
+
e.preventDefault();
|
|
128
|
+
e.stopPropagation();
|
|
129
|
+
let maxPriority = Array.from(listeners).reduce((a, b) => Math.max(a, b.hotkeyPriority), Number.MIN_SAFE_INTEGER);
|
|
130
|
+
listeners = listeners.filter(x => x.hotkeyPriority === maxPriority);
|
|
131
|
+
if (listeners.length === 0) return;
|
|
132
|
+
listeners = listeners.filter((x, index) => x.props.callEvenIfRedundant || index === listeners.length - 1);
|
|
133
|
+
for (let { match, listener, props } of listeners) {
|
|
134
|
+
if (insideAnims.has(listener)) continue;
|
|
135
|
+
listener.element?.click();
|
|
136
|
+
if (!keyFull.includes("global+")) {
|
|
137
|
+
listener.element?.focus();
|
|
138
|
+
}
|
|
139
|
+
if (match.includes("+toggle")) {
|
|
140
|
+
function onKeyUp() {
|
|
141
|
+
keyUpListener.delete(onKeyUp);
|
|
142
|
+
listener.element?.click();
|
|
143
|
+
}
|
|
144
|
+
keyUpListener.add(onKeyUp);
|
|
145
|
+
}
|
|
146
|
+
if (Querysub.localRead(() => props.immediateRepeat)) {
|
|
147
|
+
insideAnims.add(listener);
|
|
148
|
+
let dead = false;
|
|
149
|
+
function onKeyUp() {
|
|
150
|
+
keyUpListener.delete(onKeyUp);
|
|
151
|
+
insideAnims.delete(listener);
|
|
152
|
+
dead = true;
|
|
153
|
+
}
|
|
154
|
+
keyUpListener.add(onKeyUp);
|
|
155
|
+
let prevFrame = Date.now();
|
|
156
|
+
function onFrame() {
|
|
157
|
+
if (dead) {
|
|
158
|
+
animationDuration = 0;
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
let now = Date.now();
|
|
162
|
+
animationDuration = now - prevFrame;
|
|
163
|
+
prevFrame = now;
|
|
164
|
+
listener.element?.click();
|
|
165
|
+
requestAnimationFrame(onFrame);
|
|
166
|
+
}
|
|
167
|
+
requestAnimationFrame(onFrame);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let animationDuration = 0;
|
|
175
|
+
// NOTE: This doesn't actually work, and I'm not sure why. Our tracked time between frames
|
|
176
|
+
// does not account for lag, even though we have more than enough precision to do it.
|
|
177
|
+
export function getAnimationFrames() {
|
|
178
|
+
let frames = Math.max(1, (animationDuration / 1000 * 120));
|
|
179
|
+
return frames;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export class Button extends qreact.Component<ButtonProps> {
|
|
183
|
+
onFocusText = "";
|
|
184
|
+
element: HTMLButtonElement | null = null;
|
|
185
|
+
componentDidMount(): void {
|
|
186
|
+
registeredWatches.add(this);
|
|
187
|
+
}
|
|
188
|
+
componentWillUnmount(): void {
|
|
189
|
+
registeredWatches.delete(this);
|
|
190
|
+
}
|
|
191
|
+
static renderInline(props: ButtonProps) { return true; }
|
|
192
|
+
render() {
|
|
193
|
+
let { square, flavor, children, className, style, hue, showHotkeys, hotkeys, hotkeyPosition: typeHotkeyPosition, ...props } = this.props;
|
|
194
|
+
|
|
195
|
+
delete props["class"];
|
|
196
|
+
|
|
197
|
+
let flavorOverrides: preact.JSX.CSSProperties = {};
|
|
198
|
+
if (square) {
|
|
199
|
+
flavorOverrides.padding = "1px";
|
|
200
|
+
}
|
|
201
|
+
if (flavor === "large") {
|
|
202
|
+
flavorOverrides = {
|
|
203
|
+
fontSize: 18,
|
|
204
|
+
padding: "10px 15px",
|
|
205
|
+
};
|
|
206
|
+
if (square) {
|
|
207
|
+
flavorOverrides.padding = "10px";
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (flavor === "small") {
|
|
211
|
+
flavorOverrides = {
|
|
212
|
+
fontSize: 12,
|
|
213
|
+
padding: "5px 10px",
|
|
214
|
+
};
|
|
215
|
+
if (square) {
|
|
216
|
+
flavorOverrides.padding = "5px";
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (flavor === "tiny") {
|
|
220
|
+
flavorOverrides = {
|
|
221
|
+
fontSize: 10,
|
|
222
|
+
padding: "3px 6px",
|
|
223
|
+
};
|
|
224
|
+
if (square) {
|
|
225
|
+
flavorOverrides.padding = "3px";
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let colorStyle = (
|
|
230
|
+
hue !== undefined &&
|
|
231
|
+
css.background(`hsl(${hue}, 50%, 50%)`, "soft")
|
|
232
|
+
.background(`hsl(${hue}, 50%, 40%)`, "active", "soft")
|
|
233
|
+
.border(`1px solid hsl(${hue}, 0%, 50%)`, "soft")
|
|
234
|
+
.outline(`3px solid hsl(204, 100%, 50%)`, "focus", "soft")
|
|
235
|
+
.color("white")
|
|
236
|
+
|| css.background("hsl(0, 0%, 39%)", "soft")
|
|
237
|
+
.background("hsl(0, 0%, 50%)", "hover", "soft")
|
|
238
|
+
.background("hsl(0, 0%, 50%)", "active", "soft")
|
|
239
|
+
.border("1px solid hsl(0, 0%, 50%)", "soft")
|
|
240
|
+
.outline("3px solid hsl(204, 100%, 50%)", "focus", "soft")
|
|
241
|
+
.color("white")
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
let type = this.props.typeOverride || this.props.flavor === "noui" && "div" || "button";
|
|
246
|
+
return qreact.createElement(
|
|
247
|
+
type,
|
|
248
|
+
{
|
|
249
|
+
...props,
|
|
250
|
+
className: (
|
|
251
|
+
(className || this.props.class || "")
|
|
252
|
+
+ " trigger-hover"
|
|
253
|
+
+ css.zIndex(1, "hover").position("relative", "soft")
|
|
254
|
+
+ (
|
|
255
|
+
flavor !== "noui" && (
|
|
256
|
+
colorStyle.display("flex", "soft")
|
|
257
|
+
.cursor("pointer")
|
|
258
|
+
.padding("4px 6px" as "1px")
|
|
259
|
+
.filter("brightness(1.1)", "hover", "soft")
|
|
260
|
+
)
|
|
261
|
+
)
|
|
262
|
+
),
|
|
263
|
+
style: flavor === "noui" ? {} : {
|
|
264
|
+
alignItems: "center",
|
|
265
|
+
userSelect: this.props.useLegacySelection ? "none" : undefined,
|
|
266
|
+
flexDirection: (
|
|
267
|
+
this.props.showHotkeys === "vertical" && "column"
|
|
268
|
+
|| this.props.showHotkeys === "reverse" && "row-reverse"
|
|
269
|
+
|| "row"
|
|
270
|
+
),
|
|
271
|
+
...flavorOverrides,
|
|
272
|
+
...style as any,
|
|
273
|
+
},
|
|
274
|
+
ref: (x: any) => this.element = x,
|
|
275
|
+
onMouseDown: ((e: MouseEvent) => {
|
|
276
|
+
// NOTE: THIS is the correct way to prevent selection. This prevent clicking
|
|
277
|
+
// on a button from accidentally selecting text (EVEN other text), while
|
|
278
|
+
// still allowing the user to select the button text if they want to.
|
|
279
|
+
if (!this.props.useLegacySelection) {
|
|
280
|
+
e.preventDefault();
|
|
281
|
+
}
|
|
282
|
+
return this.props.onMouseDown?.(e as any);
|
|
283
|
+
})
|
|
284
|
+
},
|
|
285
|
+
<>
|
|
286
|
+
{children}
|
|
287
|
+
{
|
|
288
|
+
<div class={
|
|
289
|
+
(this.props.hideHotkeys && " ")
|
|
290
|
+
|| showHotkeys && hotkeys?.length && css.marginLeft(10).vbox(6).pointerEvents("none")
|
|
291
|
+
|| (
|
|
292
|
+
css.absolute.zIndex(1).pointerEvents("none")
|
|
293
|
+
|
|
294
|
+
.whiteSpace("nowrap")
|
|
295
|
+
+ " show-on-hover"
|
|
296
|
+
+ (
|
|
297
|
+
typeHotkeyPosition === "left" && css.pos("0%", "50%").offset("-100%", "-50%")
|
|
298
|
+
|| typeHotkeyPosition === "above" && css.pos("50%", "0%").offset("-50%", "-100%")
|
|
299
|
+
|| css.pos("100%", "50%").offset("6px", "-50%")
|
|
300
|
+
)
|
|
301
|
+
)
|
|
302
|
+
}>
|
|
303
|
+
{this.props.hotkeys?.map((x, i) =>
|
|
304
|
+
<div class={
|
|
305
|
+
"" + (
|
|
306
|
+
css
|
|
307
|
+
.center
|
|
308
|
+
.height(showHotkeys ? 20 : 35)
|
|
309
|
+
.minWidth(showHotkeys ? 20 : 35)
|
|
310
|
+
.pad(0, 10)
|
|
311
|
+
.fontSize(14)
|
|
312
|
+
.color("hsl(0, 0%, 20%)")
|
|
313
|
+
.background("hsl(0, 0%, 90%)")
|
|
314
|
+
.background("hsl(0, 0%, 70%)", "hover")
|
|
315
|
+
.background("hsl(0, 0%, 60%)", "active")
|
|
316
|
+
.borderRadius(3)
|
|
317
|
+
.boxShadow("0px 2px 4px rgba(0, 0, 0, 0.5)")
|
|
318
|
+
.boxShadow("0px 2px 4px rgba(0, 0, 0, 0.9)", "active")
|
|
319
|
+
.transition("all 0.2s ease-in-out")
|
|
320
|
+
.whiteSpace("nowrap")
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
>
|
|
324
|
+
{formatHotkey(x)}
|
|
325
|
+
</div>
|
|
326
|
+
)}
|
|
327
|
+
</div>
|
|
328
|
+
}
|
|
329
|
+
</>
|
|
330
|
+
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function formatHotkey(key: string) {
|
|
336
|
+
key = key.replace(/\b\w/g, x => x.toUpperCase());
|
|
337
|
+
if (key === "Minus") {
|
|
338
|
+
return "‒";
|
|
339
|
+
}
|
|
340
|
+
if (key === "Equal") {
|
|
341
|
+
return "+";
|
|
342
|
+
}
|
|
343
|
+
return key;
|
|
344
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import preact from "preact"; import { qreact } from "../../src/4-dom/qreact";
|
|
2
|
+
import { Querysub } from "../../src/4-querysub/QuerysubController";
|
|
3
|
+
import { css } from "../../src/4-dom/css";
|
|
4
|
+
import { Button } from "../../src/library-components/Button";
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export class ButtonSelector<T> extends qreact.Component<{
|
|
8
|
+
title?: string;
|
|
9
|
+
value: T;
|
|
10
|
+
options: { value: T; title: preact.ComponentChild; isDefault?: boolean; hotkeys?: string[] }[];
|
|
11
|
+
onChange: (value: T) => void;
|
|
12
|
+
noPadding?: boolean;
|
|
13
|
+
noDefault?: boolean;
|
|
14
|
+
noUI?: boolean;
|
|
15
|
+
|
|
16
|
+
classWrapper?: string;
|
|
17
|
+
}> {
|
|
18
|
+
render() {
|
|
19
|
+
const { options, onChange, title } = this.props;
|
|
20
|
+
const selectedValue = this.props.value;
|
|
21
|
+
let selectedOption = (
|
|
22
|
+
options.find(o => o.value === selectedValue)
|
|
23
|
+
|| (!this.props.noDefault ? (options.find(o => o.isDefault) || options[0]) : undefined)
|
|
24
|
+
)?.value;
|
|
25
|
+
return (
|
|
26
|
+
<div class={css.hbox(2).wrap + this.props.classWrapper}>
|
|
27
|
+
{title && <div
|
|
28
|
+
class={
|
|
29
|
+
css.fontWeight("bold")
|
|
30
|
+
.hsl(0, 0, 25)
|
|
31
|
+
.border("1px solid hsl(0, 0%, 5%)").pad(4, 6)
|
|
32
|
+
.color("white")
|
|
33
|
+
}
|
|
34
|
+
>
|
|
35
|
+
{title}
|
|
36
|
+
</div>}
|
|
37
|
+
{options.map(({ value, title, hotkeys }) =>
|
|
38
|
+
<Button
|
|
39
|
+
style={{
|
|
40
|
+
background: (
|
|
41
|
+
this.props.noUI && "transparent"
|
|
42
|
+
|| selectedOption === value && "hsl(110, 75%, 40%)"
|
|
43
|
+
|| this.props.noPadding && "hsl(0, 0%, 40%)"
|
|
44
|
+
|| ""
|
|
45
|
+
),
|
|
46
|
+
border: this.props.noUI ? "none" : undefined,
|
|
47
|
+
}}
|
|
48
|
+
onClick={() => onChange(value)}
|
|
49
|
+
hotkeys={hotkeys}
|
|
50
|
+
showHotkeys
|
|
51
|
+
className={
|
|
52
|
+
css.button.flex
|
|
53
|
+
+ ((this.props.noPadding || this.props.noUI) && css.pad(0))
|
|
54
|
+
}
|
|
55
|
+
flavor={(this.props.noPadding || this.props.noUI) ? "noui" : undefined}
|
|
56
|
+
title={String(value)}
|
|
57
|
+
>
|
|
58
|
+
{title}
|
|
59
|
+
</Button>
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import preact from "preact"; import { qreact } from "../../src/4-dom/qreact";
|
|
2
|
+
|
|
3
|
+
import { Querysub } from "../../src/4-querysub/QuerysubController";
|
|
4
|
+
import { Icon } from "../../src/library-components/icons";
|
|
5
|
+
import { css } from "../../src/4-dom/css";
|
|
6
|
+
import { LengthOrPercentage } from "../../src/4-dom/cssTypes";
|
|
7
|
+
import { Button } from "../../src/library-components/Button";
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
export class DropdownCustom<T> extends qreact.Component<{
|
|
11
|
+
class?: string;
|
|
12
|
+
optionClass?: string;
|
|
13
|
+
title?: string;
|
|
14
|
+
value: T;
|
|
15
|
+
onChange: (value: T, index: number) => void;
|
|
16
|
+
maxWidth?: LengthOrPercentage;
|
|
17
|
+
options: { value: T; label: (isOpen: boolean) => preact.ComponentChild; }[];
|
|
18
|
+
}> {
|
|
19
|
+
state = {
|
|
20
|
+
isOpen: false,
|
|
21
|
+
tempIndexSelected: null as null | number,
|
|
22
|
+
};
|
|
23
|
+
base: HTMLElement | null = null;
|
|
24
|
+
componentDidMount(): void {
|
|
25
|
+
const handler = (e: MouseEvent) => {
|
|
26
|
+
Querysub.commit(() => {
|
|
27
|
+
if (!this.state.isOpen) return;
|
|
28
|
+
let el = e.target as HTMLElement | null;
|
|
29
|
+
while (el) {
|
|
30
|
+
if (el === this.base) return;
|
|
31
|
+
el = el.parentElement;
|
|
32
|
+
}
|
|
33
|
+
this.state.isOpen = false;
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
window.addEventListener("click", handler);
|
|
37
|
+
qreact.onUnmount(() => window.removeEventListener("click", handler));
|
|
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.state.tempIndexSelected ?? selectedIndex;
|
|
47
|
+
return (
|
|
48
|
+
<div class={css.absolute.width(this.props.maxWidth || "50vw")}>
|
|
49
|
+
<Button
|
|
50
|
+
class={css.opacity(0).visibility("hidden").absolute}
|
|
51
|
+
hotkeys={["Enter", "Escape"]}
|
|
52
|
+
onClick={() => {
|
|
53
|
+
if (this.state.tempIndexSelected !== null) {
|
|
54
|
+
this.props.onChange(options[this.state.tempIndexSelected].value, this.state.tempIndexSelected);
|
|
55
|
+
}
|
|
56
|
+
this.state.isOpen = false;
|
|
57
|
+
this.state.tempIndexSelected = null;
|
|
58
|
+
}}
|
|
59
|
+
/>
|
|
60
|
+
<Button
|
|
61
|
+
class={css.opacity(0).visibility("hidden").absolute}
|
|
62
|
+
hotkeys={["ArrowUp"]}
|
|
63
|
+
onClick={(e) => {
|
|
64
|
+
e.stopPropagation();
|
|
65
|
+
this.state.tempIndexSelected = (this.state.tempIndexSelected ?? selectedIndex) - 1;
|
|
66
|
+
if (this.state.tempIndexSelected < 0) this.state.tempIndexSelected = options.length - 1;
|
|
67
|
+
}}
|
|
68
|
+
/>
|
|
69
|
+
<Button
|
|
70
|
+
class={css.opacity(0).visibility("hidden").absolute}
|
|
71
|
+
hotkeys={["ArrowDown"]}
|
|
72
|
+
onClick={(e) => {
|
|
73
|
+
e.stopPropagation();
|
|
74
|
+
this.state.tempIndexSelected = (this.state.tempIndexSelected ?? selectedIndex) + 1;
|
|
75
|
+
if (this.state.tempIndexSelected >= options.length) this.state.tempIndexSelected = 0;
|
|
76
|
+
}}
|
|
77
|
+
/>
|
|
78
|
+
<div
|
|
79
|
+
class={
|
|
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 class={css.height(1).hsl(0, 0, 20)} />
|
|
94
|
+
}
|
|
95
|
+
<div
|
|
96
|
+
onClick={() => {
|
|
97
|
+
this.props.onChange(value, index);
|
|
98
|
+
this.state.isOpen = false;
|
|
99
|
+
this.state.tempIndexSelected = null;
|
|
100
|
+
}}
|
|
101
|
+
class={
|
|
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
|
+
ref={e => this.base = e}
|
|
126
|
+
className={(this.state.isOpen && css.zIndex(1)) + (this.props.class || "")}
|
|
127
|
+
>
|
|
128
|
+
{this.props.title && (
|
|
129
|
+
<div
|
|
130
|
+
style={{
|
|
131
|
+
fontWeight: "bold",
|
|
132
|
+
}}
|
|
133
|
+
onClick={() => this.setState({ isOpen: !this.state.isOpen })}
|
|
134
|
+
>
|
|
135
|
+
{this.props.title}
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
<div class={css.relative.vbox0.maxWidth(this.props.maxWidth)}>
|
|
139
|
+
<div
|
|
140
|
+
class={css.hbox(10).hsl(0, 0, 25).pad(4, 10).button + this.props.optionClass}
|
|
141
|
+
onClick={() => this.setState({ isOpen: !this.state.isOpen })}
|
|
142
|
+
>
|
|
143
|
+
{selectedItem?.label(false)}
|
|
144
|
+
{Icon.chevronDoubleDown()}
|
|
145
|
+
</div>
|
|
146
|
+
{this.state.isOpen && renderOptions()}
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import preact from "preact";
|
|
2
|
+
import { Querysub } from "../../src/4-querysub/QuerysubController";
|
|
3
|
+
import { qreact } from "../../src/4-dom/qreact";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export class DropdownSelector<T> extends qreact.Component<{
|
|
7
|
+
title?: string;
|
|
8
|
+
value: T;
|
|
9
|
+
onChange: (value: T) => void;
|
|
10
|
+
options: { value: T; label: string; }[];
|
|
11
|
+
}> {
|
|
12
|
+
render() {
|
|
13
|
+
const { options, value, title, onChange } = this.props;
|
|
14
|
+
let optionValues = options.map(o => String(o.value));
|
|
15
|
+
return (
|
|
16
|
+
<label style={{ display: "flex", gap: 10, alignItems: "center" }}>
|
|
17
|
+
{title && <span>{title}</span>}
|
|
18
|
+
<select
|
|
19
|
+
value={String(value)}
|
|
20
|
+
onChange={e => {
|
|
21
|
+
let option = options[optionValues.indexOf(e.currentTarget.value)];
|
|
22
|
+
onChange(option ? option.value : e.currentTarget.value as T);
|
|
23
|
+
}}
|
|
24
|
+
>
|
|
25
|
+
{options.map(({ value, label }) =>
|
|
26
|
+
<option value={String(value)}>{label}</option>
|
|
27
|
+
)}
|
|
28
|
+
</select>
|
|
29
|
+
</label>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|