omnibot3000 1.8.2
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/.prettierrc +18 -0
- package/README.md +9 -0
- package/api/server.ts +153 -0
- package/eslint.config.js +103 -0
- package/index.html +22 -0
- package/netlify.toml +9 -0
- package/nodemon.json +4 -0
- package/omnibot3000.code-workspace +55 -0
- package/package.json +58 -0
- package/pnpm-workspace.yaml +2 -0
- package/public/fonts/vt220.woff2 +0 -0
- package/src/App.module.css +128 -0
- package/src/App.tsx +193 -0
- package/src/Error.tsx +80 -0
- package/src/commons/OmnibotSpeak.module.css +43 -0
- package/src/commons/OmnibotSpeak.tsx +31 -0
- package/src/commons/api/api.ts +150 -0
- package/src/commons/constants.ts +34 -0
- package/src/commons/favicon.ts +69 -0
- package/src/commons/hooks/useConfig.tsx +50 -0
- package/src/commons/hooks/useKeyPress.tsx +76 -0
- package/src/commons/hooks/useStorage.tsx +38 -0
- package/src/commons/layout/Background.module.css +47 -0
- package/src/commons/layout/Background.tsx +138 -0
- package/src/commons/layout/Breadcrumb.module.css +28 -0
- package/src/commons/layout/Breadcrumb.tsx +54 -0
- package/src/commons/layout/Container.module.css +51 -0
- package/src/commons/layout/Container.tsx +60 -0
- package/src/commons/layout/Footer.module.css +36 -0
- package/src/commons/layout/Footer.tsx +74 -0
- package/src/commons/layout/Header.module.css +73 -0
- package/src/commons/layout/Header.tsx +102 -0
- package/src/commons/layout/Menu.module.css +36 -0
- package/src/commons/layout/Menu.tsx +37 -0
- package/src/commons/persona.txt +38 -0
- package/src/commons/styles/debug.css +71 -0
- package/src/commons/styles/main.css +221 -0
- package/src/commons/styles/vt220.css +69 -0
- package/src/commons/ui/Button.tsx +22 -0
- package/src/commons/ui/Caret.tsx +20 -0
- package/src/commons/ui/Line.tsx +64 -0
- package/src/commons/ui/ScrollSnap.tsx +51 -0
- package/src/commons/ui/Separator.tsx +19 -0
- package/src/commons/ui/Spacer.tsx +12 -0
- package/src/commons/utils/canvas.ts +20 -0
- package/src/commons/utils/color.ts +4 -0
- package/src/commons/utils/math.ts +43 -0
- package/src/commons/utils/strings.ts +47 -0
- package/src/commons/utils/styles.ts +11 -0
- package/src/commons/utils/system.ts +6 -0
- package/src/commons/utils/version.ts +24 -0
- package/src/features/chat/Chat.module.css +8 -0
- package/src/features/chat/Chat.tsx +188 -0
- package/src/features/chat/commons/strings.ts +6 -0
- package/src/features/chat/components/Message.module.css +28 -0
- package/src/features/chat/components/Message.tsx +45 -0
- package/src/features/chat/components/Toolbar.module.css +19 -0
- package/src/features/chat/components/Toolbar.tsx +44 -0
- package/src/features/chat/hooks/useChatCompletionStore.tsx +160 -0
- package/src/features/cli/Cli.module.css +75 -0
- package/src/features/cli/Cli.tsx +303 -0
- package/src/features/console/cmd.ts +93 -0
- package/src/features/console/config.ts +106 -0
- package/src/features/help/Help.module.css +8 -0
- package/src/features/help/Help.tsx +78 -0
- package/src/features/history/History.module.css +77 -0
- package/src/features/history/History.tsx +92 -0
- package/src/features/home/Home.module.css +26 -0
- package/src/features/home/Home.tsx +101 -0
- package/src/features/life/Life.module.css +8 -0
- package/src/features/life/Life.tsx +16 -0
- package/src/features/life/generation.ts +103 -0
- package/src/features/life/lifeforms.ts +138 -0
- package/src/features/life/types.ts +5 -0
- package/src/features/version/Version.module.css +8 -0
- package/src/features/version/Version.tsx +70 -0
- package/src/global.d.ts +10 -0
- package/src/main.tsx +32 -0
- package/src/vite-env.d.ts +16 -0
- package/tsconfig.api.json +16 -0
- package/tsconfig.json +47 -0
- package/upgrade.sh +22 -0
- package/vite.config.ts +51 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import {useEffect, useState} from "react";
|
|
2
|
+
|
|
3
|
+
import {clamp} from "@utils/math";
|
|
4
|
+
|
|
5
|
+
export type Modifiers = {
|
|
6
|
+
shft?: boolean;
|
|
7
|
+
ctrl?: boolean;
|
|
8
|
+
alt?: boolean;
|
|
9
|
+
meta?: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type Detection = "all" | "keydown" | "keyup";
|
|
13
|
+
|
|
14
|
+
function useKeyPress(
|
|
15
|
+
targetKey: string,
|
|
16
|
+
modifiers: Modifiers,
|
|
17
|
+
detection: Detection = "all",
|
|
18
|
+
): number {
|
|
19
|
+
const [keyCount, setKeyCount] = useState(0);
|
|
20
|
+
|
|
21
|
+
const getModifiers = (e: KeyboardEvent): Modifiers => ({
|
|
22
|
+
shft: Boolean(e.shiftKey),
|
|
23
|
+
ctrl: Boolean(e.ctrlKey),
|
|
24
|
+
alt: Boolean(e.altKey),
|
|
25
|
+
meta: Boolean(e.metaKey),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const detectModifiers = (
|
|
29
|
+
sourceMod: Modifiers,
|
|
30
|
+
targetMod: Modifiers,
|
|
31
|
+
): boolean =>
|
|
32
|
+
Boolean(sourceMod.shft) === Boolean(targetMod.shft) &&
|
|
33
|
+
Boolean(sourceMod.ctrl) === Boolean(targetMod.ctrl) &&
|
|
34
|
+
Boolean(sourceMod.alt) === Boolean(targetMod.alt) &&
|
|
35
|
+
Boolean(sourceMod.meta) === Boolean(targetMod.meta);
|
|
36
|
+
|
|
37
|
+
const onKeyDownHandler = (key: string, mod: Modifiers): void => {
|
|
38
|
+
if (detectModifiers(modifiers, mod) && key === targetKey)
|
|
39
|
+
setKeyCount((n) => clamp(n + 1, 0, 16));
|
|
40
|
+
else setKeyCount(0);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const onKeyUpHandler = (key: string, mod: Modifiers): void => {
|
|
44
|
+
if (
|
|
45
|
+
!detectModifiers(modifiers, mod) ||
|
|
46
|
+
(detectModifiers(modifiers, mod) && key === targetKey)
|
|
47
|
+
)
|
|
48
|
+
setKeyCount(0);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (detection === "all" || detection === "keydown")
|
|
53
|
+
window.addEventListener("keydown", (e) =>
|
|
54
|
+
onKeyDownHandler(e.key, getModifiers(e)),
|
|
55
|
+
);
|
|
56
|
+
if (detection === "all" || detection === "keyup")
|
|
57
|
+
window.addEventListener("keyup", (e) =>
|
|
58
|
+
onKeyUpHandler(e.key, getModifiers(e)),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
return () => {
|
|
62
|
+
if (detection === "all" || detection === "keydown")
|
|
63
|
+
window.removeEventListener("keydown", (e) =>
|
|
64
|
+
onKeyDownHandler(e.key, getModifiers(e)),
|
|
65
|
+
);
|
|
66
|
+
if (detection === "all" || detection === "keyup")
|
|
67
|
+
window.removeEventListener("keyup", (e) =>
|
|
68
|
+
onKeyUpHandler(e.key, getModifiers(e)),
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
}, []); // create this event only on first run
|
|
72
|
+
|
|
73
|
+
return keyCount;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export default useKeyPress;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {SESSION_KEY} from "@commons/constants";
|
|
2
|
+
|
|
3
|
+
import useConfig from "@hooks/useConfig";
|
|
4
|
+
|
|
5
|
+
import useChatCompletionStore from "@chat/hooks/useChatCompletionStore";
|
|
6
|
+
|
|
7
|
+
function useStorage() {
|
|
8
|
+
const config = useConfig();
|
|
9
|
+
const chatStore = useChatCompletionStore();
|
|
10
|
+
|
|
11
|
+
const {debug} = config.getConfig();
|
|
12
|
+
|
|
13
|
+
const load = () => {
|
|
14
|
+
try {
|
|
15
|
+
const data = localStorage.getItem(`${SESSION_KEY}_data`);
|
|
16
|
+
if (data) {
|
|
17
|
+
chatStore.importData(JSON.parse(data));
|
|
18
|
+
if (debug) console.info("%cdata loaded", "color:#999");
|
|
19
|
+
}
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.error("failed to load:", error);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const save = () => {
|
|
26
|
+
try {
|
|
27
|
+
const data = chatStore.exportData();
|
|
28
|
+
localStorage.setItem(`${SESSION_KEY}_data`, JSON.stringify(data));
|
|
29
|
+
if (debug) console.info("%cdata saved", "color:#999");
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error("failed to save:", error);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return {load, save};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default useStorage;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
.root {
|
|
2
|
+
display: relative;
|
|
3
|
+
width: 100%;
|
|
4
|
+
height: 100%;
|
|
5
|
+
opacity: var(--opacity-ghosting);
|
|
6
|
+
z-index: var(--z-index-background);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.board {
|
|
10
|
+
position: absolute;
|
|
11
|
+
word-wrap: break-word;
|
|
12
|
+
white-space: pre-wrap;
|
|
13
|
+
opacity: 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.cursor {
|
|
17
|
+
position: absolute;
|
|
18
|
+
width: var(--font-width);
|
|
19
|
+
height: var(--line-height);
|
|
20
|
+
opacity: var(--opacity-ghosting);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.life {
|
|
24
|
+
animation: life var(--duration-lifespan) ease-out;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@keyframes life {
|
|
28
|
+
from {
|
|
29
|
+
opacity: 0;
|
|
30
|
+
}
|
|
31
|
+
to {
|
|
32
|
+
opacity: 1;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.death {
|
|
37
|
+
animation: death var(--duration-lifespan) ease-in;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@keyframes death {
|
|
41
|
+
from {
|
|
42
|
+
opacity: 1;
|
|
43
|
+
}
|
|
44
|
+
to {
|
|
45
|
+
opacity: 0;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import {useEffect, useRef, useState} from "react";
|
|
2
|
+
|
|
3
|
+
import {ASCII_CURRENCY} from "@commons/constants";
|
|
4
|
+
import styles from "@layout/Background.module.css";
|
|
5
|
+
import {vec2} from "@utils/math";
|
|
6
|
+
import {getCharWidth, getLineHeight} from "@utils/strings";
|
|
7
|
+
import {getVariableFromCSS} from "@utils/styles";
|
|
8
|
+
|
|
9
|
+
import useConfig from "@hooks/useConfig";
|
|
10
|
+
|
|
11
|
+
import {birth, init, render, tick} from "@life/generation";
|
|
12
|
+
import {Grid} from "@life/types";
|
|
13
|
+
import cls from "classnames";
|
|
14
|
+
|
|
15
|
+
const Background = (props: {w: number; h: number}) => {
|
|
16
|
+
const config = useConfig();
|
|
17
|
+
const {debug} = config.getConfig();
|
|
18
|
+
const {w, h} = props;
|
|
19
|
+
|
|
20
|
+
const [grid, setGrid] = useState<Grid>(init(w, h));
|
|
21
|
+
const [generation, setGeneration] = useState(0);
|
|
22
|
+
const [board, setBoard] = useState<string[]>(["", ""]);
|
|
23
|
+
const [cursor, setCursor] = useState<vec2>([0, 0]);
|
|
24
|
+
const [clicked, setClicked] = useState<boolean>(false);
|
|
25
|
+
|
|
26
|
+
const refBoard1 = useRef<HTMLDivElement>(null);
|
|
27
|
+
const refBoard2 = useRef<HTMLDivElement>(null);
|
|
28
|
+
const refCursor = useRef<HTMLDivElement>(null);
|
|
29
|
+
|
|
30
|
+
const board1 = refBoard1.current;
|
|
31
|
+
const board2 = refBoard2.current;
|
|
32
|
+
const cur = refCursor.current;
|
|
33
|
+
|
|
34
|
+
const mouseMoveHandler = (event: MouseEvent) => {
|
|
35
|
+
setCursor([event.clientX, event.clientY]);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const mouseDownHandler = () => {
|
|
39
|
+
setGeneration((n) => n + 1);
|
|
40
|
+
setClicked(true);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const mouseUpHandler = () => {
|
|
44
|
+
setClicked(false);
|
|
45
|
+
setGeneration((n) => n + 1);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
window.addEventListener("mousemove", mouseMoveHandler);
|
|
50
|
+
window.addEventListener("mousedown", mouseDownHandler);
|
|
51
|
+
window.addEventListener("mouseup", mouseUpHandler);
|
|
52
|
+
return () => {
|
|
53
|
+
window.removeEventListener("mousemove", mouseMoveHandler);
|
|
54
|
+
window.removeEventListener("mousedown", mouseDownHandler);
|
|
55
|
+
window.removeEventListener("mouseup", mouseUpHandler);
|
|
56
|
+
};
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
59
|
+
const update = () => {
|
|
60
|
+
if (!clicked) setGrid((grid) => tick(grid, w, h));
|
|
61
|
+
setBoard((board) => {
|
|
62
|
+
const b = [...board];
|
|
63
|
+
b[generation % 2] = render(grid, w);
|
|
64
|
+
return b;
|
|
65
|
+
});
|
|
66
|
+
if (!board1 || !board2) return;
|
|
67
|
+
if (generation % 2 === 0) {
|
|
68
|
+
board1.classList.add(styles.life);
|
|
69
|
+
board2.classList.add(styles.death);
|
|
70
|
+
board1.classList.remove(styles.death);
|
|
71
|
+
board2.classList.remove(styles.life);
|
|
72
|
+
} else {
|
|
73
|
+
board1.classList.add(styles.death);
|
|
74
|
+
board2.classList.add(styles.life);
|
|
75
|
+
board1.classList.remove(styles.life);
|
|
76
|
+
board2.classList.remove(styles.death);
|
|
77
|
+
}
|
|
78
|
+
/*if (generation % 100 === 0) setGrid((grid) => randomize(grid, 1, w, h));*/
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
const board = refBoard1.current;
|
|
83
|
+
if (!board) return;
|
|
84
|
+
const cw = getCharWidth();
|
|
85
|
+
const lh = getLineHeight();
|
|
86
|
+
const rect = board.getBoundingClientRect();
|
|
87
|
+
const bw = rect.width;
|
|
88
|
+
const bh = rect.height;
|
|
89
|
+
if (!bw || !bh) return;
|
|
90
|
+
const bx = cursor[0] - rect.left;
|
|
91
|
+
const by = cursor[1] - rect.top;
|
|
92
|
+
const x = Math.floor((bx / bw) * w);
|
|
93
|
+
const y = Math.floor((by / bh) * h);
|
|
94
|
+
if (clicked) {
|
|
95
|
+
setGrid((grid) => birth(grid, [[0, 0]], x, y, w, h));
|
|
96
|
+
update();
|
|
97
|
+
}
|
|
98
|
+
if (!cur) return;
|
|
99
|
+
cur.style.left = `${cw + x * cw}px`;
|
|
100
|
+
cur.style.top = `${cw + y * lh}px`;
|
|
101
|
+
cur.style.opacity = `var(--opacity-${clicked ? "tertiary" : "ghosting"})`;
|
|
102
|
+
document.body.style.cursor = clicked ? "pointer" : "default";
|
|
103
|
+
cur.style.visibility =
|
|
104
|
+
x >= 0 && x < bw / cw && y >= 0 && y < bh / lh ? "visible" : "hidden";
|
|
105
|
+
}, [cursor, clicked]);
|
|
106
|
+
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
update();
|
|
109
|
+
}, [generation]);
|
|
110
|
+
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
const lifespan = parseInt(getVariableFromCSS("lifespan")) || 0;
|
|
113
|
+
setGrid(tick(init(w, h), w, h));
|
|
114
|
+
const interval = setInterval(() => {
|
|
115
|
+
setGeneration((n) => n + 1);
|
|
116
|
+
}, lifespan);
|
|
117
|
+
if (debug) console.info(`%cresize grid: ${w} x ${h}`, "color:#999");
|
|
118
|
+
return () => clearInterval(interval);
|
|
119
|
+
}, [w, h]);
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<>
|
|
123
|
+
<div className={cls("ascii", styles.root)}>
|
|
124
|
+
<div ref={refBoard1} className={styles.board}>
|
|
125
|
+
{board[0]}
|
|
126
|
+
</div>
|
|
127
|
+
<div ref={refBoard2} className={styles.board}>
|
|
128
|
+
{board[1]}
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
<div ref={refCursor} className={cls("ascii", styles.cursor)}>
|
|
132
|
+
{ASCII_CURRENCY}
|
|
133
|
+
</div>
|
|
134
|
+
</>
|
|
135
|
+
);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export default Background;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
.root {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: row;
|
|
4
|
+
flex-grow: 1;
|
|
5
|
+
flex-shrink: 1;
|
|
6
|
+
align-items: center;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.separator {
|
|
10
|
+
opacity: var(--opacity-tertiary);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.path {
|
|
14
|
+
animation: fadein var(--duration-fade) forwards;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.button {
|
|
18
|
+
text-transform: uppercase;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@keyframes fadein {
|
|
22
|
+
from {
|
|
23
|
+
opacity: 0;
|
|
24
|
+
}
|
|
25
|
+
to {
|
|
26
|
+
opacity: 1;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {Fragment, memo, useEffect, useState} from "react";
|
|
2
|
+
import {useLocation, useNavigate} from "react-router-dom";
|
|
3
|
+
|
|
4
|
+
import {NAME} from "@commons/constants";
|
|
5
|
+
import styles from "@layout/Breadcrumb.module.css";
|
|
6
|
+
import Button from "@ui/Button";
|
|
7
|
+
|
|
8
|
+
import useChatCompletionStore from "@chat/hooks/useChatCompletionStore";
|
|
9
|
+
|
|
10
|
+
const Breadcrumb = () => {
|
|
11
|
+
const chatStore = useChatCompletionStore();
|
|
12
|
+
|
|
13
|
+
const location = useLocation();
|
|
14
|
+
const navigate = useNavigate();
|
|
15
|
+
|
|
16
|
+
const [path, setPath] = useState<string[]>([]);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const path = location.pathname.split("/").filter((v) => v.trim() !== "");
|
|
20
|
+
switch (path[0]) {
|
|
21
|
+
case "chat":
|
|
22
|
+
if (path[1]) path[1] = "id";
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
setPath(path);
|
|
26
|
+
document.title = `${NAME} /${path.join("/")}`;
|
|
27
|
+
}, [location]);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<nav className={styles.root}>
|
|
31
|
+
{path.map((v, i) => {
|
|
32
|
+
return (
|
|
33
|
+
<Fragment key={`breadcrumb-${i}`}>
|
|
34
|
+
<span className={styles.separator}>/</span>
|
|
35
|
+
{i < path.length - 1 ? (
|
|
36
|
+
<Button
|
|
37
|
+
className={styles.button}
|
|
38
|
+
name={v}
|
|
39
|
+
handler={() => {
|
|
40
|
+
if (v === "chat") chatStore.resetChat();
|
|
41
|
+
navigate(`/${path.slice(0, i + 1).join("/")}`);
|
|
42
|
+
}}
|
|
43
|
+
/>
|
|
44
|
+
) : (
|
|
45
|
+
<span>{v}</span>
|
|
46
|
+
)}
|
|
47
|
+
</Fragment>
|
|
48
|
+
);
|
|
49
|
+
})}
|
|
50
|
+
</nav>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export default memo(Breadcrumb);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
.root {
|
|
2
|
+
width: 100%;
|
|
3
|
+
height: fit-content;
|
|
4
|
+
overflow-x: hidden;
|
|
5
|
+
overflow-y: auto;
|
|
6
|
+
outline: none;
|
|
7
|
+
scroll-snap-type: both mandatory;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.container {
|
|
11
|
+
display: flex;
|
|
12
|
+
flex-direction: row;
|
|
13
|
+
ul {
|
|
14
|
+
margin-bottom: 0 !important;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.content {
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
flex-grow: 1;
|
|
22
|
+
flex-shrink: 1;
|
|
23
|
+
justify-content: end;
|
|
24
|
+
width: 100%;
|
|
25
|
+
max-width: calc((var(--content-width) - 3) * var(--font-width));
|
|
26
|
+
height: fit-content;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.content > :first-child {
|
|
30
|
+
div {
|
|
31
|
+
margin-top: 0 !important;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.snap {
|
|
36
|
+
flex-grow: 0;
|
|
37
|
+
flex-shrink: 0;
|
|
38
|
+
justify-content: end;
|
|
39
|
+
width: var(--font-width);
|
|
40
|
+
opacity: var(--opacity-secondary);
|
|
41
|
+
visibility: hidden;
|
|
42
|
+
list-style-type: none;
|
|
43
|
+
li {
|
|
44
|
+
scroll-snap-align: start;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
#end-of-line {
|
|
49
|
+
width: 100%;
|
|
50
|
+
max-width: calc((var(--content-width) - 3) * var(--font-width));
|
|
51
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {memo, ReactNode, useEffect, useRef} from "react";
|
|
2
|
+
|
|
3
|
+
import styles from "@layout/Container.module.css";
|
|
4
|
+
import {getCharWidth} from "@utils/strings";
|
|
5
|
+
|
|
6
|
+
import ScrollSnap from "../ui/ScrollSnap";
|
|
7
|
+
|
|
8
|
+
export const Container = ({children}: {children: ReactNode}) => {
|
|
9
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
10
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
11
|
+
|
|
12
|
+
const cw = getCharWidth();
|
|
13
|
+
|
|
14
|
+
const update = () => {
|
|
15
|
+
const content = contentRef.current;
|
|
16
|
+
if (!content) return;
|
|
17
|
+
|
|
18
|
+
const body = rootRef.current?.parentElement?.parentElement;
|
|
19
|
+
if (!body) return;
|
|
20
|
+
|
|
21
|
+
const bodyWidth = body.offsetWidth ?? 0;
|
|
22
|
+
const contentWidth = content.clientWidth ?? 0;
|
|
23
|
+
|
|
24
|
+
const n = Math.floor((bodyWidth - contentWidth) / 2 / cw);
|
|
25
|
+
body.style.paddingLeft = `calc(${n} * var(--font-width))`;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const resizeObserver = new ResizeObserver(update);
|
|
30
|
+
if (contentRef.current) resizeObserver.observe(contentRef.current);
|
|
31
|
+
return () => {
|
|
32
|
+
resizeObserver.disconnect();
|
|
33
|
+
};
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const eol = document.getElementById("end-of-line");
|
|
38
|
+
if (eol) {
|
|
39
|
+
eol.scrollIntoView({
|
|
40
|
+
behavior: "smooth",
|
|
41
|
+
block: "nearest",
|
|
42
|
+
inline: "nearest",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}, [children]);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div ref={rootRef} className={styles.root}>
|
|
49
|
+
<div className={styles.container}>
|
|
50
|
+
<ScrollSnap content={contentRef} className={styles.snap} />
|
|
51
|
+
<div ref={contentRef} className={styles.content}>
|
|
52
|
+
{children}
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
<a id="end-of-line" />
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export default memo(Container);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
.root {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: row;
|
|
4
|
+
flex-grow: 0;
|
|
5
|
+
flex-shrink: 0;
|
|
6
|
+
column-gap: var(--font-width);
|
|
7
|
+
align-items: start;
|
|
8
|
+
align-self: stretch;
|
|
9
|
+
padding-left: var(--font-width);
|
|
10
|
+
height: fit-content;
|
|
11
|
+
animation: fadein var(--duration-fade) forwards;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.spacing {
|
|
15
|
+
display: flex;
|
|
16
|
+
flex-direction: row;
|
|
17
|
+
column-gap: var(--font-width);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.copyright {
|
|
21
|
+
opacity: var(--opacity-tertiary);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.info {
|
|
25
|
+
opacity: var(--opacity-secondary);
|
|
26
|
+
word-break: break-all;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@keyframes fadein {
|
|
30
|
+
0% {
|
|
31
|
+
opacity: 0;
|
|
32
|
+
}
|
|
33
|
+
100% {
|
|
34
|
+
opacity: 1;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {RefObject} from "react";
|
|
2
|
+
import {useNavigate} from "react-router-dom";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
ASCII_COPYRIGHT,
|
|
6
|
+
ASCII_CURRENCY,
|
|
7
|
+
AUTHOR,
|
|
8
|
+
NAME,
|
|
9
|
+
VERSION,
|
|
10
|
+
} from "@commons/constants";
|
|
11
|
+
import Breadcrumb from "@layout/Breadcrumb";
|
|
12
|
+
import styles from "@layout/Footer.module.css";
|
|
13
|
+
import Button from "@ui/Button";
|
|
14
|
+
import Separator from "@ui/Separator";
|
|
15
|
+
import Spacer from "@ui/Spacer";
|
|
16
|
+
import {numberToRoman} from "@utils/math";
|
|
17
|
+
import {isDev} from "@utils/system";
|
|
18
|
+
|
|
19
|
+
import useConfig from "@hooks/useConfig";
|
|
20
|
+
|
|
21
|
+
import {RenderTime} from "@/App";
|
|
22
|
+
|
|
23
|
+
import cls from "classnames";
|
|
24
|
+
|
|
25
|
+
const Footer = (props: {renderTime: RefObject<RenderTime>}) => {
|
|
26
|
+
const {phase, duration} = props.renderTime.current;
|
|
27
|
+
|
|
28
|
+
const config = useConfig();
|
|
29
|
+
const {debug} = config.getConfig();
|
|
30
|
+
|
|
31
|
+
const navigate = useNavigate();
|
|
32
|
+
|
|
33
|
+
const versionHandler = () => {
|
|
34
|
+
navigate("/version");
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<footer className={cls("text", styles.root)}>
|
|
39
|
+
<div className={styles.spacing}>
|
|
40
|
+
<div>
|
|
41
|
+
<span className={styles.copyright}>{ASCII_COPYRIGHT}</span>
|
|
42
|
+
<span className={styles.info}>
|
|
43
|
+
{` ${numberToRoman(Number(new Date().getFullYear()))} `}
|
|
44
|
+
</span>
|
|
45
|
+
</div>
|
|
46
|
+
<a href={`${AUTHOR.url}/${NAME}`} target="_blank">
|
|
47
|
+
{AUTHOR.name}
|
|
48
|
+
</a>
|
|
49
|
+
</div>
|
|
50
|
+
<Separator />
|
|
51
|
+
<Breadcrumb />
|
|
52
|
+
<Spacer />
|
|
53
|
+
{isDev() && (
|
|
54
|
+
<>
|
|
55
|
+
<div className={styles.spacing}>
|
|
56
|
+
<span className={styles.info}>{`${phase}:`}</span>
|
|
57
|
+
<span style={{whiteSpace: "nowrap"}}>
|
|
58
|
+
{duration.toFixed(1)}
|
|
59
|
+
<span className={styles.info}>ms</span>
|
|
60
|
+
</span>
|
|
61
|
+
</div>
|
|
62
|
+
<Separator />
|
|
63
|
+
</>
|
|
64
|
+
)}
|
|
65
|
+
{debug && <div className={styles.info}>{ASCII_CURRENCY}</div>}
|
|
66
|
+
<div className={styles.spacing}>
|
|
67
|
+
<span className={styles.info}>ver </span>
|
|
68
|
+
<Button name={VERSION.join(".")} handler={versionHandler} />
|
|
69
|
+
</div>
|
|
70
|
+
</footer>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export default Footer;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
.root {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: row;
|
|
4
|
+
flex-grow: 0;
|
|
5
|
+
flex-shrink: 0;
|
|
6
|
+
column-gap: var(--font-width);
|
|
7
|
+
align-items: center;
|
|
8
|
+
align-self: stretch;
|
|
9
|
+
padding-left: var(--font-width);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.container {
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-direction: row;
|
|
15
|
+
column-gap: var(--font-width);
|
|
16
|
+
height: fit-content;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.title {
|
|
20
|
+
display: inline-block;
|
|
21
|
+
text-wrap: wrap;
|
|
22
|
+
text-overflow: ellipsis;
|
|
23
|
+
width: fit-content;
|
|
24
|
+
opacity: var(--opacity-primary);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.subtitle {
|
|
28
|
+
min-height: var(--line-height);
|
|
29
|
+
opacity: var(--opacity-secondary);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.subtext {
|
|
33
|
+
animation: fadein var(--duration-fade) forwards;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.gradient {
|
|
37
|
+
margin-left: var(--font-width);
|
|
38
|
+
margin-right: var(--font-width);
|
|
39
|
+
opacity: var(--opacity-tertiary);
|
|
40
|
+
text-transform: none;
|
|
41
|
+
user-select: none;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.avatar {
|
|
45
|
+
display: flex;
|
|
46
|
+
flex: row;
|
|
47
|
+
column-gap: var(--font-width);
|
|
48
|
+
align-items: flex-start;
|
|
49
|
+
opacity: 0;
|
|
50
|
+
animation: fadein var(--duration-fade) forwards;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.help {
|
|
54
|
+
height: var(--line-height);
|
|
55
|
+
opacity: var(--opacity-tertiary) !important;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.button {
|
|
59
|
+
flex-grow: 0;
|
|
60
|
+
flex-shrink: 0;
|
|
61
|
+
text-align: right;
|
|
62
|
+
width: fit-content;
|
|
63
|
+
height: fit-content;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@keyframes fadein {
|
|
67
|
+
from {
|
|
68
|
+
opacity: 0;
|
|
69
|
+
}
|
|
70
|
+
to {
|
|
71
|
+
opacity: 1;
|
|
72
|
+
}
|
|
73
|
+
}
|