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.
Files changed (83) hide show
  1. package/.prettierrc +18 -0
  2. package/README.md +9 -0
  3. package/api/server.ts +153 -0
  4. package/eslint.config.js +103 -0
  5. package/index.html +22 -0
  6. package/netlify.toml +9 -0
  7. package/nodemon.json +4 -0
  8. package/omnibot3000.code-workspace +55 -0
  9. package/package.json +58 -0
  10. package/pnpm-workspace.yaml +2 -0
  11. package/public/fonts/vt220.woff2 +0 -0
  12. package/src/App.module.css +128 -0
  13. package/src/App.tsx +193 -0
  14. package/src/Error.tsx +80 -0
  15. package/src/commons/OmnibotSpeak.module.css +43 -0
  16. package/src/commons/OmnibotSpeak.tsx +31 -0
  17. package/src/commons/api/api.ts +150 -0
  18. package/src/commons/constants.ts +34 -0
  19. package/src/commons/favicon.ts +69 -0
  20. package/src/commons/hooks/useConfig.tsx +50 -0
  21. package/src/commons/hooks/useKeyPress.tsx +76 -0
  22. package/src/commons/hooks/useStorage.tsx +38 -0
  23. package/src/commons/layout/Background.module.css +47 -0
  24. package/src/commons/layout/Background.tsx +138 -0
  25. package/src/commons/layout/Breadcrumb.module.css +28 -0
  26. package/src/commons/layout/Breadcrumb.tsx +54 -0
  27. package/src/commons/layout/Container.module.css +51 -0
  28. package/src/commons/layout/Container.tsx +60 -0
  29. package/src/commons/layout/Footer.module.css +36 -0
  30. package/src/commons/layout/Footer.tsx +74 -0
  31. package/src/commons/layout/Header.module.css +73 -0
  32. package/src/commons/layout/Header.tsx +102 -0
  33. package/src/commons/layout/Menu.module.css +36 -0
  34. package/src/commons/layout/Menu.tsx +37 -0
  35. package/src/commons/persona.txt +38 -0
  36. package/src/commons/styles/debug.css +71 -0
  37. package/src/commons/styles/main.css +221 -0
  38. package/src/commons/styles/vt220.css +69 -0
  39. package/src/commons/ui/Button.tsx +22 -0
  40. package/src/commons/ui/Caret.tsx +20 -0
  41. package/src/commons/ui/Line.tsx +64 -0
  42. package/src/commons/ui/ScrollSnap.tsx +51 -0
  43. package/src/commons/ui/Separator.tsx +19 -0
  44. package/src/commons/ui/Spacer.tsx +12 -0
  45. package/src/commons/utils/canvas.ts +20 -0
  46. package/src/commons/utils/color.ts +4 -0
  47. package/src/commons/utils/math.ts +43 -0
  48. package/src/commons/utils/strings.ts +47 -0
  49. package/src/commons/utils/styles.ts +11 -0
  50. package/src/commons/utils/system.ts +6 -0
  51. package/src/commons/utils/version.ts +24 -0
  52. package/src/features/chat/Chat.module.css +8 -0
  53. package/src/features/chat/Chat.tsx +188 -0
  54. package/src/features/chat/commons/strings.ts +6 -0
  55. package/src/features/chat/components/Message.module.css +28 -0
  56. package/src/features/chat/components/Message.tsx +45 -0
  57. package/src/features/chat/components/Toolbar.module.css +19 -0
  58. package/src/features/chat/components/Toolbar.tsx +44 -0
  59. package/src/features/chat/hooks/useChatCompletionStore.tsx +160 -0
  60. package/src/features/cli/Cli.module.css +75 -0
  61. package/src/features/cli/Cli.tsx +303 -0
  62. package/src/features/console/cmd.ts +93 -0
  63. package/src/features/console/config.ts +106 -0
  64. package/src/features/help/Help.module.css +8 -0
  65. package/src/features/help/Help.tsx +78 -0
  66. package/src/features/history/History.module.css +77 -0
  67. package/src/features/history/History.tsx +92 -0
  68. package/src/features/home/Home.module.css +26 -0
  69. package/src/features/home/Home.tsx +101 -0
  70. package/src/features/life/Life.module.css +8 -0
  71. package/src/features/life/Life.tsx +16 -0
  72. package/src/features/life/generation.ts +103 -0
  73. package/src/features/life/lifeforms.ts +138 -0
  74. package/src/features/life/types.ts +5 -0
  75. package/src/features/version/Version.module.css +8 -0
  76. package/src/features/version/Version.tsx +70 -0
  77. package/src/global.d.ts +10 -0
  78. package/src/main.tsx +32 -0
  79. package/src/vite-env.d.ts +16 -0
  80. package/tsconfig.api.json +16 -0
  81. package/tsconfig.json +47 -0
  82. package/upgrade.sh +22 -0
  83. 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
+ }